From 6dad448ca6b7fb2a90d58e694a9c9228bb39230a Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Tue, 1 May 2018 16:22:34 -0500 Subject: [PATCH] [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 --- drydock_provisioner/drivers/node/driver.py | 3 +- .../drivers/node/maasdriver/actions/node.py | 108 ++++++++++++++++++ .../drivers/node/maasdriver/driver.py | 3 + .../node/maasdriver/models/repository.py | 41 +++++++ drydock_provisioner/objects/fields.py | 4 +- .../orchestrator/actions/orchestrator.py | 25 ++++ .../orchestrator/orchestrator.py | 5 +- .../postgres/test_action_config_node_prov.py | 32 ++++++ tests/unit/test_ingester.py | 3 +- 9 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 drydock_provisioner/drivers/node/maasdriver/models/repository.py create mode 100644 tests/integration/postgres/test_action_config_node_prov.py diff --git a/drydock_provisioner/drivers/node/driver.py b/drydock_provisioner/drivers/node/driver.py index fb00fd52..462e0da7 100644 --- a/drydock_provisioner/drivers/node/driver.py +++ b/drydock_provisioner/drivers/node/driver.py @@ -42,7 +42,8 @@ class NodeDriver(ProviderDriver): hd_fields.OrchestratorAction.ApplyNodePlatform, hd_fields.OrchestratorAction.DeployNode, hd_fields.OrchestratorAction.DestroyNode, - hd_fields.OrchestratorAction.ConfigureUserCredentials + hd_fields.OrchestratorAction.ConfigureUserCredentials, + hd_fields.OrchestratorAction.ConfigureNodeProvisioner ] def execute_task(self, task_id): diff --git a/drydock_provisioner/drivers/node/maasdriver/actions/node.py b/drydock_provisioner/drivers/node/maasdriver/actions/node.py index 08a16ce7..ed2ac26d 100644 --- a/drydock_provisioner/drivers/node/maasdriver/actions/node.py +++ b/drydock_provisioner/drivers/node/maasdriver/actions/node.py @@ -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.partition as maas_partition 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): @@ -618,6 +619,113 @@ class CreateNetworkTemplate(BaseMaasAction): 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): """Action for configuring user public keys.""" diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index 25973e51..ba2f6118 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -40,6 +40,7 @@ from .actions.node import ApplyNodeNetworking from .actions.node import ApplyNodePlatform from .actions.node import ApplyNodeStorage from .actions.node import DeployNode +from .actions.node import ConfigureNodeProvisioner class MaasNodeDriver(NodeDriver): @@ -87,6 +88,8 @@ class MaasNodeDriver(NodeDriver): ApplyNodeStorage, hd_fields.OrchestratorAction.DeployNode: DeployNode, + hd_fields.OrchestratorAction.ConfigureNodeProvisioner: + ConfigureNodeProvisioner, } def __init__(self, **kwargs): diff --git a/drydock_provisioner/drivers/node/maasdriver/models/repository.py b/drydock_provisioner/drivers/node/maasdriver/models/repository.py new file mode 100644 index 00000000..0ea49ae9 --- /dev/null +++ b/drydock_provisioner/drivers/node/maasdriver/models/repository.py @@ -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) diff --git a/drydock_provisioner/objects/fields.py b/drydock_provisioner/objects/fields.py index 81b81052..954cf5f3 100644 --- a/drydock_provisioner/objects/fields.py +++ b/drydock_provisioner/objects/fields.py @@ -47,6 +47,7 @@ class OrchestratorAction(BaseDrydockEnum): CreateStorageTemplate = 'create_storage_template' CreateBootMedia = 'create_boot_media' ConfigureUserCredentials = 'configure_user_credentials' + ConfigureNodeProvisioner = 'configure_node_provisioner' PrepareHardwareConfig = 'prepare_hardware_config' IdentifyNode = 'identify_node' ConfigureHardware = 'configure_hardware' @@ -69,7 +70,8 @@ class OrchestratorAction(BaseDrydockEnum): PowerCycleNode, InterrogateOob, CreateNetworkTemplate, CreateStorageTemplate, CreateBootMedia, PrepareHardwareConfig, ConfigureHardware, InterrogateNode, ApplyNodeNetworking, - ApplyNodeStorage, ApplyNodePlatform, DeployNode, DestroyNode) + ApplyNodeStorage, ApplyNodePlatform, DeployNode, DestroyNode, + ConfigureNodeProvisioner) class OrchestratorActionField(fields.BaseEnumField): diff --git a/drydock_provisioner/orchestrator/actions/orchestrator.py b/drydock_provisioner/orchestrator/actions/orchestrator.py index 96f9ba2d..fd90e3f0 100644 --- a/drydock_provisioner/orchestrator/actions/orchestrator.py +++ b/drydock_provisioner/orchestrator/actions/orchestrator.py @@ -305,12 +305,37 @@ class PrepareSite(BaseAction): self.step_networktemplate(driver) self.step_usercredentials(driver) + self.step_configureprovisioner(driver) self.task.align_result() self.task.set_status(hd_fields.TaskStatus.Complete) self.task.save() 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): """Run the CreateNetworkTemplate step of this action. diff --git a/drydock_provisioner/orchestrator/orchestrator.py b/drydock_provisioner/orchestrator/orchestrator.py index 044d5ca3..5e3bd7bb 100644 --- a/drydock_provisioner/orchestrator/orchestrator.py +++ b/drydock_provisioner/orchestrator/orchestrator.py @@ -379,7 +379,10 @@ class Orchestrator(object): for ba in site_design.bootactions: nf = ba.node_filter 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): try: diff --git a/tests/integration/postgres/test_action_config_node_prov.py b/tests/integration/postgres/test_action_config_node_prov.py new file mode 100644 index 00000000..371baaea --- /dev/null +++ b/tests/integration/postgres/test_action_config_node_prov.py @@ -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) diff --git a/tests/unit/test_ingester.py b/tests/unit/test_ingester.py index 613c80c1..9b9404a6 100644 --- a/tests/unit/test_ingester.py +++ b/tests/unit/test_ingester.py @@ -31,7 +31,8 @@ class TestClass(object): assert len(design_data.host_profiles) == 2 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.""" input_file = input_files.join("deckhand_fullsite.yaml")