[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.DeployNode,
hd_fields.OrchestratorAction.DestroyNode,
hd_fields.OrchestratorAction.ConfigureUserCredentials
hd_fields.OrchestratorAction.ConfigureUserCredentials,
hd_fields.OrchestratorAction.ConfigureNodeProvisioner
]
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.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."""

View File

@ -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):

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'
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):

View File

@ -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.

View File

@ -379,6 +379,9 @@ class Orchestrator(object):
for ba in site_design.bootactions:
nf = ba.node_filter
target_nodes = self.process_node_filter(nf, site_design)
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):

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.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")