[411390] Configure repo in MAAS

- Add a new action for ConfigureNodeProvisioner to configure a node
  provisioner with site-wide configuration
- Add a maasdriver action to configure repositories

Change-Id: I8e216a269b300159b7cc26c3a4542e8b61496dc7
This commit is contained in:
Scott Hussey 2018-05-01 16:22:34 -05:00 committed by Scott Hussey
parent cc77125953
commit 6dad448ca6
9 changed files with 220 additions and 4 deletions

View File

@ -42,7 +42,8 @@ class NodeDriver(ProviderDriver):
hd_fields.OrchestratorAction.ApplyNodePlatform, hd_fields.OrchestratorAction.ApplyNodePlatform,
hd_fields.OrchestratorAction.DeployNode, hd_fields.OrchestratorAction.DeployNode,
hd_fields.OrchestratorAction.DestroyNode, hd_fields.OrchestratorAction.DestroyNode,
hd_fields.OrchestratorAction.ConfigureUserCredentials hd_fields.OrchestratorAction.ConfigureUserCredentials,
hd_fields.OrchestratorAction.ConfigureNodeProvisioner
] ]
def execute_task(self, task_id): def execute_task(self, task_id):

View File

@ -38,6 +38,7 @@ import drydock_provisioner.drivers.node.maasdriver.models.boot_resource as maas_
import drydock_provisioner.drivers.node.maasdriver.models.rack_controller as maas_rack import drydock_provisioner.drivers.node.maasdriver.models.rack_controller as maas_rack
import drydock_provisioner.drivers.node.maasdriver.models.partition as maas_partition import drydock_provisioner.drivers.node.maasdriver.models.partition as maas_partition
import drydock_provisioner.drivers.node.maasdriver.models.volumegroup as maas_vg import drydock_provisioner.drivers.node.maasdriver.models.volumegroup as maas_vg
import drydock_provisioner.drivers.node.maasdriver.models.repository as maas_repo
class BaseMaasAction(BaseAction): class BaseMaasAction(BaseAction):
@ -618,6 +619,113 @@ class CreateNetworkTemplate(BaseMaasAction):
return return
class ConfigureNodeProvisioner(BaseMaasAction):
"""Action for configuring site-wide node provisioner options."""
def start(self):
self.task.set_status(hd_fields.TaskStatus.Running)
self.task.save()
try:
site_design = self._load_site_design()
except errors.OrchestratorError:
self.task.add_status_msg(
msg="Error loading site design.",
error=True,
ctx='NA',
ctx_type='NA')
self.task.set_status(hd_fields.TaskStatus.Complete)
self.task.failure()
self.task.save()
return
try:
current_repos = maas_repo.Repositories(self.maas_client)
current_repos.refresh()
except Exception as ex:
self.logger.debug("Error accessing the MaaS API.", exc_info=ex)
self.task.set_status(hd_fields.TaskStatus.Complete)
self.task.failure()
self.task.add_status_msg(
msg='Error accessing MaaS SshKeys API',
error=True,
ctx='NA',
ctx_type='NA')
self.task.save()
return
site_model = site_design.get_site()
repo_list = getattr(site_model, 'repositories', None) or []
if repo_list:
for r in repo_list:
try:
existing_repo = current_repos.singleton({
'name': r.get_id()
})
new_repo = self.create_maas_repo(self.maas_client, r)
if existing_repo:
new_repo.resource_id = existing_repo.resource_id
new_repo.update()
msg = "Updating repository definition for %s." % (
r.name)
self.logger.debug(msg)
self.task.add_status_msg(
msg=msg, error=False, ctx='NA', ctx_type='NA')
self.task.success()
else:
new_repo = current_repos.add(new_repo)
msg = "Adding repository definition for %s." % (r.name)
self.logger.debug(msg)
self.task.add_status_msg(
msg=msg, error=False, ctx='NA', ctx_type='NA')
self.task.success()
except Exception as ex:
msg = "Error adding repository to MaaS configuration: %s" % str(
ex)
self.logger.warning(msg)
self.task.add_status_msg(
msg=msg, error=True, ctx='NA', ctx_type='NA')
self.task.failure()
else:
msg = ("No repositories to add, no work to do.")
self.logger.debug(msg)
self.task.success()
self.task.add_status_msg(
msg=msg, error=False, ctx='NA', ctx_type='NA')
self.task.set_status(hd_fields.TaskStatus.Complete)
self.task.save()
return
@staticmethod
def create_maas_repo(api_client, repo_obj):
"""Create a MAAS model of a repo based of a Drydock model.
If resource_id is specified, assign it the resource_id so the new instance
can be used to update an existing repo.
:param api_client: A MAAS API client configured to connect to MAAS
:param repo_obj: Instance of objects.Repository
"""
model_fields = dict()
if repo_obj.distributions:
model_fields['distributions'] = ','.join(repo_obj.distributions)
if repo_obj.components:
model_fields['components'] = ','.join(repo_obj.components)
if repo_obj.arches:
model_fields['arches'] = ','.join(repo_obj.arches)
model_fields['key'] = repo_obj.gpgkey
for k in ['name', 'url']:
model_fields[k] = getattr(repo_obj, k)
repo_model = maas_repo.Repository(api_client, **model_fields)
return repo_model
class ConfigureUserCredentials(BaseMaasAction): class ConfigureUserCredentials(BaseMaasAction):
"""Action for configuring user public keys.""" """Action for configuring user public keys."""

View File

@ -40,6 +40,7 @@ from .actions.node import ApplyNodeNetworking
from .actions.node import ApplyNodePlatform from .actions.node import ApplyNodePlatform
from .actions.node import ApplyNodeStorage from .actions.node import ApplyNodeStorage
from .actions.node import DeployNode from .actions.node import DeployNode
from .actions.node import ConfigureNodeProvisioner
class MaasNodeDriver(NodeDriver): class MaasNodeDriver(NodeDriver):
@ -87,6 +88,8 @@ class MaasNodeDriver(NodeDriver):
ApplyNodeStorage, ApplyNodeStorage,
hd_fields.OrchestratorAction.DeployNode: hd_fields.OrchestratorAction.DeployNode:
DeployNode, DeployNode,
hd_fields.OrchestratorAction.ConfigureNodeProvisioner:
ConfigureNodeProvisioner,
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):

View File

@ -0,0 +1,41 @@
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Model for MaaS Package Repository resources."""
import drydock_provisioner.drivers.node.maasdriver.models.base as model_base
class Repository(model_base.ResourceBase):
resource_url = 'package-repositories/{resource_id}/'
fields = [
'resource_id', 'name', 'url', 'distributions', 'components', 'arches',
'key', 'enabled'
]
json_fields = [
'name', 'url', 'distributions', 'components', 'arches', 'key',
'enabled'
]
def __init__(self, api_client, **kwargs):
super().__init__(api_client, **kwargs)
class Repositories(model_base.ResourceCollectionBase):
collection_url = 'package-repositories/'
collection_resource = Repository
def __init__(self, api_client):
super().__init__(api_client)

View File

@ -47,6 +47,7 @@ class OrchestratorAction(BaseDrydockEnum):
CreateStorageTemplate = 'create_storage_template' CreateStorageTemplate = 'create_storage_template'
CreateBootMedia = 'create_boot_media' CreateBootMedia = 'create_boot_media'
ConfigureUserCredentials = 'configure_user_credentials' ConfigureUserCredentials = 'configure_user_credentials'
ConfigureNodeProvisioner = 'configure_node_provisioner'
PrepareHardwareConfig = 'prepare_hardware_config' PrepareHardwareConfig = 'prepare_hardware_config'
IdentifyNode = 'identify_node' IdentifyNode = 'identify_node'
ConfigureHardware = 'configure_hardware' ConfigureHardware = 'configure_hardware'
@ -69,7 +70,8 @@ class OrchestratorAction(BaseDrydockEnum):
PowerCycleNode, InterrogateOob, CreateNetworkTemplate, PowerCycleNode, InterrogateOob, CreateNetworkTemplate,
CreateStorageTemplate, CreateBootMedia, PrepareHardwareConfig, CreateStorageTemplate, CreateBootMedia, PrepareHardwareConfig,
ConfigureHardware, InterrogateNode, ApplyNodeNetworking, ConfigureHardware, InterrogateNode, ApplyNodeNetworking,
ApplyNodeStorage, ApplyNodePlatform, DeployNode, DestroyNode) ApplyNodeStorage, ApplyNodePlatform, DeployNode, DestroyNode,
ConfigureNodeProvisioner)
class OrchestratorActionField(fields.BaseEnumField): class OrchestratorActionField(fields.BaseEnumField):

View File

@ -305,12 +305,37 @@ class PrepareSite(BaseAction):
self.step_networktemplate(driver) self.step_networktemplate(driver)
self.step_usercredentials(driver) self.step_usercredentials(driver)
self.step_configureprovisioner(driver)
self.task.align_result() self.task.align_result()
self.task.set_status(hd_fields.TaskStatus.Complete) self.task.set_status(hd_fields.TaskStatus.Complete)
self.task.save() self.task.save()
return return
def step_configureprovisioner(self, driver):
"""Run the ConfigureNodeProvisioner step of this action.
:param driver: The driver instance to use for execution.
"""
config_prov_task = self.orchestrator.create_task(
design_ref=self.task.design_ref,
action=hd_fields.OrchestratorAction.ConfigureNodeProvisioner)
self.task.register_subtask(config_prov_task)
self.logger.info(
"Starting node drvier task %s to configure the provisioner" %
(config_prov_task.get_id()))
driver.execute_task(config_prov_task.get_id())
self.task.add_status_msg(
msg="Collected subtask %s" % str(config_prov_task.get_id()),
error=False,
ctx=str(config_prov_task.get_id()),
ctx_type='task')
self.logger.info("Node driver task %s:%s is complete." %
(config_prov_task.get_id(), config_prov_task.action))
def step_networktemplate(self, driver): def step_networktemplate(self, driver):
"""Run the CreateNetworkTemplate step of this action. """Run the CreateNetworkTemplate step of this action.

View File

@ -379,7 +379,10 @@ class Orchestrator(object):
for ba in site_design.bootactions: for ba in site_design.bootactions:
nf = ba.node_filter nf = ba.node_filter
target_nodes = self.process_node_filter(nf, site_design) target_nodes = self.process_node_filter(nf, site_design)
ba.target_nodes = [x.get_id() for x in target_nodes] if not target_nodes:
ba.target_nodes = []
else:
ba.target_nodes = [x.get_id() for x in target_nodes]
def process_node_filter(self, node_filter, site_design): def process_node_filter(self, node_filter, site_design):
try: try:

View File

@ -0,0 +1,32 @@
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Testing the ConfigureNodeProvisioner action."""
from drydock_provisioner import objects
from drydock_provisioner.drivers.node.maasdriver.actions.node import ConfigureNodeProvisioner
class TestActionConfigureNodeProvisioner(object):
def test_create_maas_repo(selfi, mocker):
distribution_list = ['xenial', 'xenial-updates']
repo_obj = objects.Repository(name='foo',
url='https://foo.com/repo',
repo_type='apt',
gpgkey="-----START STUFF----\nSTUFF\n-----END STUFF----\n",
distributions=distribution_list,
components=['main'])
maas_model = ConfigureNodeProvisioner.create_maas_repo(mocker.MagicMock(), repo_obj)
assert maas_model.distributions == ",".join(distribution_list)

View File

@ -31,7 +31,8 @@ class TestClass(object):
assert len(design_data.host_profiles) == 2 assert len(design_data.host_profiles) == 2
assert len(design_data.baremetal_nodes) == 3 assert len(design_data.baremetal_nodes) == 3
def test_ingest_deckhand_repos(self, input_files, setup, deckhand_ingester): def test_ingest_deckhand_repos(self, input_files, setup,
deckhand_ingester):
"""Test that the ingester properly parses repo definitions.""" """Test that the ingester properly parses repo definitions."""
input_file = input_files.join("deckhand_fullsite.yaml") input_file = input_files.join("deckhand_fullsite.yaml")