From 80b3a1e99d8cc72c509f46fa8822658135754a26 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Thu, 15 Jun 2017 22:42:53 -0500 Subject: [PATCH 01/16] Orchestration of MaaS enlistment (#42) * WIP - Initial API implementation with Falcon * API service for /designs and /tasks endpoints REST API using falcon library Middleware for authentication (stubbed until Keystone is avail) Middleware for context and logging Request logging and initial error logging README updates * Cleanup readme formatting * Rename helm_drydock to drydock_provisioner * Fixed missing except statement * Fixed missing except statement * Reorganize API paths in a list Fix duplication of context init Add API version prefix to URLs * DRYD-2 MVP - phase 1 - node enlistment Add node driver task for IdentifyNode Implement MaaS API interface for Machines and Interfaces --- README.md | 9 +- .../__init__.py | 0 .../config.py | 17 +- .../control}/__init__.py | 0 drydock_provisioner/control/api.py | 51 ++ drydock_provisioner/control/base.py | 144 ++++++ drydock_provisioner/control/designs.py | 164 +++++++ drydock_provisioner/control/middleware.py | 92 ++++ drydock_provisioner/control/readme.md | 30 ++ drydock_provisioner/control/tasks.py | 79 ++++ .../drivers/__init__.py | 8 +- .../drivers/node/__init__.py | 7 +- .../drivers/node/maasdriver}/__init__.py | 2 +- .../drivers/node/maasdriver/api_client.py | 0 .../drivers/node/maasdriver/driver.py | 447 ++++++++++++++++++ .../node/maasdriver/models}/__init__.py | 0 .../drivers/node/maasdriver/models/base.py | 43 +- .../drivers/node/maasdriver/models/fabric.py | 4 +- .../node/maasdriver/models/interface.py | 34 ++ .../drivers/node/maasdriver/models/machine.py | 185 ++++++++ .../drivers/node/maasdriver/models/subnet.py | 2 +- .../drivers/node/maasdriver/models/vlan.py | 4 +- .../drivers/node/maasdriver/readme.md | 0 .../drivers/oob/__init__.py | 8 +- .../drivers/oob/pyghmi_driver/__init__.py | 36 +- .../drivers/readme.md | 1 + drydock_provisioner/drydock.py | 55 +++ .../error.py | 6 + drydock_provisioner/ingester/__init__.py | 120 +++++ .../ingester/plugins/__init__.py | 0 .../ingester/plugins/yaml.py | 15 +- .../ingester/readme.md | 2 +- .../introspection/readme.md | 0 .../objects/__init__.py | 12 +- .../objects/base.py | 50 +- .../objects/fields.py | 1 + .../objects/hostprofile.py | 6 +- .../objects/hwprofile.py | 6 +- .../objects/network.py | 8 +- .../objects/node.py | 12 +- .../objects/readme.md | 0 .../objects/site.py | 23 +- .../objects/task.py | 42 +- .../orchestrator/__init__.py | 157 ++++-- .../orchestrator/readme.md | 0 .../statemgmt/__init__.py | 9 +- .../statemgmt/readme.md | 0 examples/designparts_v1.0.yaml | 4 +- helm_drydock/control/readme.md | 14 - .../drivers/node/maasdriver/driver.py | 306 ------------ helm_drydock/ingester/__init__.py | 107 ----- setup.py | 33 +- tests/integration/test_maasdriver_client.py | 4 +- tests/integration/test_maasdriver_network.py | 8 +- tests/integration/test_orch_node_networks.py | 26 +- tests/unit/test_design_inheritance.py | 12 +- tests/unit/test_ingester.py | 12 +- tests/unit/test_ingester_yaml.py | 2 +- tests/unit/test_models.py | 12 +- tests/unit/test_orch_generic.py | 10 +- tests/unit/test_orch_oob.py | 20 +- tests/unit/test_statemgmt.py | 4 +- 62 files changed, 1822 insertions(+), 643 deletions(-) rename {helm_drydock => drydock_provisioner}/__init__.py (100%) rename {helm_drydock => drydock_provisioner}/config.py (59%) rename {helm_drydock/drivers/node/maasdriver => drydock_provisioner/control}/__init__.py (100%) create mode 100644 drydock_provisioner/control/api.py create mode 100644 drydock_provisioner/control/base.py create mode 100644 drydock_provisioner/control/designs.py create mode 100644 drydock_provisioner/control/middleware.py create mode 100644 drydock_provisioner/control/readme.md create mode 100644 drydock_provisioner/control/tasks.py rename {helm_drydock => drydock_provisioner}/drivers/__init__.py (94%) rename {helm_drydock => drydock_provisioner}/drivers/node/__init__.py (90%) rename {helm_drydock/drivers/node/maasdriver/models => drydock_provisioner/drivers/node/maasdriver}/__init__.py (94%) rename {helm_drydock => drydock_provisioner}/drivers/node/maasdriver/api_client.py (100%) create mode 100644 drydock_provisioner/drivers/node/maasdriver/driver.py rename {helm_drydock/control => drydock_provisioner/drivers/node/maasdriver/models}/__init__.py (100%) rename {helm_drydock => drydock_provisioner}/drivers/node/maasdriver/models/base.py (87%) rename {helm_drydock => drydock_provisioner}/drivers/node/maasdriver/models/fabric.py (88%) create mode 100644 drydock_provisioner/drivers/node/maasdriver/models/interface.py create mode 100644 drydock_provisioner/drivers/node/maasdriver/models/machine.py rename {helm_drydock => drydock_provisioner}/drivers/node/maasdriver/models/subnet.py (96%) rename {helm_drydock => drydock_provisioner}/drivers/node/maasdriver/models/vlan.py (95%) rename {helm_drydock => drydock_provisioner}/drivers/node/maasdriver/readme.md (100%) rename {helm_drydock => drydock_provisioner}/drivers/oob/__init__.py (87%) rename {helm_drydock => drydock_provisioner}/drivers/oob/pyghmi_driver/__init__.py (92%) rename {helm_drydock => drydock_provisioner}/drivers/readme.md (93%) create mode 100644 drydock_provisioner/drydock.py rename {helm_drydock => drydock_provisioner}/error.py (92%) create mode 100644 drydock_provisioner/ingester/__init__.py rename {helm_drydock => drydock_provisioner}/ingester/plugins/__init__.py (100%) rename {helm_drydock => drydock_provisioner}/ingester/plugins/yaml.py (97%) rename {helm_drydock => drydock_provisioner}/ingester/readme.md (84%) rename {helm_drydock => drydock_provisioner}/introspection/readme.md (100%) rename {helm_drydock => drydock_provisioner}/objects/__init__.py (93%) rename {helm_drydock => drydock_provisioner}/objects/base.py (59%) rename {helm_drydock => drydock_provisioner}/objects/fields.py (99%) rename {helm_drydock => drydock_provisioner}/objects/hostprofile.py (98%) rename {helm_drydock => drydock_provisioner}/objects/hwprofile.py (96%) rename {helm_drydock => drydock_provisioner}/objects/network.py (94%) rename {helm_drydock => drydock_provisioner}/objects/node.py (92%) rename {helm_drydock => drydock_provisioner}/objects/readme.md (100%) rename {helm_drydock => drydock_provisioner}/objects/site.py (93%) rename {helm_drydock => drydock_provisioner}/objects/task.py (70%) rename {helm_drydock => drydock_provisioner}/orchestrator/__init__.py (71%) rename {helm_drydock => drydock_provisioner}/orchestrator/readme.md (100%) rename {helm_drydock => drydock_provisioner}/statemgmt/__init__.py (97%) rename {helm_drydock => drydock_provisioner}/statemgmt/readme.md (100%) delete mode 100644 helm_drydock/control/readme.md delete mode 100644 helm_drydock/drivers/node/maasdriver/driver.py delete mode 100644 helm_drydock/ingester/__init__.py diff --git a/README.md b/README.md index dc619477..8e2f0347 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ -# helm_drydock +# drydock_provisioner A python REST orchestrator to translate a YAML host topology to a provisioned set of hosts and provide a set of cloud-init post-provisioning instructions. +To run: + + $ virtualenv -p python3 /var/tmp/drydock + $ . /var/tmp/drydock/bin/activate + $ python setup.py install + $ uwsgi --http :9000 -w drydock_provisioner.drydock --callable drydock --enable-threads -L + ## Modular service ### Design Consumer ### diff --git a/helm_drydock/__init__.py b/drydock_provisioner/__init__.py similarity index 100% rename from helm_drydock/__init__.py rename to drydock_provisioner/__init__.py diff --git a/helm_drydock/config.py b/drydock_provisioner/config.py similarity index 59% rename from helm_drydock/config.py rename to drydock_provisioner/config.py index 244e85c0..f64e91e9 100644 --- a/helm_drydock/config.py +++ b/drydock_provisioner/config.py @@ -21,13 +21,24 @@ class DrydockConfig(object): + global_config = { + 'log_level': 'DEBUG', + } + node_driver = { 'maasdriver': { - 'api_key': 'KTMHgA42cNSMnfmJ82:cdg4yQUhp542aHsCTV:7Dc2KB9hQpWq3LfQAAAKAj6wdg22yWxZ', - 'api_url': 'http://localhost:5240/MAAS/api/2.0/' + 'api_key': 'UTBfxGL69XWjaffQek:NuKZSYGuBs6ZpYC6B9:byvXBgY8CsW5VQKxGdQjvJXtjXwr5G4U', + 'api_url': 'http://10.23.19.16:30773/MAAS/api/2.0/', }, } ingester_config = { - 'plugins': ['helm_drydock.ingester.plugins.yaml'] + 'plugins': ['drydock_provisioner.ingester.plugins.yaml.YamlIngester'], + } + + orchestrator_config = { + 'drivers': { + 'oob': 'drydock_provisioner.drivers.oob.pyghmi_driver.PyghmiDriver', + 'node': 'drydock_provisioner.drivers.node.maasdriver.driver.MaasNodeDriver', + } } \ No newline at end of file diff --git a/helm_drydock/drivers/node/maasdriver/__init__.py b/drydock_provisioner/control/__init__.py similarity index 100% rename from helm_drydock/drivers/node/maasdriver/__init__.py rename to drydock_provisioner/control/__init__.py diff --git a/drydock_provisioner/control/api.py b/drydock_provisioner/control/api.py new file mode 100644 index 00000000..a6d832f9 --- /dev/null +++ b/drydock_provisioner/control/api.py @@ -0,0 +1,51 @@ +# Copyright 2017 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. +import falcon + +from .designs import * +from .tasks import * + +from .base import DrydockRequest +from .middleware import AuthMiddleware, ContextMiddleware, LoggingMiddleware + +def start_api(state_manager=None, ingester=None, orchestrator=None): + """ + Start the Drydock API service + + :param state_manager: Instance of drydock_provisioner.statemgmt.manager.DesignState for accessing + state persistence + :param ingester: Instance of drydock_provisioner.ingester.ingester.Ingester for handling design + part input + """ + control_api = falcon.API(request_type=DrydockRequest, + middleware=[AuthMiddleware(), ContextMiddleware(), LoggingMiddleware()]) + + # v1.0 of Drydock API + v1_0_routes = [ + # API for managing orchestrator tasks + ('/tasks', TasksResource(state_manager=state_manager, orchestrator=orchestrator)), + ('/tasks/{task_id}', TaskResource(state_manager=state_manager)), + + # API for managing site design data + ('/designs', DesignsResource(state_manager=state_manager)), + ('/designs/{design_id}', DesignResource(state_manager=state_manager, orchestrator=orchestrator)), + ('/designs/{design_id}/parts', DesignsPartsResource(state_manager=state_manager, ingester=ingester)), + ('/designs/{design_id}/parts/{kind}', DesignsPartsKindsResource(state_manager=state_manager)), + ('/designs/{design_id}/parts/{kind}/{name}', DesignsPartResource(state_manager=state_manager, orchestrator=orchestrator)) + ] + + for path, res in v1_0_routes: + control_api.add_route('/api/v1.0' + path, res) + + return control_api diff --git a/drydock_provisioner/control/base.py b/drydock_provisioner/control/base.py new file mode 100644 index 00000000..3f0f8d88 --- /dev/null +++ b/drydock_provisioner/control/base.py @@ -0,0 +1,144 @@ +# Copyright 2017 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. +import falcon.request as request +import uuid +import json +import logging + +import drydock_provisioner.error as errors + +class BaseResource(object): + + def __init__(self): + self.logger = logging.getLogger('control') + self.authorized_roles = [] + + def on_options(self, req, resp): + self_attrs = dir(self) + methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH'] + allowed_methods = [] + + for m in methods: + if 'on_' + m.lower() in self_attrs: + allowed_methods.append(m) + + resp.headers['Allow'] = ','.join(allowed_methods) + resp.status = falcon.HTTP_200 + + # For authorizing access at the Resource level. A Resource requiring + # finer grained authorization at the method or instance level must + # implement that in the request handlers + def authorize_roles(self, role_list): + authorized = set(self.authorized_roles) + applied = set(role_list) + + if authorized.isdisjoint(applied): + return False + else: + return True + + def req_json(self, req): + if req.content_length is None or req.content_length == 0: + return None + + if req.content_type is not None and req.content_type.lower() == 'application/json': + raw_body = req.stream.read(req.content_length or 0) + + if raw_body is None: + return None + + try: + json_body = json.loads(raw_body.decode('utf-8')) + return json_body + except json.JSONDecodeError as jex: + raise errors.InvalidFormat("%s: Invalid JSON in body: %s" % (req.path, jex)) + else: + raise errors.InvalidFormat("Requires application/json payload") + + def return_error(self, resp, status_code, message="", retry=False): + resp.body = json.dumps({'type': 'error', 'message': message, 'retry': retry}) + resp.status = status_code + + def log_error(self, ctx, level, msg): + extra = { + 'user': 'N/A', + 'req_id': 'N/A', + 'external_ctx': 'N/A' + } + + if ctx is not None: + extra = { + 'user': ctx.user, + 'req_id': ctx.request_id, + 'external_ctx': ctx.external_marker, + } + + self.logger.log(level, msg, extra=extra) + + def debug(self, ctx, msg): + self.log_error(ctx, logging.DEBUG, msg) + + def info(self, ctx, msg): + self.log_error(ctx, logging.INFO, msg) + + def warn(self, ctx, msg): + self.log_error(ctx, logging.WARN, msg) + + def error(self, ctx, msg): + self.log_error(ctx, logging.ERROR, msg) + + +class StatefulResource(BaseResource): + + def __init__(self, state_manager=None): + super(StatefulResource, self).__init__() + + if state_manager is None: + self.error(None, "StatefulResource:init - StatefulResources require a state manager be set") + raise ValueError("StatefulResources require a state manager be set") + + self.state_manager = state_manager + + +class DrydockRequestContext(object): + + def __init__(self): + self.log_level = 'ERROR' + self.user = None + self.roles = ['anyone'] + self.req_id = str(uuid.uuid4()) + self.external_marker = None + + def set_log_level(self, level): + if level in ['error', 'info', 'debug']: + self.log_level = level + + def set_user(self, user): + self.user = user + + def add_role(self, role): + self.roles.append(role) + + def add_roles(self, roles): + self.roles.extend(roles) + + def remove_role(self, role): + self.roles = [x for x in self.roles + if x != role] + + def set_external_marker(self, marker): + self.external_marker = str(marker)[:20] + +class DrydockRequest(request.Request): + context_type = DrydockRequestContext \ No newline at end of file diff --git a/drydock_provisioner/control/designs.py b/drydock_provisioner/control/designs.py new file mode 100644 index 00000000..23afc457 --- /dev/null +++ b/drydock_provisioner/control/designs.py @@ -0,0 +1,164 @@ +# Copyright 2017 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. +import falcon +import json +import uuid +import logging + +import drydock_provisioner.objects as hd_objects +import drydock_provisioner.error as errors + +from .base import StatefulResource + +class DesignsResource(StatefulResource): + + def __init__(self, **kwargs): + super(DesignsResource, self).__init__(**kwargs) + self.authorized_roles = ['user'] + + def on_get(self, req, resp): + state = self.state_manager + + designs = list(state.designs.keys()) + + resp.body = json.dumps(designs) + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + try: + json_data = self.req_json(req) + design = None + if json_data is not None: + base_design = json_data.get('base_design_id', None) + + if base_design is not None: + base_design = uuid.UUID(base_design) + design = hd_objects.SiteDesign(base_design_id=base_design_uuid) + else: + design = hd_objects.SiteDesign() + design.assign_id() + design.create(req.context, self.state_manager) + + resp.body = json.dumps(design.obj_to_simple()) + resp.status = falcon.HTTP_201 + except errors.StateError as stex: + self.error(req.context, "Error updating persistence") + self.return_error(resp, falcon.HTTP_500, message="Error updating persistence", retry=True) + except errors.InvalidFormat as fex: + self.error(req.context, str(fex)) + self.return_error(resp, falcon.HTTP_400, message=str(fex), retry=False) + + +class DesignResource(StatefulResource): + + def __init__(self, orchestrator=None, **kwargs): + super(DesignResource, self).__init__(**kwargs) + self.authorized_roles = ['user'] + self.orchestrator = orchestrator + + def on_get(self, req, resp, design_id): + source = req.params.get('source', 'designed') + + try: + design = None + if source == 'compiled': + design = self.orchestrator.get_effective_site(design_id) + elif source == 'designed': + design = self.orchestrator.get_described_site(design_id) + + resp.body = json.dumps(design.obj_to_simple()) + except errors.DesignError: + self.error(req.context, "Design %s not found" % design_id) + self.return_error(resp, falcon.HTTP_404, message="Design %s not found" % design_id, retry=False) + +class DesignsPartsResource(StatefulResource): + + def __init__(self, ingester=None, **kwargs): + super(DesignsPartsResource, self).__init__(**kwargs) + self.ingester = ingester + self.authorized_roles = ['user'] + + if ingester is None: + self.error(None, "DesignsPartsResource requires a configured Ingester instance") + raise ValueError("DesignsPartsResource requires a configured Ingester instance") + + def on_post(self, req, resp, design_id): + ingester_name = req.params.get('ingester', None) + + if ingester_name is None: + self.error(None, "DesignsPartsResource POST requires parameter 'ingester'") + self.return_error(resp, falcon.HTTP_400, message="POST requires parameter 'ingester'", retry=False) + else: + try: + raw_body = req.stream.read(req.content_length or 0) + if raw_body is not None and len(raw_body) > 0: + parsed_items = self.ingester.ingest_data(plugin_name=ingester_name, design_state=self.state_manager, + content=raw_body, design_id=design_id, context=req.context) + resp.status = falcon.HTTP_201 + resp.body = json.dumps([x.obj_to_simple() for x in parsed_items]) + else: + self.return_error(resp, falcon.HTTP_400, message="Empty body not supported", retry=False) + except ValueError: + self.return_error(resp, falcon.HTTP_500, message="Error processing input", retry=False) + except LookupError: + self.return_error(resp, falcon.HTTP_400, message="Ingester %s not registered" % ingester_name, retry=False) + + +class DesignsPartsKindsResource(StatefulResource): + def __init__(self, **kwargs): + super(DesignsPartsKindsResource, self).__init__(**kwargs) + self.authorized_roles = ['user'] + + def on_get(self, req, resp, design_id, kind): + pass + +class DesignsPartResource(StatefulResource): + + def __init__(self, orchestrator=None, **kwargs): + super(DesignsPartResource, self).__init__(**kwargs) + self.authorized_roles = ['user'] + self.orchestrator = orchestrator + + def on_get(self, req , resp, design_id, kind, name): + source = req.params.get('source', 'designed') + + try: + design = None + if source == 'compiled': + design = self.orchestrator.get_effective_site(design_id) + elif source == 'designed': + design = self.orchestrator.get_described_site(design_id) + + part = None + if kind == 'Site': + part = design.get_site() + elif kind == 'Network': + part = design.get_network(name) + elif kind == 'NetworkLink': + part = design.get_network_link(name) + elif kind == 'HardwareProfile': + part = design.get_hardware_profile(name) + elif kind == 'HostProfile': + part = design.get_host_profile(name) + elif kind == 'BaremetalNode': + part = design.get_baremetal_node(name) + else: + self.error(req.context, "Kind %s unknown" % kind) + self.return_error(resp, falcon.HTTP_404, message="Kind %s unknown" % kind, retry=False) + return + + resp.body = json.dumps(part.obj_to_simple()) + except errors.DesignError as dex: + self.error(req.context, str(dex)) + self.return_error(resp, falcon.HTTP_404, message=str(dex), retry=False) \ No newline at end of file diff --git a/drydock_provisioner/control/middleware.py b/drydock_provisioner/control/middleware.py new file mode 100644 index 00000000..a522af7b --- /dev/null +++ b/drydock_provisioner/control/middleware.py @@ -0,0 +1,92 @@ +# Copyright 2017 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. + +import falcon +import logging +import uuid + +import drydock_provisioner.config as config + +class AuthMiddleware(object): + + # Authentication + def process_request(self, req, resp): + ctx = req.context + token = req.get_header('X-Auth-Token') + + user = self.validate_token(token) + + if user is not None: + ctx.set_user(user) + user_roles = self.role_list(user) + ctx.add_roles(user_roles) + else: + ctx.add_role('anyone') + + # Authorization + def process_resource(self, req, resp, resource, params): + ctx = req.context + + if not resource.authorize_roles(ctx.roles): + raise falcon.HTTPUnauthorized('Authentication required', + ('This resource requires an authorized role.')) + + # Return the username associated with an authenticated token or None + def validate_token(self, token): + if token == '42': + return 'scott' + elif token == 'bigboss': + return 'admin' + else: + return None + + # Return the list of roles assigned to the username + # Roles need to be an enum + def role_list(self, username): + if username == 'scott': + return ['user'] + elif username == 'admin': + return ['user', 'admin'] + +class ContextMiddleware(object): + + def process_request(self, req, resp): + ctx = req.context + + requested_logging = req.get_header('X-Log-Level') + + if (config.DrydockConfig.global_config.get('log_level', '') == 'DEBUG' or + (requested_logging == 'DEBUG' and 'admin' in ctx.roles)): + ctx.set_log_level('DEBUG') + elif requested_logging == 'INFO': + ctx.set_log_level('INFO') + + ext_marker = req.get_header('X-Context-Marker') + + ctx.set_external_marker(ext_marker if ext_marker is not None else '') + +class LoggingMiddleware(object): + + def __init__(self): + self.logger = logging.getLogger('drydock.control') + + def process_response(self, req, resp, resource, req_succeeded): + ctx = req.context + extra = { + 'user': ctx.user, + 'req_id': ctx.req_id, + 'external_ctx': ctx.external_marker, + } + resp.append_header('X-Drydock-Req', ctx.req_id) + self.logger.info("%s - %s" % (req.uri, resp.status), extra=extra) diff --git a/drydock_provisioner/control/readme.md b/drydock_provisioner/control/readme.md new file mode 100644 index 00000000..d09f2d0e --- /dev/null +++ b/drydock_provisioner/control/readme.md @@ -0,0 +1,30 @@ +# Control # + +This is the external facing API service to control the rest +of Drydock and query Drydock-managed data. + +## v1.0 Endpoints ## + +### /api/v1.0/tasks ### + +POST - Create a new orchestration task and submit it for execution +GET - Get status of a task +DELETE - Cancel execution of a task if permitted + +### /api/v1.0/designs ### + +POST - Create a new site design so design parts can be added + +### /api/v1.0/designs/{id} + +GET - Get a current design if available. Param 'source=compiled' to calculate the inheritance chain and compile the effective design. + +### /api/v1.0/designs/{id}/parts + +POST - Submit a new design part to be ingested and added to this design +GET - View a currently defined design part +PUT - Replace an existing design part *Not Implemented* + +### /api/v1.0/designs/{id}/parts/{kind}/{name} + +GET - View a single design part. param 'source=compiled' to calculate the inheritance chain and compile the effective configuration for the design part. \ No newline at end of file diff --git a/drydock_provisioner/control/tasks.py b/drydock_provisioner/control/tasks.py new file mode 100644 index 00000000..a4a49da9 --- /dev/null +++ b/drydock_provisioner/control/tasks.py @@ -0,0 +1,79 @@ +# Copyright 2017 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. +import falcon +import json +import threading +import traceback + +import drydock_provisioner.objects.task as obj_task +from .base import StatefulResource + +class TasksResource(StatefulResource): + + def __init__(self, orchestrator=None, **kwargs): + super(TasksResource, self).__init__(**kwargs) + self.authorized_roles = ['user'] + self.orchestrator = orchestrator + + def on_get(self, req, resp): + task_id_list = [str(x.get_id()) for x in self.state_manager.tasks] + resp.body = json.dumps(task_id_list) + + def on_post(self, req, resp): + try: + json_data = self.req_json(req) + + sitename = json_data.get('sitename', None) + design_id = json_data.get('design_id', None) + action = json_data.get('action', None) + + if sitename is None or design_id is None or action is None: + self.info(req.context, "Task creation requires fields sitename, design_id, action") + self.return_error(resp, falcon.HTTP_400, message="Task creation requires fields sitename, design_id, action", retry=False) + return + + task = self.orchestrator.create_task(obj_task.OrchestratorTask, site=sitename, + design_id=design_id, action=action) + + task_thread = threading.Thread(target=self.orchestrator.execute_task, args=[task.get_id()]) + task_thread.start() + + resp.body = json.dumps(task.to_dict()) + resp.status = falcon.HTTP_201 + except Exception as ex: + self.error(req.context, "Unknown error: %s\n%s" % (str(ex), traceback.format_exc())) + self.return_error(resp, falcon.HTTP_500, message="Unknown error", retry=False) + + +class TaskResource(StatefulResource): + + def __init__(self, orchestrator=None, **kwargs): + super(TaskResource, self).__init__(**kwargs) + self.authorized_roles = ['user'] + self.orchestrator = orchestrator + + def on_get(self, req, resp, task_id): + try: + task = self.state_manager.get_task(task_id) + + if task is None: + self.info(req.context, "Task %s does not exist" % task_id ) + self.return_error(resp, falcon.HTTP_404, message="Task %s does not exist" % task_id, retry=False) + return + + resp.body = json.dumps(task.to_dict()) + resp.status = falcon.HTTP_200 + except Exception as ex: + self.error(req.context, "Unknown error: %s" % (str(ex))) + self.return_error(resp, falcon.HTTP_500, message="Unknown error", retry=False) diff --git a/helm_drydock/drivers/__init__.py b/drydock_provisioner/drivers/__init__.py similarity index 94% rename from helm_drydock/drivers/__init__.py rename to drydock_provisioner/drivers/__init__.py index 2ced9f00..c3868af1 100644 --- a/helm_drydock/drivers/__init__.py +++ b/drydock_provisioner/drivers/__init__.py @@ -15,10 +15,10 @@ from threading import Thread, Lock import uuid import time -import helm_drydock.objects.fields as hd_fields -import helm_drydock.statemgmt as statemgmt -import helm_drydock.objects.task as tasks -import helm_drydock.error as errors +import drydock_provisioner.objects.fields as hd_fields +import drydock_provisioner.statemgmt as statemgmt +import drydock_provisioner.objects.task as tasks +import drydock_provisioner.error as errors # This is the interface for the orchestrator to access a driver # TODO Need to have each driver spin up a seperate thread to manage diff --git a/helm_drydock/drivers/node/__init__.py b/drydock_provisioner/drivers/node/__init__.py similarity index 90% rename from helm_drydock/drivers/node/__init__.py rename to drydock_provisioner/drivers/node/__init__.py index 87ea3046..a28c7cec 100644 --- a/helm_drydock/drivers/node/__init__.py +++ b/drydock_provisioner/drivers/node/__init__.py @@ -13,10 +13,10 @@ # limitations under the License. # -import helm_drydock.objects.fields as hd_fields -import helm_drydock.error as errors +import drydock_provisioner.objects.fields as hd_fields +import drydock_provisioner.error as errors -from helm_drydock.drivers import ProviderDriver +from drydock_provisioner.drivers import ProviderDriver class NodeDriver(ProviderDriver): @@ -28,6 +28,7 @@ class NodeDriver(ProviderDriver): hd_fields.OrchestratorAction.CreateStorageTemplate, hd_fields.OrchestratorAction.CreateBootMedia, hd_fields.OrchestratorAction.PrepareHardwareConfig, + hd_fields.OrchestratorAction.IdentifyNode, hd_fields.OrchestratorAction.ConfigureHardware, hd_fields.OrchestratorAction.InterrogateNode, hd_fields.OrchestratorAction.ApplyNodeNetworking, diff --git a/helm_drydock/drivers/node/maasdriver/models/__init__.py b/drydock_provisioner/drivers/node/maasdriver/__init__.py similarity index 94% rename from helm_drydock/drivers/node/maasdriver/models/__init__.py rename to drydock_provisioner/drivers/node/maasdriver/__init__.py index 2a385a45..f10bbbf6 100644 --- a/helm_drydock/drivers/node/maasdriver/models/__init__.py +++ b/drydock_provisioner/drivers/node/maasdriver/__init__.py @@ -10,4 +10,4 @@ # 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. \ No newline at end of file +# limitations under the License. diff --git a/helm_drydock/drivers/node/maasdriver/api_client.py b/drydock_provisioner/drivers/node/maasdriver/api_client.py similarity index 100% rename from helm_drydock/drivers/node/maasdriver/api_client.py rename to drydock_provisioner/drivers/node/maasdriver/api_client.py diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py new file mode 100644 index 00000000..f7079dcc --- /dev/null +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -0,0 +1,447 @@ +# Copyright 2017 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. +import time +import logging + +import drydock_provisioner.error as errors +import drydock_provisioner.config as config +import drydock_provisioner.drivers as drivers +import drydock_provisioner.objects.fields as hd_fields +import drydock_provisioner.objects.task as task_model + +from drydock_provisioner.drivers.node import NodeDriver +from .api_client import MaasRequestFactory +import drydock_provisioner.drivers.node.maasdriver.models.fabric as maas_fabric +import drydock_provisioner.drivers.node.maasdriver.models.vlan as maas_vlan +import drydock_provisioner.drivers.node.maasdriver.models.subnet as maas_subnet +import drydock_provisioner.drivers.node.maasdriver.models.machine as maas_machine + +class MaasNodeDriver(NodeDriver): + + def __init__(self, **kwargs): + super(MaasNodeDriver, self).__init__(**kwargs) + + self.driver_name = "maasdriver" + self.driver_key = "maasdriver" + self.driver_desc = "MaaS Node Provisioning Driver" + + self.config = config.DrydockConfig.node_driver[self.driver_key] + + self.logger = logging.getLogger('drydock.nodedriver.maasdriver') + + def execute_task(self, task_id): + task = self.state_manager.get_task(task_id) + + if task is None: + raise errors.DriverError("Invalid task %s" % (task_id)) + + if task.action not in self.supported_actions: + raise errors.DriverError("Driver %s doesn't support task action %s" + % (self.driver_desc, task.action)) + + if task.action == hd_fields.OrchestratorAction.ValidateNodeServices: + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Running) + maas_client = MaasRequestFactory(self.config['api_url'], self.config['api_key']) + + try: + if maas_client.test_connectivity(): + if maas_client.test_authentication(): + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=hd_fields.ActionResult.Success) + return + except errors.TransientDriverError(ex): + result = { + 'retry': True, + 'detail': str(ex), + } + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=hd_fields.ActionResult.Failure, + result_details=result) + return + except errors.PersistentDriverError(ex): + result = { + 'retry': False, + 'detail': str(ex), + } + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=hd_fields.ActionResult.Failure, + result_details=result) + return + except Exception(ex): + result = { + 'retry': False, + 'detail': str(ex), + } + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=hd_fields.ActionResult.Failure, + result_details=result) + return + + design_id = getattr(task, 'design_id', None) + + if design_id is None: + raise errors.DriverError("No design ID specified in task %s" % + (task_id)) + + + if task.site_name is None: + raise errors.DriverError("No site specified for task %s." % + (task_id)) + + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Running) + + site_design = self.orchestrator.get_effective_site(design_id) + + if task.action == hd_fields.OrchestratorAction.CreateNetworkTemplate: + self.orchestrator.task_field_update(task.get_id(), status=hd_fields.TaskStatus.Running) + + subtask = self.orchestrator.create_task(task_model.DriverTask, + parent_task_id=task.get_id(), design_id=design_id, + action=task.action, site_name=task.site_name, + task_scope={'site': task.site_name}) + runner = MaasTaskRunner(state_manager=self.state_manager, + orchestrator=self.orchestrator, + task_id=subtask.get_id(),config=self.config) + + self.logger.info("Starting thread for task %s to create network templates" % (subtask.get_id())) + + runner.start() + + # TODO Figure out coherent system for putting all the timeouts in + # the config + runner.join(timeout=120) + + if runner.is_alive(): + result = { + 'retry': False, + 'detail': 'MaaS Network creation timed-out' + } + self.logger.warn("Thread for task %s timed out after 120s" % (subtask.get_id())) + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=hd_fields.ActionResult.Failure, + result_detail=result) + else: + subtask = self.state_manager.get_task(subtask.get_id()) + self.logger.info("Thread for task %s completed - result %s" % (subtask.get_id(), subtask.get_result())) + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=subtask.get_result()) + + return + elif task.action == hd_fields.OrchestratorAction.IdentifyNode: + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Running) + + subtasks = [] + + result_detail = { + 'detail': [] + } + + for n in task.node_list: + subtask = self.orchestrator.create_task(task_model.DriverTask, + parent_task_id=task.get_id(), design_id=design_id, + action=hd_fields.OrchestratorAction.IdentifyNode, + site_name=task.site_name, + task_scope={'site': task.site_name, 'node_names': [n]}) + runner = MaasTaskRunner(state_manager=self.state_manager, + orchestrator=self.orchestrator, + task_id=subtask.get_id(),config=self.config) + + self.logger.info("Starting thread for task %s to identify node %s" % (subtask.get_id(), n)) + + runner.start() + subtasks.append(subtask.get_id()) + + running_subtasks = len(subtasks) + attempts = 0 + worked = failed = False + + #TODO Add timeout to config + while running_subtasks > 0 and attempts < 3: + for t in subtasks: + subtask = self.state_manager.get_task(t) + + if subtask.status == hd_fields.TaskStatus.Complete: + self.logger.info("Task %s to identify node %s complete - status %s" % + (subtask.get_id(), n, subtask.get_result())) + + result_detail['detail'].extend(subtask.result_detail['detail']) + running_subtasks = running_subtasks - 1 + + if subtask.result in [hd_fields.ActionResult.Success, + hd_fields.ActionResult.PartialSuccess]: + worked = True + elif subtask.result in [hd_fields.ActionResult.Failure, + hd_fields.ActionResult.PartialSuccess]: + failed = True + + time.sleep(1 * 60) + attempts = attempts + 1 + + if running_subtasks > 0: + self.logger.warn("Time out for task %s before all subtask threads complete" % (task.get_id())) + result = hd_fields.ActionResult.DependentFailure + result_detail['detail'].append('Some subtasks did not complete before the timeout threshold') + if worked and failed: + result = hd_fields.ActionResult.PartialSuccess + elif worked: + result = hd_fields.ActionResult.Success + else: + result = hd_fields.ActionResult.Failure + + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=result, + result_detail=result_detail) + +class MaasTaskRunner(drivers.DriverTaskRunner): + + def __init__(self, config=None, **kwargs): + super(MaasTaskRunner, self).__init__(**kwargs) + + self.driver_config = config + self.logger = logging.getLogger('drydock.nodedriver.maasdriver') + + def execute_task(self): + task_action = self.task.action + + self.orchestrator.task_field_update(self.task.get_id(), + status=hd_fields.TaskStatus.Running, + result=hd_fields.ActionResult.Incomplete) + + self.maas_client = MaasRequestFactory(self.driver_config['api_url'], + self.driver_config['api_key']) + + site_design = self.orchestrator.get_effective_site(self.task.design_id) + + if task_action == hd_fields.OrchestratorAction.CreateNetworkTemplate: + # Try to true up MaaS definitions of fabrics/vlans/subnets + # with the networks defined in Drydock + design_networks = site_design.networks + + subnets = maas_subnet.Subnets(self.maas_client) + subnets.refresh() + + result_detail = { + 'detail': [] + } + + for n in design_networks: + try: + subnet = subnets.singleton({'cidr': n.cidr}) + + if subnet is not None: + subnet.name = n.name + subnet.dns_servers = n.dns_servers + + vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=subnet.fabric) + vlan_list.refresh() + + vlan = vlan_list.select(subnet.vlan) + + if vlan is not None: + if ((n.vlan_id is None and vlan.vid != 0) or + (n.vlan_id is not None and vlan.vid != n.vlan_id)): + + # if the VLAN name matches, assume this is the correct resource + # and it needs to be updated + if vlan.name == n.name: + vlan.set_vid(n.vlan_id) + vlan.mtu = n.mtu + vlan.update() + result_detail['detail'].append("VLAN %s found for network %s, updated attributes" + % (vlan.resource_id, n.name)) + else: + # Found a VLAN with the correct VLAN tag, update subnet to use it + target_vlan = vlan_list.singleton({'vid': n.vlan_id if n.vlan_id is not None else 0}) + if target_vlan is not None: + subnet.vlan = target_vlan.resource_id + else: + # This is a flag that after creating a fabric and + # VLAN below, update the subnet + subnet.vlan = None + else: + subnet.vlan = None + + # Check if the routes have a default route + subnet.gateway_ip = n.get_default_gateway() + + + result_detail['detail'].append("Subnet %s found for network %s, updated attributes" + % (subnet.resource_id, n.name)) + + # Need to find or create a Fabric/Vlan for this subnet + if (subnet is None or (subnet is not None and subnet.vlan is None)): + fabric_list = maas_fabric.Fabrics(self.maas_client) + fabric_list.refresh() + fabric = fabric_list.singleton({'name': n.name}) + + vlan = None + + if fabric is not None: + vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=fabric.resource_id) + vlan_list.refresh() + + vlan = vlan_list.singleton({'vid': n.vlan_id if n.vlan_id is not None else 0}) + + if vlan is not None: + vlan = matching_vlans[0] + + vlan.name = n.name + if getattr(n, 'mtu', None) is not None: + vlan.mtu = n.mtu + + if subnet is not None: + subnet.vlan = vlan.resource_id + subnet.update() + + vlan.update() + result_detail['detail'].append("VLAN %s found for network %s, updated attributes" + % (vlan.resource_id, n.name)) + else: + # Create a new VLAN in this fabric and assign subnet to it + vlan = maas_vlan.Vlan(self.maas_client, name=n.name, vid=vlan_id, + mtu=getattr(n, 'mtu', None),fabric_id=fabric.resource_id) + vlan = vlan_list.add(vlan) + + result_detail['detail'].append("VLAN %s created for network %s" + % (vlan.resource_id, n.name)) + if subnet is not None: + subnet.vlan = vlan.resource_id + subnet.update() + + else: + # Create new fabric and VLAN + fabric = maas_fabric.Fabric(self.maas_client, name=n.name) + fabric = fabric_list.add(fabric) + fabric_list.refresh() + + result_detail['detail'].append("Fabric %s created for network %s" + % (fabric.resource_id, n.name)) + + vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=new_fabric.resource_id) + vlan_list.refresh() + + # A new fabric comes with a single default VLAN. Retrieve it and update attributes + vlan = vlan_list.single() + + vlan.name = n.name + vlan.vid = n.vlan_id if n.vlan_id is not None else 0 + if getattr(n, 'mtu', None) is not None: + vlan.mtu = n.mtu + + vlan.update() + result_detail['detail'].append("VLAN %s updated for network %s" + % (vlan.resource_id, n.name)) + if subnet is not None: + # If subnet was found above, but needed attached to a new fabric/vlan then + # attach it + subnet.vlan = vlan.resource_id + subnet.update() + + if subnet is None: + # If subnet did not exist, create it here and attach it to the fabric/VLAN + subnet = maas_subnet.Subnet(self.maas_client, name=n.name, cidr=n.cidr, fabric=fabric.resource_id, + vlan=vlan.resource_id, gateway_ip=n.get_default_gateway()) + + subnet_list = maas_subnet.Subnets(self.maas_client) + subnet = subnet_list.add(subnet) + except ValueError as vex: + raise errors.DriverError("Inconsistent data from MaaS") + + subnet_list = maas_subnet.Subnets(self.maas_client) + subnet_list.refresh() + + action_result = hd_fields.ActionResult.Incomplete + + success_rate = 0 + + for n in design_networks: + exists = subnet_list.query({'cidr': n.cidr}) + if len(exists) > 0: + subnet = exists[0] + if subnet.name == n.name: + success_rate = success_rate + 1 + else: + success_rate = success_rate + 1 + else: + success_rate = success_rate + 1 + + if success_rate == len(design_networks): + action_result = hd_fields.ActionResult.Success + elif success_rate == - (len(design_networks)): + action_result = hd_fields.ActionResult.Failure + else: + action_result = hd_fields.ActionResult.PartialSuccess + + self.orchestrator.task_field_update(self.task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=action_result, + result_detail=result_detail) + elif task_action == hd_fields.OrchestratorAction.IdentifyNode: + try: + machine_list = maas_machine.Machines(self.maas_client) + machine_list.refresh() + except: + self.orchestrator.task_field_update(self.task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=hd_fields.ActionResult.Failure, + result_detail={'detail': 'Error accessing MaaS Machines API', 'retry': True}) + return + + nodes = self.task.node_list + + result_detail = {'detail': []} + + worked = failed = False + + for n in nodes: + try: + node = site_design.get_baremetal_node(n) + machine = machine_list.identify_baremetal_node(node) + if machine is not None: + worked = True + result_detail['detail'].append("Node %s identified in MaaS" % n) + else: + failed = True + result_detail['detail'].append("Node %s not found in MaaS" % n) + except Exception as ex: + failed = True + result_detail['detail'].append("Error identifying node %s: %s" % (n, str(ex))) + + result = None + if worked and failed: + result = hd_fields.ActionResult.PartialSuccess + elif worked: + result = hd_fields.ActionResult.Success + elif failed: + result = hd_fields.ActionResult.Failure + + self.orchestrator.task_field_update(self.task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=result, + result_detail=result_detail) + + + + + diff --git a/helm_drydock/control/__init__.py b/drydock_provisioner/drivers/node/maasdriver/models/__init__.py similarity index 100% rename from helm_drydock/control/__init__.py rename to drydock_provisioner/drivers/node/maasdriver/models/__init__.py diff --git a/helm_drydock/drivers/node/maasdriver/models/base.py b/drydock_provisioner/drivers/node/maasdriver/models/base.py similarity index 87% rename from helm_drydock/drivers/node/maasdriver/models/base.py rename to drydock_provisioner/drivers/node/maasdriver/models/base.py index 9f3aa336..7d6cbd49 100644 --- a/helm_drydock/drivers/node/maasdriver/models/base.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/base.py @@ -13,8 +13,9 @@ # limitations under the License. import json import re +import logging -import helm_drydock.error as errors +import drydock_provisioner.error as errors """ A representation of a MaaS REST resource. Should be subclassed for different resources and augmented with operations specific @@ -28,6 +29,7 @@ class ResourceBase(object): def __init__(self, api_client, **kwargs): self.api_client = api_client + self.logger = logging.getLogger('drydock.drivers.maasdriver') for f in self.fields: if f in kwargs.keys(): @@ -143,13 +145,16 @@ class ResourceBase(object): return i -""" -A collection of MaaS resources. -Rather than a simple list, we will key the collection on resource -ID for more efficient access. -""" class ResourceCollectionBase(object): + """ + A collection of MaaS resources. + + Rather than a simple list, we will key the collection on resource + ID for more efficient access. + + :param api_client: An instance of api_client.MaasRequestFactory + """ collection_url = '' collection_resource = ResourceBase @@ -157,12 +162,13 @@ class ResourceCollectionBase(object): def __init__(self, api_client): self.api_client = api_client self.resources = {} + self.logger = logging.getLogger('drydock.drivers.maasdriver') - """ - Parse URL for placeholders and replace them with current - instance values - """ def interpolate_url(self): + """ + Parse URL for placeholders and replace them with current + instance values + """ pattern = '\{([a-z_]+)\}' regex = re.compile(pattern) start = 0 @@ -250,8 +256,23 @@ class ResourceCollectionBase(object): return result + def singleton(self, query): + """ + A query that requires a single item response + + :param query: A dict of k:v pairs defining the query parameters + """ + result = self.query(query) + + if len(result) > 1: + raise ValueError("Multiple results found") + elif len(result) == 1: + return result[0] + + return None + """ - If the collection has a single item, return it + If the collection contains a single item, return it """ def single(self): if self.len() == 1: diff --git a/helm_drydock/drivers/node/maasdriver/models/fabric.py b/drydock_provisioner/drivers/node/maasdriver/models/fabric.py similarity index 88% rename from helm_drydock/drivers/node/maasdriver/models/fabric.py rename to drydock_provisioner/drivers/node/maasdriver/models/fabric.py index a105f354..90e954af 100644 --- a/helm_drydock/drivers/node/maasdriver/models/fabric.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/fabric.py @@ -13,8 +13,8 @@ # limitations under the License. import json -import helm_drydock.drivers.node.maasdriver.models.base as model_base -import helm_drydock.drivers.node.maasdriver.models.vlan as model_vlan +import drydock_provisioner.drivers.node.maasdriver.models.base as model_base +import drydock_provisioner.drivers.node.maasdriver.models.vlan as model_vlan class Fabric(model_base.ResourceBase): diff --git a/drydock_provisioner/drivers/node/maasdriver/models/interface.py b/drydock_provisioner/drivers/node/maasdriver/models/interface.py new file mode 100644 index 00000000..5c257279 --- /dev/null +++ b/drydock_provisioner/drivers/node/maasdriver/models/interface.py @@ -0,0 +1,34 @@ +# Copyright 2017 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. + +import drydock_provisioner.drivers.node.maasdriver.models.base as model_base + +class Interface(model_base.ResourceBase): + + resource_url = 'nodes/{system_id}/interfaces/{resource_id}/' + fields = ['resource_id', 'system_id', 'name', 'type', 'mac_address', 'vlan', + 'links', 'effective_mtu'] + json_fields = ['name', 'type', 'mac_address', 'vlan', 'links', 'effective_mtu'] + + def __init__(self, api_client, **kwargs): + super(Interface, self).__init__(api_client, **kwargs) + +class Interfaces(model_base.ResourceCollectionBase): + + collection_url = 'nodes/{system_id}/interfaces/' + collection_resource = Interface + + def __init__(self, api_client, **kwargs): + super(Interfaces, self).__init__(api_client) + self.system_id = kwargs.get('system_id', None) \ No newline at end of file diff --git a/drydock_provisioner/drivers/node/maasdriver/models/machine.py b/drydock_provisioner/drivers/node/maasdriver/models/machine.py new file mode 100644 index 00000000..f4ec3609 --- /dev/null +++ b/drydock_provisioner/drivers/node/maasdriver/models/machine.py @@ -0,0 +1,185 @@ +# Copyright 2017 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. + +import drydock_provisioner.drivers.node.maasdriver.models.base as model_base +import drydock_provisioner.drivers.node.maasdriver.models.interface as maas_interface +import bson +import yaml + +class Machine(model_base.ResourceBase): + + resource_url = 'machines/{resource_id}/' + fields = ['resource_id', 'hostname', 'power_type', 'power_state', 'power_parameters', 'interfaces', + 'boot_interface', 'memory', 'cpu_count', 'tag_names'] + json_fields = ['hostname', 'power_type'] + + def __init__(self, api_client, **kwargs): + super(Machine, self).__init__(api_client, **kwargs) + + # Replace generic dicts with interface collection model + if getattr(self, 'resource_id', None) is not None: + self.interfaces = maas_interface.Interfaces(api_client, system_id=self.resource_id) + self.interfaces.refresh() + + def get_power_params(self): + url = self.interpolate_url() + + resp = self.api_client.get(url, op='power_parameters') + + if resp.status_code == 200: + self.power_parameters = resp.json() + + def commission(self, debug=False): + url = self.interpolate_url() + + # If we want to debug this node commissioning, enable SSH + # after commissioning and leave the node powered up + + options = {'enable_ssh': '1' if debug else '0'} + + resp = self.api_client.post(url, op='commission', files=options) + + # Need to sort out how to handle exceptions + if not resp.ok: + raise Exception() + + def get_details(self): + url = self.interpolate_url() + + resp = self.api_client.get(url, op='details') + + if resp.status_code == 200: + detail_config = bson.loads(resp.text) + return detail_config + + + def to_dict(self): + """ + Serialize this resource instance into a dict matching the + MAAS representation of the resource + """ + data_dict = {} + + for f in self.json_fields: + if getattr(self, f, None) is not None: + if f == 'resource_id': + data_dict['system_id'] = getattr(self, f) + else: + data_dict[f] = getattr(self, f) + + return data_dict + + @classmethod + def from_dict(cls, api_client, obj_dict): + """ + Create a instance of this resource class based on a dict + of MaaS type attributes + + Customized for Machine due to use of system_id instead of id + as resource key + + :param api_client: Instance of api_client.MaasRequestFactory for accessing MaaS API + :param obj_dict: Python dict as parsed from MaaS API JSON representing this resource type + """ + + refined_dict = {k: obj_dict.get(k, None) for k in cls.fields} + + if 'system_id' in obj_dict.keys(): + refined_dict['resource_id'] = obj_dict.get('system_id') + + i = cls(api_client, **refined_dict) + return i + +class Machines(model_base.ResourceCollectionBase): + + collection_url = 'machines/' + collection_resource = Machine + + def __init__(self, api_client, **kwargs): + super(Machines, self).__init__(api_client) + + # Add the OOB power parameters to each machine instance + def collect_power_params(self): + for k, v in self.resources.items(): + v.get_power_params() + + + def identify_baremetal_node(self, node_model, update_name=True): + """ + Search all the defined MaaS Machines and attempt to match + one against the provided Drydock BaremetalNode model. Update + the MaaS instance with the correct hostname + + :param node_model: Instance of objects.node.BaremetalNode to search MaaS for matching resource + :param update_name: Whether Drydock should update the MaaS resource name to match the Drydock design + """ + node_oob_network = node_model.oob_network + node_oob_ip = node_model.get_network_address(node_oob_network) + + if node_oob_ip is None: + self.logger.warn("Node model missing OOB IP address") + raise ValueError('Node model missing OOB IP address') + + try: + self.collect_power_params() + + maas_node = self.singleton({'power_params.power_address': node_oob_ip}) + + self.logger.debug("Found MaaS resource %s matching Node %s" % (maas_node.resource_id, node_model.get_id())) + + if maas_node.hostname != node_model.name and update_name: + maas_node.hostname = node_model.name + maas_node.update() + self.logger.debug("Updated MaaS resource %s hostname to %s" % (maas_node.resource_id, node_model.name)) + return maas_node + + except ValueError as ve: + self.logger.warn("Error locating matching MaaS resource for OOB IP %s" % (node_oob_ip)) + return None + + def query(self, query): + """ + Custom query method to deal with complex fields + """ + result = list(self.resources.values()) + for (k, v) in query.items(): + if k.startswith('power_params.'): + field = k[13:] + result = [i for i in result + if str(getattr(i,'power_parameters', {}).get(field, None)) == str(v)] + else: + result = [i for i in result + if str(getattr(i, k, None)) == str(v)] + + return result + + + def add(self, res): + """ + Create a new resource in this collection in MaaS + + Customize as Machine resources use 'system_id' instead of 'id' + """ + data_dict = res.to_dict() + url = self.interpolate_url() + + resp = self.api_client.post(url, files=data_dict) + + if resp.status_code == 200: + resp_json = resp.json() + res.set_resource_id(resp_json.get('system_id')) + return res + + raise errors.DriverError("Failed updating MAAS url %s - return code %s" + % (url, resp.status_code)) \ No newline at end of file diff --git a/helm_drydock/drivers/node/maasdriver/models/subnet.py b/drydock_provisioner/drivers/node/maasdriver/models/subnet.py similarity index 96% rename from helm_drydock/drivers/node/maasdriver/models/subnet.py rename to drydock_provisioner/drivers/node/maasdriver/models/subnet.py index ccf677c2..8aec521e 100644 --- a/helm_drydock/drivers/node/maasdriver/models/subnet.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/subnet.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import helm_drydock.drivers.node.maasdriver.models.base as model_base +import drydock_provisioner.drivers.node.maasdriver.models.base as model_base class Subnet(model_base.ResourceBase): diff --git a/helm_drydock/drivers/node/maasdriver/models/vlan.py b/drydock_provisioner/drivers/node/maasdriver/models/vlan.py similarity index 95% rename from helm_drydock/drivers/node/maasdriver/models/vlan.py rename to drydock_provisioner/drivers/node/maasdriver/models/vlan.py index f4f506ef..36acceac 100644 --- a/helm_drydock/drivers/node/maasdriver/models/vlan.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/vlan.py @@ -13,8 +13,8 @@ # limitations under the License. import json -import helm_drydock.error as errors -import helm_drydock.drivers.node.maasdriver.models.base as model_base +import drydock_provisioner.error as errors +import drydock_provisioner.drivers.node.maasdriver.models.base as model_base class Vlan(model_base.ResourceBase): diff --git a/helm_drydock/drivers/node/maasdriver/readme.md b/drydock_provisioner/drivers/node/maasdriver/readme.md similarity index 100% rename from helm_drydock/drivers/node/maasdriver/readme.md rename to drydock_provisioner/drivers/node/maasdriver/readme.md diff --git a/helm_drydock/drivers/oob/__init__.py b/drydock_provisioner/drivers/oob/__init__.py similarity index 87% rename from helm_drydock/drivers/oob/__init__.py rename to drydock_provisioner/drivers/oob/__init__.py index ada30fb8..8e7a9c26 100644 --- a/helm_drydock/drivers/oob/__init__.py +++ b/drydock_provisioner/drivers/oob/__init__.py @@ -12,17 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -import helm_drydock.objects.fields as hd_fields -import helm_drydock.error as errors +import drydock_provisioner.objects.fields as hd_fields +import drydock_provisioner.error as errors -from helm_drydock.drivers import ProviderDriver +from drydock_provisioner.drivers import ProviderDriver class OobDriver(ProviderDriver): def __init__(self, **kwargs): super(OobDriver, self).__init__(**kwargs) - self.supported_actions = [hd_fields.OrchestrationAction.ValidateOobServices, + self.supported_actions = [hd_fields.OrchestratorAction.ValidateOobServices, hd_fields.OrchestratorAction.ConfigNodePxe, hd_fields.OrchestratorAction.SetNodeBoot, hd_fields.OrchestratorAction.PowerOffNode, diff --git a/helm_drydock/drivers/oob/pyghmi_driver/__init__.py b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py similarity index 92% rename from helm_drydock/drivers/oob/pyghmi_driver/__init__.py rename to drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py index 9a57efe9..73c2bd5c 100644 --- a/helm_drydock/drivers/oob/pyghmi_driver/__init__.py +++ b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py @@ -12,17 +12,18 @@ # See the License for the specific language governing permissions and # limitations under the License. import time +import logging from pyghmi.ipmi.command import Command -import helm_drydock.error as errors -import helm_drydock.config as config +import drydock_provisioner.error as errors +import drydock_provisioner.config as config -import helm_drydock.objects.fields as hd_fields -import helm_drydock.objects.task as task_model +import drydock_provisioner.objects.fields as hd_fields +import drydock_provisioner.objects.task as task_model -import helm_drydock.drivers.oob as oob -import helm_drydock.drivers as drivers +import drydock_provisioner.drivers.oob as oob +import drydock_provisioner.drivers as drivers class PyghmiDriver(oob.OobDriver): @@ -34,15 +35,19 @@ class PyghmiDriver(oob.OobDriver): self.driver_key = "pyghmi_driver" self.driver_desc = "Pyghmi OOB Driver" - self.config = config.DrydockConfig.node_driver[self.driver_key] + self.logger = logging.getLogger('drydock.oobdriver.pyghmi') + self.config = config.DrydockConfig.node_driver.get(self.driver_key, {}) def execute_task(self, task_id): task = self.state_manager.get_task(task_id) if task is None: + self.logger.error("Invalid task %s" % (task_id)) raise errors.DriverError("Invalid task %s" % (task_id)) if task.action not in self.supported_actions: + self.logger.error("Driver %s doesn't support task action %s" + % (self.driver_desc, task.action)) raise errors.DriverError("Driver %s doesn't support task action %s" % (self.driver_desc, task.action)) @@ -66,7 +71,7 @@ class PyghmiDriver(oob.OobDriver): result=hd_fields.ActionResult.Success) return - site_design = self.orchestrator.get_effective_site(design_id, task.site_name) + site_design = self.orchestrator.get_effective_site(design_id) target_nodes = [] @@ -118,13 +123,6 @@ class PyghmiDriver(oob.OobDriver): if x.get_result() in [hd_fields.ActionResult.PartialSuccess, hd_fields.ActionResult.Failure]] - print("Task %s successful subtasks: %s" % - (task.get_id(), len(success_subtasks))) - print("Task %s unsuccessful subtasks: %s" % - (task.get_id(), len(nosuccess_subtasks))) - print("Task %s total subtasks: %s" % - (task.get_id(), len(task.get_subtasks()))) - task_result = None if len(success_subtasks) > 0 and len(nosuccess_subtasks) > 0: task_result = hd_fields.ActionResult.PartialSuccess @@ -145,9 +143,11 @@ class PyghmiTaskRunner(drivers.DriverTaskRunner): def __init__(self, node=None, **kwargs): super(PyghmiTaskRunner, self).__init__(**kwargs) + self.logger = logging.getLogger('drydock.oobdriver.pyghmi') # We cheat here by providing the Node model instead # of making the runner source it from statemgmt if node is None: + self.logger.error("Did not specify target node") raise errors.DriverError("Did not specify target node") self.node = node @@ -172,7 +172,7 @@ class PyghmiTaskRunner(drivers.DriverTaskRunner): "task node scope") - ipmi_network = self.node.applied.get('oob_network') + ipmi_network = self.node.oob_network ipmi_address = self.node.get_network_address(ipmi_network) if ipmi_address is None: @@ -184,8 +184,8 @@ class PyghmiTaskRunner(drivers.DriverTaskRunner): self.orchestrator.task_field_update(self.task.get_id(), status=hd_fields.TaskStatus.Running) - ipmi_account = self.node.applied.get('oob_account', '') - ipmi_credential = self.node.applied.get('oob_credential', '') + ipmi_account = self.node.oob_account + ipmi_credential = self.node.oob_credential ipmi_session = Command(bmc=ipmi_address, userid=ipmi_account, password=ipmi_credential) diff --git a/helm_drydock/drivers/readme.md b/drydock_provisioner/drivers/readme.md similarity index 93% rename from helm_drydock/drivers/readme.md rename to drydock_provisioner/drivers/readme.md index 0aab4c1c..63214dd7 100644 --- a/helm_drydock/drivers/readme.md +++ b/drydock_provisioner/drivers/readme.md @@ -32,6 +32,7 @@ and storage. * CreateStorageTemplate - Configure site-wide storage information in bootstrapper * CreateBootMedia - Ensure all needed boot media is available to the bootstrapper including external repositories * PrepareHardwareConfig - Prepare the bootstrapper to handle all hardware configuration actions (firmware updates, RAID configuration, driver installation) +* IdentifyNode - Correlate a node definition in the Drydock internal model with a node detected by the downstream node bootstrapper. * ConfigureHardware - Update and validate all hardware configurations on a node prior to deploying the OS on it * InterrogateNode - Interrogate the bootstrapper about node information. Depending on the current state of the node, this interrogation will produce different information. * ApplyNodeNetworking - Configure networking for a node diff --git a/drydock_provisioner/drydock.py b/drydock_provisioner/drydock.py new file mode 100644 index 00000000..18e53a13 --- /dev/null +++ b/drydock_provisioner/drydock.py @@ -0,0 +1,55 @@ +# Copyright 2017 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. +import logging + +import drydock_provisioner.config as config +import drydock_provisioner.objects as objects +import drydock_provisioner.ingester as ingester +import drydock_provisioner.statemgmt as statemgmt +import drydock_provisioner.orchestrator as orch +import drydock_provisioner.control.api as api + +def start_drydock(): + objects.register_all() + + # Setup root logger + logger = logging.getLogger('drydock') + + logger.setLevel(config.DrydockConfig.global_config.get('log_level')) + ch = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s:%(funcName)s - %(message)s') + ch.setFormatter(formatter) + logger.addHandler(ch) + + # Specalized format for API logging + logger = logging.getLogger('drydock.control') + logger.propagate = False + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(user)s - %(req_id)s - %(external_ctx)s - %(message)s') + + ch = logging.StreamHandler() + ch.setFormatter(formatter) + logger.addHandler(ch) + + state = statemgmt.DesignState() + + orchestrator = orch.Orchestrator(config.DrydockConfig.orchestrator_config.get('drivers', {}), + state_manager=state) + input_ingester = ingester.Ingester() + input_ingester.enable_plugins(config.DrydockConfig.ingester_config.get('plugins', [])) + + return api.start_api(state_manager=state, ingester=input_ingester, + orchestrator=orchestrator) + +drydock = start_drydock() + diff --git a/helm_drydock/error.py b/drydock_provisioner/error.py similarity index 92% rename from helm_drydock/error.py rename to drydock_provisioner/error.py index a8988f97..04a38196 100644 --- a/helm_drydock/error.py +++ b/drydock_provisioner/error.py @@ -34,4 +34,10 @@ class TransientDriverError(DriverError): pass class PersistentDriverError(DriverError): + pass + +class ApiError(Exception): + pass + +class InvalidFormat(ApiError): pass \ No newline at end of file diff --git a/drydock_provisioner/ingester/__init__.py b/drydock_provisioner/ingester/__init__.py new file mode 100644 index 00000000..daaff310 --- /dev/null +++ b/drydock_provisioner/ingester/__init__.py @@ -0,0 +1,120 @@ +# Copyright 2017 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. +# +# ingester - Ingest host topologies to define site design and +# persist design to helm-drydock's statemgmt service + +import logging +import yaml +import uuid +import importlib + +import drydock_provisioner.objects as objects +import drydock_provisioner.objects.site as site +import drydock_provisioner.objects.network as network +import drydock_provisioner.objects.hwprofile as hwprofile +import drydock_provisioner.objects.node as node +import drydock_provisioner.objects.hostprofile as hostprofile + +from drydock_provisioner.statemgmt import DesignState + +class Ingester(object): + + def __init__(self): + self.logger = logging.getLogger("drydock.ingester") + self.registered_plugins = {} + + def enable_plugins(self, plugins=[]): + """ + enable_plugins + + :params plugins: - A list of strings naming class objects denoting the ingester plugins to be enabled + + Enable plugins that can be used for ingest_data calls. Each plugin should use + drydock_provisioner.ingester.plugins.IngesterPlugin as its base class. As long as one + enabled plugin successfully initializes, the call is considered successful. Otherwise + it will throw an exception + """ + if len(plugins) == 0: + self.log.error("Cannot have an empty plugin list.") + + for plugin in plugins: + try: + (module, x, classname) = plugin.rpartition('.') + + if module == '': + raise Exception() + mod = importlib.import_module(module) + klass = getattr(mod, classname) + new_plugin = klass() + plugin_name = new_plugin.get_name() + self.registered_plugins[plugin_name] = new_plugin + except Exception as ex: + self.logger.error("Could not enable plugin %s - %s" % (plugin, str(ex))) + + if len(self.registered_plugins) == 0: + self.logger.error("Could not enable at least one plugin") + raise Exception("Could not enable at least one plugin") + + + def ingest_data(self, plugin_name='', design_state=None, design_id=None, context=None, **kwargs): + if design_state is None: + self.logger.error("Ingester:ingest_data called without valid DesignState handler") + raise ValueError("Invalid design_state handler") + + # If no design_id is specified, instantiate a new one + if 'design_id' is None: + self.logger.error("Ingester:ingest_data required kwarg 'design_id' missing") + raise ValueError("Ingester:ingest_data required kwarg 'design_id' missing") + + design_data = design_state.get_design(design_id) + + self.logger.debug("Ingester:ingest_data ingesting design parts for design %s" % design_id) + + if plugin_name in self.registered_plugins: + try: + design_items = self.registered_plugins[plugin_name].ingest_data(**kwargs) + except ValueError as vex: + self.logger.warn("Ingester:ingest_data - Error process data - %s" % (str(vex))) + return None + self.logger.debug("Ingester:ingest_data parsed %s design parts" % str(len(design_items))) + for m in design_items: + if context is not None: + m.set_create_fields(context) + if type(m) is site.Site: + design_data.set_site(m) + elif type(m) is network.Network: + design_data.add_network(m) + elif type(m) is network.NetworkLink: + design_data.add_network_link(m) + elif type(m) is hostprofile.HostProfile: + design_data.add_host_profile(m) + elif type(m) is hwprofile.HardwareProfile: + design_data.add_hardware_profile(m) + elif type(m) is node.BaremetalNode: + design_data.add_baremetal_node(m) + design_state.put_design(design_data) + return design_items + else: + self.logger.error("Could not find plugin %s to ingest data." % (plugin_name)) + raise LookupError("Could not find plugin %s" % plugin_name) + """ + ingest_data + + params: plugin_name - Which plugin should be used for ingestion + params: params - A map of parameters that will be passed to the plugin's ingest_data method + + Execute a data ingestion using the named plugin (assuming it is enabled) + """ + diff --git a/helm_drydock/ingester/plugins/__init__.py b/drydock_provisioner/ingester/plugins/__init__.py similarity index 100% rename from helm_drydock/ingester/plugins/__init__.py rename to drydock_provisioner/ingester/plugins/__init__.py diff --git a/helm_drydock/ingester/plugins/yaml.py b/drydock_provisioner/ingester/plugins/yaml.py similarity index 97% rename from helm_drydock/ingester/plugins/yaml.py rename to drydock_provisioner/ingester/plugins/yaml.py index ce531f8c..d3a9c4dd 100644 --- a/helm_drydock/ingester/plugins/yaml.py +++ b/drydock_provisioner/ingester/plugins/yaml.py @@ -19,15 +19,16 @@ import yaml import logging -import helm_drydock.objects.fields as hd_fields +import drydock_provisioner.objects.fields as hd_fields -from helm_drydock import objects -from helm_drydock.ingester.plugins import IngesterPlugin +from drydock_provisioner import objects +from drydock_provisioner.ingester.plugins import IngesterPlugin class YamlIngester(IngesterPlugin): def __init__(self): super(YamlIngester, self).__init__() + self.logger = logging.getLogger('drydock.ingester.yaml') def get_name(self): return "yaml" @@ -37,7 +38,7 @@ class YamlIngester(IngesterPlugin): filenames - Array of absolute path to the YAML files to ingest - returns an array of objects from helm_drydock.model + returns an array of objects from drydock_provisioner.model """ def ingest_data(self, **kwargs): @@ -52,12 +53,10 @@ class YamlIngester(IngesterPlugin): file.close() models.extend(self.parse_docs(contents)) except OSError as err: - self.log.error( + self.logger.error( "Error opening input file %s for ingestion: %s" % (filename, err)) continue - - elif 'content' in kwargs: models.extend(self.parse_docs(kwargs.get('content'))) else: @@ -71,6 +70,8 @@ class YamlIngester(IngesterPlugin): def parse_docs(self, yaml_string): models = [] + self.logger.debug("yamlingester:parse_docs - Parsing YAML string \n%s" % (yaml_string)) + try: parsed_data = yaml.load_all(yaml_string) except yaml.YAMLError as err: diff --git a/helm_drydock/ingester/readme.md b/drydock_provisioner/ingester/readme.md similarity index 84% rename from helm_drydock/ingester/readme.md rename to drydock_provisioner/ingester/readme.md index 1437c216..16352970 100644 --- a/helm_drydock/ingester/readme.md +++ b/drydock_provisioner/ingester/readme.md @@ -6,7 +6,7 @@ different sources. Each ingester plugin should be able source data based on user-provided parameters and parse that data -into the Drydock internal model (helm_drydock.model). +into the Drydock internal model (drydock_provisioner.model). Each plugin does not need to support every type of design data as a single site design may be federated from multiple diff --git a/helm_drydock/introspection/readme.md b/drydock_provisioner/introspection/readme.md similarity index 100% rename from helm_drydock/introspection/readme.md rename to drydock_provisioner/introspection/readme.md diff --git a/helm_drydock/objects/__init__.py b/drydock_provisioner/objects/__init__.py similarity index 93% rename from helm_drydock/objects/__init__.py rename to drydock_provisioner/objects/__init__.py index b88365d2..be295cf6 100644 --- a/helm_drydock/objects/__init__.py +++ b/drydock_provisioner/objects/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Models for helm_drydock +# Models for drydock_provisioner # import logging @@ -23,11 +23,11 @@ def register_all(): # NOTE(sh8121att) - Import all versioned objects so # they are available via RPC. Any new object definitions # need to be added here. - __import__('helm_drydock.objects.network') - __import__('helm_drydock.objects.node') - __import__('helm_drydock.objects.hostprofile') - __import__('helm_drydock.objects.hwprofile') - __import__('helm_drydock.objects.site') + __import__('drydock_provisioner.objects.network') + __import__('drydock_provisioner.objects.node') + __import__('drydock_provisioner.objects.hostprofile') + __import__('drydock_provisioner.objects.hwprofile') + __import__('drydock_provisioner.objects.site') # Utility class for calculating inheritance diff --git a/helm_drydock/objects/base.py b/drydock_provisioner/objects/base.py similarity index 59% rename from helm_drydock/objects/base.py rename to drydock_provisioner/objects/base.py index d22b2183..3bf2c027 100644 --- a/helm_drydock/objects/base.py +++ b/drydock_provisioner/objects/base.py @@ -11,16 +11,17 @@ # 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. +import datetime from oslo_versionedobjects import base from oslo_versionedobjects import fields as obj_fields -import helm_drydock.objects as objects +import drydock_provisioner.objects as objects class DrydockObjectRegistry(base.VersionedObjectRegistry): # Steal this from Cinder to bring all registered objects - # into the helm_drydock.objects namespace + # into the drydock_provisioner.objects namespace def registration_hook(self, cls, index): setattr(objects, cls.obj_name(), cls) @@ -29,7 +30,7 @@ class DrydockObject(base.VersionedObject): VERSION = '1.0' - OBJ_PROJECT_NAMESPACE = 'helm_drydock.objects' + OBJ_PROJECT_NAMESPACE = 'drydock_provisioner.objects' # Return None for undefined attributes def obj_load_attr(self, attrname): @@ -38,6 +39,32 @@ class DrydockObject(base.VersionedObject): else: raise ValueError("Unknown field %s" % (attrname)) + def obj_to_simple(self): + """ + Create a simple primitive representation of this object excluding + all the versioning stuff. Used to serialize an object for public + consumption, not intended to be deserialized by OVO + """ + + primitive = dict() + + primitive['model_type'] = self.__class__.__name__ + primitive['model_version'] = self.VERSION + + for name, field in self.fields.items(): + if self.obj_attr_is_set(name): + value = getattr(self, name) + if (hasattr(value, 'obj_to_simple') and + callable(value.obj_to_simple)): + primitive[name] = value.obj_to_simple() + else: + value = field.to_primitive(self, name, value) + if value is not None: + primitive[name] = value + + return primitive + + class DrydockPersistentObject(base.VersionedObject): fields = { @@ -47,6 +74,15 @@ class DrydockPersistentObject(base.VersionedObject): 'updated_by': obj_fields.StringField(nullable=True), } + def set_create_fields(self, context): + self.created_at = datetime.datetime.now() + self.created_by = context.user + + def set_update_fields(self, context): + self.updated_at = datetime.datetime.now() + self.updated_by = context.user + + class DrydockObjectListBase(base.ObjectListBase): def __init__(self, **kwargs): @@ -73,3 +109,11 @@ class DrydockObjectListBase(base.ObjectListBase): model_list.append(o) return model_list + + def obj_to_simple(self): + primitive_list = list() + + for o in self.objects: + primitive_list.append(o.obj_to_simple()) + + return primitive_list diff --git a/helm_drydock/objects/fields.py b/drydock_provisioner/objects/fields.py similarity index 99% rename from helm_drydock/objects/fields.py rename to drydock_provisioner/objects/fields.py index c6ac8ac3..2abb6098 100644 --- a/helm_drydock/objects/fields.py +++ b/drydock_provisioner/objects/fields.py @@ -44,6 +44,7 @@ class OrchestratorAction(BaseDrydockEnum): CreateStorageTemplate = 'create_storage_template' CreateBootMedia = 'create_boot_media' PrepareHardwareConfig = 'prepare_hardware_config' + IdentifyNode = 'identify_node' ConfigureHardware = 'configure_hardware' InterrogateNode = 'interrogate_node' ApplyNodeNetworking = 'apply_node_networking' diff --git a/helm_drydock/objects/hostprofile.py b/drydock_provisioner/objects/hostprofile.py similarity index 98% rename from helm_drydock/objects/hostprofile.py rename to drydock_provisioner/objects/hostprofile.py index 5a416dde..36aad003 100644 --- a/helm_drydock/objects/hostprofile.py +++ b/drydock_provisioner/objects/hostprofile.py @@ -16,9 +16,9 @@ from copy import deepcopy import oslo_versionedobjects.fields as obj_fields -import helm_drydock.objects as objects -import helm_drydock.objects.base as base -import helm_drydock.objects.fields as hd_fields +import drydock_provisioner.objects as objects +import drydock_provisioner.objects.base as base +import drydock_provisioner.objects.fields as hd_fields @base.DrydockObjectRegistry.register diff --git a/helm_drydock/objects/hwprofile.py b/drydock_provisioner/objects/hwprofile.py similarity index 96% rename from helm_drydock/objects/hwprofile.py rename to drydock_provisioner/objects/hwprofile.py index 3ff5afbe..bda20183 100644 --- a/helm_drydock/objects/hwprofile.py +++ b/drydock_provisioner/objects/hwprofile.py @@ -16,9 +16,9 @@ from copy import deepcopy from oslo_versionedobjects import fields as ovo_fields -import helm_drydock.objects as objects -import helm_drydock.objects.base as base -import helm_drydock.objects.fields as hd_fields +import drydock_provisioner.objects as objects +import drydock_provisioner.objects.base as base +import drydock_provisioner.objects.fields as hd_fields @base.DrydockObjectRegistry.register class HardwareProfile(base.DrydockPersistentObject, base.DrydockObject): diff --git a/helm_drydock/objects/network.py b/drydock_provisioner/objects/network.py similarity index 94% rename from helm_drydock/objects/network.py rename to drydock_provisioner/objects/network.py index e1ccc693..028161da 100644 --- a/helm_drydock/objects/network.py +++ b/drydock_provisioner/objects/network.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Models for helm_drydock +# Models for drydock_provisioner # import logging @@ -20,9 +20,9 @@ from copy import deepcopy import oslo_versionedobjects.fields as ovo_fields -import helm_drydock.objects as objects -import helm_drydock.objects.base as base -import helm_drydock.objects.fields as hd_fields +import drydock_provisioner.objects as objects +import drydock_provisioner.objects.base as base +import drydock_provisioner.objects.fields as hd_fields @base.DrydockObjectRegistry.register class NetworkLink(base.DrydockPersistentObject, base.DrydockObject): diff --git a/helm_drydock/objects/node.py b/drydock_provisioner/objects/node.py similarity index 92% rename from helm_drydock/objects/node.py rename to drydock_provisioner/objects/node.py index 1d353576..7854b346 100644 --- a/helm_drydock/objects/node.py +++ b/drydock_provisioner/objects/node.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Models for helm_drydock +# Models for drydock_provisioner # import logging @@ -20,13 +20,13 @@ from copy import deepcopy from oslo_versionedobjects import fields as ovo_fields -import helm_drydock.objects as objects -import helm_drydock.objects.hostprofile -import helm_drydock.objects.base as base -import helm_drydock.objects.fields as hd_fields +import drydock_provisioner.objects as objects +import drydock_provisioner.objects.hostprofile +import drydock_provisioner.objects.base as base +import drydock_provisioner.objects.fields as hd_fields @base.DrydockObjectRegistry.register -class BaremetalNode(helm_drydock.objects.hostprofile.HostProfile): +class BaremetalNode(drydock_provisioner.objects.hostprofile.HostProfile): VERSION = '1.0' diff --git a/helm_drydock/objects/readme.md b/drydock_provisioner/objects/readme.md similarity index 100% rename from helm_drydock/objects/readme.md rename to drydock_provisioner/objects/readme.md diff --git a/helm_drydock/objects/site.py b/drydock_provisioner/objects/site.py similarity index 93% rename from helm_drydock/objects/site.py rename to drydock_provisioner/objects/site.py index f786d911..c86a9184 100644 --- a/helm_drydock/objects/site.py +++ b/drydock_provisioner/objects/site.py @@ -12,16 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Models for helm_drydock +# Models for drydock_provisioner # from copy import deepcopy import uuid +import datetime import oslo_versionedobjects.fields as ovo_fields -import helm_drydock.objects as objects -import helm_drydock.objects.base as base -import helm_drydock.objects.fields as hd_fields +import drydock_provisioner.objects as objects +import drydock_provisioner.objects.base as base +import drydock_provisioner.objects.fields as hd_fields @base.DrydockObjectRegistry.register @@ -126,8 +127,6 @@ class SiteDesign(base.DrydockPersistentObject, base.DrydockObject): def __init__(self, **kwargs): super(SiteDesign, self).__init__(**kwargs) - - # Assign UUID id def assign_id(self): self.id = uuid.uuid4() @@ -228,6 +227,18 @@ class SiteDesign(base.DrydockPersistentObject, base.DrydockObject): raise DesignError("BaremetalNode %s not found in design state" % node_key) + def create(self, ctx, state_manager): + self.created_at = datetime.datetime.now() + self.created_by = ctx.user + + state_manager.post_design(self) + + def save(self, ctx, state_manager): + self.updated_at = datetime.datetime.now() + self.updated_by = ctx.user + + state_manager.put_design(self) + """ Support filtering on rack name, node name or node tag for now. Each filter can be a comma-delimited list of diff --git a/helm_drydock/objects/task.py b/drydock_provisioner/objects/task.py similarity index 70% rename from helm_drydock/objects/task.py rename to drydock_provisioner/objects/task.py index 9985b285..6da718c8 100644 --- a/helm_drydock/objects/task.py +++ b/drydock_provisioner/objects/task.py @@ -15,9 +15,9 @@ import uuid from threading import Lock -import helm_drydock.error as errors +import drydock_provisioner.error as errors -import helm_drydock.objects.fields as hd_fields +import drydock_provisioner.objects.fields as hd_fields class Task(object): @@ -66,18 +66,29 @@ class Task(object): def get_subtasks(self): return self.subtasks + def to_dict(self): + return { + 'task_id': str(self.task_id), + 'action': self.action, + 'parent_task': str(self.parent_task_id), + 'status': self.status, + 'result': self.result, + 'result_detail': self.result_detail, + 'subtasks': [str(x) for x in self.subtasks], + } + class OrchestratorTask(Task): - def __init__(self, **kwargs): + def __init__(self, site=None, design_id=None, **kwargs): super(OrchestratorTask, self).__init__(**kwargs) # Validate parameters based on action - self.site = kwargs.get('site', '') + self.site = site - if self.site == '': + if self.site is None: raise ValueError("Orchestration Task requires 'site' parameter") - self.design_id = kwargs.get('design_id', 0) + self.design_id = design_id if self.action in [hd_fields.OrchestratorAction.VerifyNode, hd_fields.OrchestratorAction.PrepareNode, @@ -85,6 +96,14 @@ class OrchestratorTask(Task): hd_fields.OrchestratorAction.DestroyNode]: self.node_filter = kwargs.get('node_filter', None) + def to_dict(self): + _dict = super(OrchestratorTask, self).to_dict() + + _dict['site'] = self.site + _dict['design_id'] = self.design_id + _dict['node_filter'] = getattr(self, 'node_filter', None) + + return _dict class DriverTask(Task): def __init__(self, task_scope={}, **kwargs): @@ -94,4 +113,13 @@ class DriverTask(Task): self.site_name = task_scope.get('site', None) - self.node_list = task_scope.get('node_names', []) \ No newline at end of file + self.node_list = task_scope.get('node_names', []) + + def to_dict(self): + _dict = super(DriverTask, self).to_dict() + + _dict['site_name'] = self.site_name + _dict['design_id'] = self.design_id + _dict['node_list'] = self.node_list + + return _dict \ No newline at end of file diff --git a/helm_drydock/orchestrator/__init__.py b/drydock_provisioner/orchestrator/__init__.py similarity index 71% rename from helm_drydock/orchestrator/__init__.py rename to drydock_provisioner/orchestrator/__init__.py index 2b589156..69984336 100644 --- a/helm_drydock/orchestrator/__init__.py +++ b/drydock_provisioner/orchestrator/__init__.py @@ -1,3 +1,4 @@ + # Copyright 2017 AT&T Intellectual Property. All other rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,13 +16,14 @@ import uuid import time import threading import importlib +import logging from copy import deepcopy -import helm_drydock.drivers as drivers -import helm_drydock.objects.task as tasks -import helm_drydock.error as errors -import helm_drydock.objects.fields as hd_fields +import drydock_provisioner.drivers as drivers +import drydock_provisioner.objects.task as tasks +import drydock_provisioner.error as errors +import drydock_provisioner.objects.fields as hd_fields class Orchestrator(object): @@ -32,6 +34,8 @@ class Orchestrator(object): self.state_manager = state_manager + self.logger = logging.getLogger('drydock.orchestrator') + if enabled_drivers is not None: oob_driver_name = enabled_drivers.get('oob', None) if oob_driver_name is not None: @@ -106,8 +110,7 @@ class Orchestrator(object): self.task_field_update(task_id, status=hd_fields.TaskStatus.Running) try: - site_design = self.get_effective_site(task_site, - change_id=design_id) + site_design = self.get_effective_site(design_id) self.task_field_update(task_id, result=hd_fields.ActionResult.Success) except: @@ -155,10 +158,14 @@ class Orchestrator(object): task_scope=task_scope, action=hd_fields.OrchestratorAction.CreateNetworkTemplate) + self.logger.info("Starting node driver task %s to create network templates" % (driver_task.get_id())) + driver.execute_task(driver_task.get_id()) driver_task = self.state_manager.get_task(driver_task.get_id()) + self.logger.info("Node driver task %s complete" % (driver_task.get_id())) + self.task_field_update(task_id, status=hd_fields.TaskStatus.Complete, result=driver_task.get_result()) @@ -167,15 +174,16 @@ class Orchestrator(object): self.task_field_update(task_id, status=hd_fields.TaskStatus.Running) - driver = self.enabled_drivers['oob'] + oob_driver = self.enabled_drivers['oob'] - if driver is None: + if oob_driver is None: self.task_field_update(task_id, status=hd_fields.TaskStatus.Errored, - result=hd_fields.ActionResult.Failure) + result=hd_fields.ActionResult.Failure, + result_detail={'detail': 'Error: No oob driver configured', 'retry': False}) return - site_design = self.get_effective_site(design_id, task_site) + site_design = self.get_effective_site(design_id) node_filter = task.node_filter @@ -186,34 +194,45 @@ class Orchestrator(object): task_scope = {'site' : task_site, 'node_names' : target_names} - driver_task = self.create_task(tasks.DriverTask, + oob_driver_task = self.create_task(tasks.DriverTask, parent_task_id=task.get_id(), design_id=design_id, - action=hd_fields.OrchestratorAction.InterrogateNode, + action=hd_fields.OrchestratorAction.InterrogateOob, task_scope=task_scope) - driver.execute_task(driver_task.get_id()) + oob_driver.execute_task(oob_driver_task.get_id()) - driver_task = self.state_manager.get_task(driver_task.get_id()) + oob_driver_task = self.state_manager.get_task(oob_driver_task.get_id()) self.task_field_update(task_id, status=hd_fields.TaskStatus.Complete, - result=driver_task.get_result()) + result=oob_driver_task.get_result()) return elif task.action == hd_fields.OrchestratorAction.PrepareNode: + failed = worked = False + self.task_field_update(task_id, status=hd_fields.TaskStatus.Running) - driver = self.enabled_drivers['oob'] + oob_driver = self.enabled_drivers['oob'] - if driver is None: + if oob_driver is None: self.task_field_update(task_id, status=hd_fields.TaskStatus.Errored, - result=hd_fields.ActionResult.Failure) + result=hd_fields.ActionResult.Failure, + result_detail={'detail': 'Error: No oob driver configured', 'retry': False}) return - site_design = self.get_effective_site(task_site, - change_id=design_id) + node_driver = self.enabled_drivers['node'] + + if node_driver is None: + self.task_field_update(task_id, + status=hd_fields.TaskStatus.Errored, + result=hd_fields.ActionResult.Failure, + result_detail={'detail': 'Error: No node driver configured', 'retry': False}) + return + + site_design = self.get_effective_site(design_id) node_filter = task.node_filter @@ -230,33 +249,88 @@ class Orchestrator(object): action=hd_fields.OrchestratorAction.SetNodeBoot, task_scope=task_scope) - driver.execute_task(setboot_task.get_id()) + self.logger.info("Starting OOB driver task %s to set PXE boot" % (setboot_task.get_id())) + + oob_driver.execute_task(setboot_task.get_id()) + + self.logger.info("OOB driver task %s complete" % (setboot_task.get_id())) setboot_task = self.state_manager.get_task(setboot_task.get_id()) + if setboot_task.get_result() == hd_fields.ActionResult.Success: + worked = True + elif setboot_task.get_result() == hd_fields.ActionResult.PartialSuccess: + worked = failed = True + elif setboot_task.get_result() == hd_fields.ActionResult.Failure: + failed = True + cycle_task = self.create_task(tasks.DriverTask, parent_task_id=task.get_id(), design_id=design_id, action=hd_fields.OrchestratorAction.PowerCycleNode, task_scope=task_scope) - driver.execute_task(cycle_task.get_id()) + + self.logger.info("Starting OOB driver task %s to power cycle nodes" % (cycle_task.get_id())) + + oob_driver.execute_task(cycle_task.get_id()) + + self.logger.info("OOB driver task %s complete" % (cycle_task.get_id())) cycle_task = self.state_manager.get_task(cycle_task.get_id()) - if (setboot_task.get_result() == hd_fields.ActionResult.Success and - cycle_task.get_result() == hd_fields.ActionResult.Success): - self.task_field_update(task_id, - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Success) - elif (setboot_task.get_result() == hd_fields.ActionResult.Success or - cycle_task.get_result() == hd_fields.ActionResult.Success): - self.task_field_update(task_id, - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.PartialSuccess) + if cycle_task.get_result() == hd_fields.ActionResult.Success: + worked = True + elif cycle_task.get_result() == hd_fields.ActionResult.PartialSuccess: + worked = failed = True + elif cycle_task.get_result() == hd_fields.ActionResult.Failure: + failed = True + + + # IdentifyNode success will take some time after PowerCycleNode finishes + # Retry the operation a few times if it fails before considering it a final failure + # Each attempt is a new task which might make the final task tree a bit confusing + + node_identify_attempts = 0 + + while True: + + node_identify_task = self.create_task(tasks.DriverTask, + parent_task_id=task.get_id(), + design_id=design_id, + action=hd_fields.OrchestratorAction.IdentifyNode, + task_scope=task_scope) + + self.logger.info("Starting node driver task %s to identify node - attempt %s" % + (node_identify_task.get_id(), node_identify_attempts+1)) + + node_driver.execute_task(node_identify_task.get_id()) + node_identify_attempts = node_identify_attempts + 1 + + node_identify_task = self.state_manager.get_task(node_identify_task.get_id()) + + if node_identify_task.get_result() == hd_fields.ActionResult.Success: + worked = True + break + elif node_identify_task.get_result() in [hd_fields.ActionResult.PartialSuccess, + hd_fields.ActionResult.Failure]: + # TODO This threshold should be a configurable default and tunable by task API + if node_identify_attempts > 2: + failed = True + break + + time.sleep(5 * 60) + + final_result = None + if worked and failed: + final_result = hd_fields.ActionResult.PartialSuccess + elif worked: + final_result = hd_fields.ActionResult.Success else: - self.task_field_update(task_id, - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Failure) + final_result = hd_fields.ActionResult.Failure + + self.task_field_update(task_id, + status=hd_fields.TaskStatus.Complete, + result=final_result) return else: @@ -331,7 +405,7 @@ class Orchestrator(object): # the baremetal nodes which recursively resolves it for host profiles # assigned to those nodes - for n in site_design.baremetal_nodes: + for n in getattr(site_design, 'baremetal_nodes', []): n.compile_applied_model(site_design) return @@ -342,18 +416,13 @@ class Orchestrator(object): return a Site model reflecting the effective design for the site """ - def get_described_site(self, design_id, site_name): - site_design = None - - if site_name is None: - raise errors.OrchestratorError("Cannot source design for site None") - + def get_described_site(self, design_id): site_design = self.state_manager.get_design(design_id) return site_design - def get_effective_site(self, design_id, site_name): - site_design = self.get_described_site(design_id, site_name) + def get_effective_site(self, design_id): + site_design = self.get_described_site(design_id) self.compute_model_inheritance(site_design) diff --git a/helm_drydock/orchestrator/readme.md b/drydock_provisioner/orchestrator/readme.md similarity index 100% rename from helm_drydock/orchestrator/readme.md rename to drydock_provisioner/orchestrator/readme.md diff --git a/helm_drydock/statemgmt/__init__.py b/drydock_provisioner/statemgmt/__init__.py similarity index 97% rename from helm_drydock/statemgmt/__init__.py rename to drydock_provisioner/statemgmt/__init__.py index 22112c29..a6be0d0c 100644 --- a/helm_drydock/statemgmt/__init__.py +++ b/drydock_provisioner/statemgmt/__init__.py @@ -18,10 +18,10 @@ from threading import Lock import uuid -import helm_drydock.objects as objects -import helm_drydock.objects.task as tasks +import drydock_provisioner.objects as objects +import drydock_provisioner.objects.task as tasks -from helm_drydock.error import DesignError, StateError +from drydock_provisioner.error import DesignError, StateError class DesignState(object): @@ -41,6 +41,7 @@ class DesignState(object): # has started def get_design(self, design_id): if design_id not in self.designs.keys(): + raise DesignError("Design ID %s not found" % (design_id)) return objects.SiteDesign.obj_from_primitive(self.designs[design_id]) @@ -133,7 +134,7 @@ class DesignState(object): def get_task(self, task_id): for t in self.tasks: - if t.get_id() == task_id: + if t.get_id() == task_id or str(t.get_id()) == task_id: return deepcopy(t) return None diff --git a/helm_drydock/statemgmt/readme.md b/drydock_provisioner/statemgmt/readme.md similarity index 100% rename from helm_drydock/statemgmt/readme.md rename to drydock_provisioner/statemgmt/readme.md diff --git a/examples/designparts_v1.0.yaml b/examples/designparts_v1.0.yaml index 530efbc0..1f669fab 100644 --- a/examples/designparts_v1.0.yaml +++ b/examples/designparts_v1.0.yaml @@ -153,7 +153,7 @@ metadata: description: Describe server configuration attributes. Not a specific server, but profile adopted by a server definition spec: # The HostProfile this profile adopts initial state from. No default. - # See helm_drydock/objects/readme.md for information on how HostProfile and BaremetalNode inheritance works + # See drydock_provisioner/objects/readme.md for information on how HostProfile and BaremetalNode inheritance works host_profile: 'defaults' # The HardwareProfile describing the node hardware. No default. hardware_profile: 'DellR720v1' @@ -242,7 +242,7 @@ metadata: description: Specify a physical server. spec: # The HostProfile this server adopts initial state from. No default. - # See helm_drydock/objects/readme.md for information on how HostProfile and BaremetalNode inheritance works + # See drydock_provisioner/objects/readme.md for information on how HostProfile and BaremetalNode inheritance works host_profile: 'defaults' # The HardwareProfile describing the node hardware. No default. hardware_profile: 'DellR720v1' diff --git a/helm_drydock/control/readme.md b/helm_drydock/control/readme.md deleted file mode 100644 index 2dad5b24..00000000 --- a/helm_drydock/control/readme.md +++ /dev/null @@ -1,14 +0,0 @@ -# Control # - -This is the external facing API service to control the rest -of Drydock and query Drydock-managed data. - -Anticipate basing this service on the falcon Python library - -## Endpoints ## - -### /tasks ### - -POST - Create a new orchestration task and submit it for execution -GET - Get status of a task -DELETE - Cancel execution of a task if permitted diff --git a/helm_drydock/drivers/node/maasdriver/driver.py b/helm_drydock/drivers/node/maasdriver/driver.py deleted file mode 100644 index 83406a18..00000000 --- a/helm_drydock/drivers/node/maasdriver/driver.py +++ /dev/null @@ -1,306 +0,0 @@ -# Copyright 2017 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. -import helm_drydock.error as errors -import helm_drydock.config as config -import helm_drydock.drivers as drivers -import helm_drydock.objects.fields as hd_fields -import helm_drydock.objects.task as task_model - -from helm_drydock.drivers.node import NodeDriver -from .api_client import MaasRequestFactory -import helm_drydock.drivers.node.maasdriver.models.fabric as maas_fabric -import helm_drydock.drivers.node.maasdriver.models.vlan as maas_vlan -import helm_drydock.drivers.node.maasdriver.models.subnet as maas_subnet - -class MaasNodeDriver(NodeDriver): - - def __init__(self, **kwargs): - super(MaasNodeDriver, self).__init__(**kwargs) - - self.driver_name = "maasdriver" - self.driver_key = "maasdriver" - self.driver_desc = "MaaS Node Provisioning Driver" - - self.config = config.DrydockConfig.node_driver[self.driver_key] - - def execute_task(self, task_id): - task = self.state_manager.get_task(task_id) - - if task is None: - raise errors.DriverError("Invalid task %s" % (task_id)) - - if task.action not in self.supported_actions: - raise errors.DriverError("Driver %s doesn't support task action %s" - % (self.driver_desc, task.action)) - - if task.action == hd_fields.OrchestratorAction.ValidateNodeServices: - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Running) - maas_client = MaasRequestFactory(self.config['api_url'], self.config['api_key']) - - try: - if maas_client.test_connectivity(): - if maas_client.test_authentication(): - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Success) - return - except errors.TransientDriverError(ex): - result = { - 'retry': True, - 'detail': str(ex), - } - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Failure, - result_details=result) - return - except errors.PersistentDriverError(ex): - result = { - 'retry': False, - 'detail': str(ex), - } - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Failure, - result_details=result) - return - except Exception(ex): - result = { - 'retry': False, - 'detail': str(ex), - } - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Failure, - result_details=result) - return - - design_id = getattr(task, 'design_id', None) - - if design_id is None: - raise errors.DriverError("No design ID specified in task %s" % - (task_id)) - - - if task.site_name is None: - raise errors.DriverError("No site specified for task %s." % - (task_id)) - - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Running) - - site_design = self.orchestrator.get_effective_site(design_id, task.site_name) - - if task.action == hd_fields.OrchestratorAction.CreateNetworkTemplate: - subtask = self.orchestrator.create_task(task_model.DriverTask, - parent_task_id=task.get_id(), design_id=design_id, - action=task.action, site_name=task.site_name, - task_scope={'site': task.site_name}) - runner = MaasTaskRunner(state_manager=self.state_manager, - orchestrator=self.orchestrator, - task_id=subtask.get_id(),config=self.config) - runner.start() - - runner.join(timeout=120) - - if runner.is_alive(): - result = { - 'retry': False, - 'detail': 'MaaS Network creation timed-out' - } - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Failure, - result_detail=result) - else: - subtask = self.state_manager.get_task(subtask.get_id()) - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=subtask.get_result()) - - return - -class MaasTaskRunner(drivers.DriverTaskRunner): - - def __init__(self, config=None, **kwargs): - super(MaasTaskRunner, self).__init__(**kwargs) - - self.driver_config = config - - def execute_task(self): - task_action = self.task.action - - self.orchestrator.task_field_update(self.task.get_id(), - status=hd_fields.TaskStatus.Running, - result=hd_fields.ActionResult.Incomplete) - - self.maas_client = MaasRequestFactory(self.driver_config['api_url'], - self.driver_config['api_key']) - - site_design = self.orchestrator.get_effective_site(self.task.design_id, - self.task.site_name) - - if task_action == hd_fields.OrchestratorAction.CreateNetworkTemplate: - # Try to true up MaaS definitions of fabrics/vlans/subnets - # with the networks defined in Drydock - design_networks = site_design.networks - - subnets = maas_subnet.Subnets(self.maas_client) - subnets.refresh() - - result_detail = { - 'detail': [] - } - - for n in design_networks: - exists = subnets.query({'cidr': n.cidr}) - - subnet = None - - if len(exists) > 0: - subnet = exists[0] - - subnet.name = n.name - subnet.dns_servers = n.dns_servers - - vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=subnet.fabric) - vlan_list.refresh() - - vlan = vlan_list.select(subnet.vlan) - - if vlan is not None: - if ((n.vlan_id is None and vlan.vid != 0) or - (n.vlan_id is not None and vlan.vid != n.vlan_id)): - - # if the VLAN name matches, assume this is the correct resource - # and it needs to be updated - if vlan.name == n.name: - vlan.set_vid(n.vlan_id) - vlan.mtu = n.mtu - vlan.update() - else: - vlan_id = n.vlan_id if n.vlan_id is not None else 0 - target_vlan = vlan_list.query({'vid': vlan_id}) - if len(target_vlan) > 0: - subnet.vlan = target_vlan[0].resource_id - else: - # This is a flag that after creating a fabric and - # VLAN below, update the subnet - subnet.vlan = None - else: - subnet.vlan = None - - # Check if the routes have a default route - subnet.gateway_ip = n.get_default_gateway() - - - result_detail['detail'].append("Subnet %s found for network %s, updated attributes" - % (exists[0].resource_id, n.name)) - - # Need to create a Fabric/Vlan for this network - if (subnet is None or (subnet is not None and subnet.vlan is None)): - fabric_list = maas_fabric.Fabrics(self.maas_client) - fabric_list.refresh() - matching_fabrics = fabric_list.query({'name': n.name}) - - fabric = None - vlan = None - - if len(matching_fabrics) > 0: - # Fabric exists, update VLAN - fabric = matching_fabrics[0] - - vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=fabric.resource_id) - vlan_list.refresh() - vlan_id = n.vlan_id if n.vlan_id is not None else 0 - matching_vlans = vlan_list.query({'vid': vlan_id}) - - if len(matching_vlans) > 0: - vlan = matching_vlans[0] - - vlan.name = n.name - if getattr(n, 'mtu', None) is not None: - vlan.mtu = n.mtu - - if subnet is not None: - subnet.vlan = vlan.resource_id - subnet.update() - vlan.update() - else: - vlan = maas_vlan.Vlan(self.maas_client, name=n.name, vid=vlan_id, - mtu=getattr(n, 'mtu', None),fabric_id=fabric.resource_id) - vlan = vlan_list.add(vlan) - - if subnet is not None: - subnet.vlan = vlan.resource_id - subnet.update() - - else: - new_fabric = maas_fabric.Fabric(self.maas_client, name=n.name) - new_fabric = fabric_list.add(new_fabric) - new_fabric.refresh() - fabric = new_fabric - - vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=new_fabric.resource_id) - vlan_list.refresh() - vlan = vlan_list.single() - - vlan.name = n.name - vlan.vid = n.vlan_id if n.vlan_id is not None else 0 - if getattr(n, 'mtu', None) is not None: - vlan.mtu = n.mtu - - vlan.update() - - if subnet is not None: - subnet.vlan = vlan.resource_id - subnet.update() - - if subnet is None: - subnet = maas_subnet.Subnet(self.maas_client, name=n.name, cidr=n.cidr, fabric=fabric.resource_id, - vlan=vlan.resource_id, gateway_ip=n.get_default_gateway()) - - subnet_list = maas_subnet.Subnets(self.maas_client) - subnet = subnet_list.add(subnet) - - subnet_list = maas_subnet.Subnets(self.maas_client) - subnet_list.refresh() - - action_result = hd_fields.ActionResult.Incomplete - - success_rate = 0 - - for n in design_networks: - exists = subnet_list.query({'cidr': n.cidr}) - if len(exists) > 0: - subnet = exists[0] - if subnet.name == n.name: - success_rate = success_rate + 1 - else: - success_rate = success_rate + 1 - else: - success_rate = success_rate + 1 - - if success_rate == len(design_networks): - action_result = hd_fields.ActionResult.Success - elif success_rate == - (len(design_networks)): - action_result = hd_fields.ActionResult.Failure - else: - action_result = hd_fields.ActionResult.PartialSuccess - - self.orchestrator.task_field_update(self.task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=action_result, - result_detail=result_detail) \ No newline at end of file diff --git a/helm_drydock/ingester/__init__.py b/helm_drydock/ingester/__init__.py deleted file mode 100644 index b2b69072..00000000 --- a/helm_drydock/ingester/__init__.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright 2017 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. -# -# ingester - Ingest host topologies to define site design and -# persist design to helm-drydock's statemgmt service - -import logging -import yaml -import uuid - -import helm_drydock.objects as objects -import helm_drydock.objects.site as site -import helm_drydock.objects.network as network -import helm_drydock.objects.hwprofile as hwprofile -import helm_drydock.objects.node as node -import helm_drydock.objects.hostprofile as hostprofile - -from helm_drydock.statemgmt import DesignState - -class Ingester(object): - - def __init__(self): - logging.basicConfig(format="%(asctime)-15s [%(levelname)] %(module)s %(process)d %(message)s") - self.log = logging.Logger("ingester") - self.registered_plugins = {} - - def enable_plugins(self, plugins=[]): - if len(plugins) == 0: - self.log.error("Cannot have an empty plugin list.") - - for plugin in plugins: - try: - new_plugin = plugin() - plugin_name = new_plugin.get_name() - self.registered_plugins[plugin_name] = new_plugin - except: - self.log.error("Could not enable plugin %s" % (plugin.__name__)) - - if len(self.registered_plugins) == 0: - self.log.error("Could not enable at least one plugin") - raise Exception("Could not enable at least one plugin") - """ - enable_plugins - - params: plugins - A list of class objects denoting the ingester plugins to be enabled - - Enable plugins that can be used for ingest_data calls. Each plugin should use - helm_drydock.ingester.plugins.IngesterPlugin as its base class. As long as one - enabled plugin successfully initializes, the call is considered successful. Otherwise - it will throw an exception - """ - - def ingest_data(self, plugin_name='', design_state=None, **kwargs): - if design_state is None: - self.log.error("ingest_data called without valid DesignState handler") - raise Exception("Invalid design_state handler") - - design_data = None - - # If no design_id is specified, instantiate a new one - if 'design_id' not in kwargs.keys(): - design_id = str(uuid.uuid4()) - design_data = objects.SiteDesign(id=design_id) - design_state.post_design(design_data) - else: - design_id = kwargs.get('design_id') - design_data = design_state.get_design(design_id) - - if plugin_name in self.registered_plugins: - design_items = self.registered_plugins[plugin_name].ingest_data(**kwargs) - for m in design_items: - if type(m) is site.Site: - design_data.set_site(m) - elif type(m) is network.Network: - design_data.add_network(m) - elif type(m) is network.NetworkLink: - design_data.add_network_link(m) - elif type(m) is hostprofile.HostProfile: - design_data.add_host_profile(m) - elif type(m) is hwprofile.HardwareProfile: - design_data.add_hardware_profile(m) - elif type(m) is node.BaremetalNode: - design_data.add_baremetal_node(m) - design_state.put_design(design_data) - else: - self.log.error("Could not find plugin %s to ingest data." % (plugin_name)) - raise LookupError("Could not find plugin %s" % plugin_name) - """ - ingest_data - - params: plugin_name - Which plugin should be used for ingestion - params: params - A map of parameters that will be passed to the plugin's ingest_data method - - Execute a data ingestion using the named plugin (assuming it is enabled) - """ - diff --git a/setup.py b/setup.py index 01bbff5f..b090c19c 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# helm_drydock - A tool to consume a host topology and orchestrate +# drydock_provisioner - A tool to consume a host topology and orchestrate # and monitor the provisioning of those hosts and execution of bootstrap # scripts # @@ -32,26 +32,27 @@ from setuptools import setup -setup(name='helm_drydock', +setup(name='drydock_provisioner', version='0.1a1', description='Bootstrapper for Kubernetes infrastructure', url='http://github.com/att-comdev/drydock', author='Scott Hussey - AT&T', author_email='sh8121@att.com', license='Apache 2.0', - packages=['helm_drydock', - 'helm_drydock.objects', - 'helm_drydock.ingester', - 'helm_drydock.ingester.plugins', - 'helm_drydock.statemgmt', - 'helm_drydock.orchestrator', - 'helm_drydock.control', - 'helm_drydock.drivers', - 'helm_drydock.drivers.oob', - 'helm_drydock.drivers.oob.pyghmi_driver', - 'helm_drydock.drivers.node', - 'helm_drydock.drivers.node.maasdriver', - 'helm_drydock.drivers.node.maasdriver.models'], + packages=['drydock_provisioner', + 'drydock_provisioner.objects', + 'drydock_provisioner.ingester', + 'drydock_provisioner.ingester.plugins', + 'drydock_provisioner.statemgmt', + 'drydock_provisioner.orchestrator', + 'drydock_provisioner.control', + 'drydock_provisioner.drivers', + 'drydock_provisioner.drivers.oob', + 'drydock_provisioner.drivers.oob.pyghmi_driver', + 'drydock_provisioner.drivers.node', + 'drydock_provisioner.drivers.node.maasdriver', + 'drydock_provisioner.drivers.node.maasdriver.models', + 'drydock_provisioner.control'], install_requires=[ 'PyYAML', 'pyghmi>=1.0.18', @@ -60,6 +61,8 @@ setup(name='helm_drydock', 'oslo.versionedobjects>=1.23.0', 'requests', 'oauthlib', + 'uwsgi>1.4', + 'bson===0.4.7' ] ) diff --git a/tests/integration/test_maasdriver_client.py b/tests/integration/test_maasdriver_client.py index 88b86e95..3c174018 100644 --- a/tests/integration/test_maasdriver_client.py +++ b/tests/integration/test_maasdriver_client.py @@ -13,8 +13,8 @@ # limitations under the License. import json -import helm_drydock.config as config -import helm_drydock.drivers.node.maasdriver.api_client as client +import drydock_provisioner.config as config +import drydock_provisioner.drivers.node.maasdriver.api_client as client class TestClass(object): diff --git a/tests/integration/test_maasdriver_network.py b/tests/integration/test_maasdriver_network.py index 36c8b324..ba5f5114 100644 --- a/tests/integration/test_maasdriver_network.py +++ b/tests/integration/test_maasdriver_network.py @@ -14,10 +14,10 @@ import json import uuid -import helm_drydock.config as config -import helm_drydock.drivers.node.maasdriver.api_client as client -import helm_drydock.drivers.node.maasdriver.models.fabric as maas_fabric -import helm_drydock.drivers.node.maasdriver.models.subnet as maas_subnet +import drydock_provisioner.config as config +import drydock_provisioner.drivers.node.maasdriver.api_client as client +import drydock_provisioner.drivers.node.maasdriver.models.fabric as maas_fabric +import drydock_provisioner.drivers.node.maasdriver.models.subnet as maas_subnet class TestClass(object): diff --git a/tests/integration/test_orch_node_networks.py b/tests/integration/test_orch_node_networks.py index 85619a30..15c2f811 100644 --- a/tests/integration/test_orch_node_networks.py +++ b/tests/integration/test_orch_node_networks.py @@ -17,23 +17,23 @@ import shutil import os import uuid -import helm_drydock.config as config -import helm_drydock.drivers.node.maasdriver.api_client as client -import helm_drydock.ingester.plugins.yaml -import helm_drydock.statemgmt as statemgmt -import helm_drydock.objects as objects -import helm_drydock.orchestrator as orch -import helm_drydock.objects.fields as hd_fields -import helm_drydock.objects.task as task -import helm_drydock.drivers as drivers -from helm_drydock.ingester import Ingester +import drydock_provisioner.config as config +import drydock_provisioner.drivers.node.maasdriver.api_client as client +import drydock_provisioner.ingester.plugins.yaml +import drydock_provisioner.statemgmt as statemgmt +import drydock_provisioner.objects as objects +import drydock_provisioner.orchestrator as orch +import drydock_provisioner.objects.fields as hd_fields +import drydock_provisioner.objects.task as task +import drydock_provisioner.drivers as drivers +from drydock_provisioner.ingester import Ingester class TestClass(object): def test_client_verify(self): design_state = statemgmt.DesignState() orchestrator = orch.Orchestrator(state_manager=design_state, - enabled_drivers={'node': 'helm_drydock.drivers.node.maasdriver.driver.MaasNodeDriver'}) + enabled_drivers={'node': 'drydock_provisioner.drivers.node.maasdriver.driver.MaasNodeDriver'}) orch_task = orchestrator.create_task(task.OrchestratorTask, site='sitename', @@ -57,14 +57,14 @@ class TestClass(object): design_state.post_design(design_data) ingester = Ingester() - ingester.enable_plugins([helm_drydock.ingester.plugins.yaml.YamlIngester]) + ingester.enable_plugins([drydock_provisioner.ingester.plugins.yaml.YamlIngester]) ingester.ingest_data(plugin_name='yaml', design_state=design_state, filenames=[str(input_file)], design_id=design_id) design_data = design_state.get_design(design_id) orchestrator = orch.Orchestrator(state_manager=design_state, - enabled_drivers={'node': 'helm_drydock.drivers.node.maasdriver.driver.MaasNodeDriver'}) + enabled_drivers={'node': 'drydock_provisioner.drivers.node.maasdriver.driver.MaasNodeDriver'}) orch_task = orchestrator.create_task(task.OrchestratorTask, site='sitename', diff --git a/tests/unit/test_design_inheritance.py b/tests/unit/test_design_inheritance.py index e5c57ce8..dad63a0f 100644 --- a/tests/unit/test_design_inheritance.py +++ b/tests/unit/test_design_inheritance.py @@ -12,16 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from helm_drydock.ingester import Ingester -from helm_drydock.statemgmt import DesignState -from helm_drydock.orchestrator import Orchestrator +from drydock_provisioner.ingester import Ingester +from drydock_provisioner.statemgmt import DesignState +from drydock_provisioner.orchestrator import Orchestrator from copy import deepcopy import pytest import shutil import os -import helm_drydock.ingester.plugins.yaml +import drydock_provisioner.ingester.plugins.yaml import yaml class TestClass(object): @@ -32,7 +32,7 @@ class TestClass(object): def test_design_inheritance(self, loaded_design): orchestrator = Orchestrator(state_manager=loaded_design, - enabled_drivers={'oob': 'helm_drydock.drivers.oob.pyghmi_driver.PyghmiDriver'}) + enabled_drivers={'oob': 'drydock_provisioner.drivers.oob.pyghmi_driver.PyghmiDriver'}) design_data = orchestrator.load_design_data("sitename") @@ -63,7 +63,7 @@ class TestClass(object): design_state.post_design_base(design_data) ingester = Ingester() - ingester.enable_plugins([helm_drydock.ingester.plugins.yaml.YamlIngester]) + ingester.enable_plugins([drydock_provisioner.ingester.plugins.yaml.YamlIngester]) ingester.ingest_data(plugin_name='yaml', design_state=design_state, filenames=[str(input_file)]) return design_state diff --git a/tests/unit/test_ingester.py b/tests/unit/test_ingester.py index a719ad6c..9b083322 100644 --- a/tests/unit/test_ingester.py +++ b/tests/unit/test_ingester.py @@ -12,14 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from helm_drydock.ingester import Ingester -from helm_drydock.statemgmt import DesignState -import helm_drydock.objects as objects +from drydock_provisioner.ingester import Ingester +from drydock_provisioner.statemgmt import DesignState +import drydock_provisioner.objects as objects import pytest import shutil import os -import helm_drydock.ingester.plugins.yaml +import drydock_provisioner.ingester.plugins.yaml class TestClass(object): @@ -37,7 +37,7 @@ class TestClass(object): design_state.post_design(design_data) ingester = Ingester() - ingester.enable_plugins([helm_drydock.ingester.plugins.yaml.YamlIngester]) + ingester.enable_plugins([drydock_provisioner.ingester.plugins.yaml.YamlIngester]) ingester.ingest_data(plugin_name='yaml', design_state=design_state, filenames=[str(input_file)], design_id=design_id) @@ -59,7 +59,7 @@ class TestClass(object): design_state.post_design(design_data) ingester = Ingester() - ingester.enable_plugins([helm_drydock.ingester.plugins.yaml.YamlIngester]) + ingester.enable_plugins([drydock_provisioner.ingester.plugins.yaml.YamlIngester]) ingester.ingest_data(plugin_name='yaml', design_state=design_state, design_id=design_id, filenames=[str(profiles_file), str(networks_file), str(nodes_file)]) diff --git a/tests/unit/test_ingester_yaml.py b/tests/unit/test_ingester_yaml.py index 3be5db08..c078401f 100644 --- a/tests/unit/test_ingester_yaml.py +++ b/tests/unit/test_ingester_yaml.py @@ -16,7 +16,7 @@ import shutil import os import uuid -from helm_drydock.ingester.plugins.yaml import YamlIngester +from drydock_provisioner.ingester.plugins.yaml import YamlIngester class TestClass(object): diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index e5014444..25c5128a 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -14,8 +14,8 @@ import pytest -import helm_drydock.objects as objects -from helm_drydock.objects import fields +import drydock_provisioner.objects as objects +from drydock_provisioner.objects import fields class TestClass(object): @@ -23,7 +23,7 @@ class TestClass(object): objects.register_all() model_attr = { - 'versioned_object.namespace': 'helm_drydock.objects', + 'versioned_object.namespace': 'drydock_provisioner.objects', 'versioned_object.name': 'HardwareProfile', 'versioned_object.version': '1.0', 'versioned_object.data': { @@ -38,13 +38,13 @@ class TestClass(object): 'bootstrap_protocol': 'pxe', 'pxe_interface': '0', 'devices': { - 'versioned_object.namespace': 'helm_drydock.objects', + 'versioned_object.namespace': 'drydock_provisioner.objects', 'versioned_object.name': 'HardwareDeviceAliasList', 'versioned_object.version': '1.0', 'versioned_object.data': { 'objects': [ { - 'versioned_object.namespace': 'helm_drydock.objects', + 'versioned_object.namespace': 'drydock_provisioner.objects', 'versioned_object.name': 'HardwareDeviceAlias', 'versioned_object.version': '1.0', 'versioned_object.data': { @@ -56,7 +56,7 @@ class TestClass(object): } }, { - 'versioned_object.namespace': 'helm_drydock.objects', + 'versioned_object.namespace': 'drydock_provisioner.objects', 'versioned_object.name': 'HardwareDeviceAlias', 'versioned_object.version': '1.0', 'versioned_object.data': { diff --git a/tests/unit/test_orch_generic.py b/tests/unit/test_orch_generic.py index 86739965..82940686 100644 --- a/tests/unit/test_orch_generic.py +++ b/tests/unit/test_orch_generic.py @@ -18,11 +18,11 @@ import threading import time -import helm_drydock.orchestrator as orch -import helm_drydock.objects.fields as hd_fields -import helm_drydock.statemgmt as statemgmt -import helm_drydock.objects.task as task -import helm_drydock.drivers as drivers +import drydock_provisioner.orchestrator as orch +import drydock_provisioner.objects.fields as hd_fields +import drydock_provisioner.statemgmt as statemgmt +import drydock_provisioner.objects.task as task +import drydock_provisioner.drivers as drivers class TestClass(object): diff --git a/tests/unit/test_orch_oob.py b/tests/unit/test_orch_oob.py index 6c10d8f1..b510b935 100644 --- a/tests/unit/test_orch_oob.py +++ b/tests/unit/test_orch_oob.py @@ -23,15 +23,15 @@ import os import shutil import uuid -from helm_drydock.ingester import Ingester +from drydock_provisioner.ingester import Ingester -import helm_drydock.orchestrator as orch -import helm_drydock.objects.fields as hd_fields -import helm_drydock.statemgmt as statemgmt -import helm_drydock.objects as objects -import helm_drydock.objects.task as task -import helm_drydock.drivers as drivers -import helm_drydock.ingester.plugins.yaml as yaml_ingester +import drydock_provisioner.orchestrator as orch +import drydock_provisioner.objects.fields as hd_fields +import drydock_provisioner.statemgmt as statemgmt +import drydock_provisioner.objects as objects +import drydock_provisioner.objects.task as task +import drydock_provisioner.drivers as drivers +import drydock_provisioner.ingester.plugins.yaml as yaml_ingester class TestClass(object): @@ -44,7 +44,7 @@ class TestClass(object): #mocker.patch.object('pyghmi.ipmi.command.Command','get_asset_tag') orchestrator = orch.Orchestrator(state_manager=loaded_design, - enabled_drivers={'oob': 'helm_drydock.drivers.oob.pyghmi_driver.PyghmiDriver'}) + enabled_drivers={'oob': 'drydock_provisioner.drivers.oob.pyghmi_driver.PyghmiDriver'}) orch_task = orchestrator.create_task(task.OrchestratorTask, site='sitename', @@ -63,7 +63,7 @@ class TestClass(object): #mocker.patch.object('pyghmi.ipmi.command.Command','set_bootdev') orchestrator = orch.Orchestrator(state_manager=loaded_design, - enabled_drivers={'oob': 'helm_drydock.drivers.oob.pyghmi_driver.PyghmiDriver'}) + enabled_drivers={'oob': 'drydock_provisioner.drivers.oob.pyghmi_driver.PyghmiDriver'}) orch_task = orchestrator.create_task(task.OrchestratorTask, site='sitename', diff --git a/tests/unit/test_statemgmt.py b/tests/unit/test_statemgmt.py index edb82764..17c90899 100644 --- a/tests/unit/test_statemgmt.py +++ b/tests/unit/test_statemgmt.py @@ -15,8 +15,8 @@ import pytest import shutil -import helm_drydock.objects as objects -import helm_drydock.statemgmt as statemgmt +import drydock_provisioner.objects as objects +import drydock_provisioner.statemgmt as statemgmt class TestClass(object): From dac6cfba16a0021362ae96638dcdb49dc57ebd1b Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Thu, 22 Jun 2017 20:43:38 -0500 Subject: [PATCH 02/16] Revert "Sh8121att initial api service" --- README.md | 2 +- drydock_provisioner/config.py | 6 +- .../node/maasdriver/models/__init__.py | 2 +- .../drivers/oob/pyghmi_driver/__init__.py | 2 +- helm_drydock/control/api.py | 51 --- helm_drydock/control/base.py | 144 -------- helm_drydock/control/designs.py | 164 --------- helm_drydock/control/middleware.py | 92 ----- helm_drydock/control/readme.md | 30 -- helm_drydock/control/tasks.py | 79 ----- .../drivers/node/maasdriver/driver.py | 317 ------------------ helm_drydock/drydock.py | 55 --- helm_drydock/ingester/__init__.py | 116 ------- 13 files changed, 6 insertions(+), 1054 deletions(-) delete mode 100644 helm_drydock/control/api.py delete mode 100644 helm_drydock/control/base.py delete mode 100644 helm_drydock/control/designs.py delete mode 100644 helm_drydock/control/middleware.py delete mode 100644 helm_drydock/control/readme.md delete mode 100644 helm_drydock/control/tasks.py delete mode 100644 helm_drydock/drivers/node/maasdriver/driver.py delete mode 100644 helm_drydock/drydock.py delete mode 100644 helm_drydock/ingester/__init__.py diff --git a/README.md b/README.md index 3918f79f..8e2f0347 100644 --- a/README.md +++ b/README.md @@ -56,4 +56,4 @@ Pluggable provisioner for server bootstrapping. Initial implementation is MaaS c aka introspection API for bootstrapping nodes to load self data. Possibly pluggable as this is basically an -authenticated bridge to the Design State API +authenticated bridge to the Design State API \ No newline at end of file diff --git a/drydock_provisioner/config.py b/drydock_provisioner/config.py index d035d29e..f64e91e9 100644 --- a/drydock_provisioner/config.py +++ b/drydock_provisioner/config.py @@ -27,8 +27,8 @@ class DrydockConfig(object): node_driver = { 'maasdriver': { - 'api_key': 'KTMHgA42cNSMnfmJ82:cdg4yQUhp542aHsCTV:7Dc2KB9hQpWq3LfQAAAKAj6wdg22yWxZ', - 'api_url': 'http://localhost:5240/MAAS/api/2.0/', + 'api_key': 'UTBfxGL69XWjaffQek:NuKZSYGuBs6ZpYC6B9:byvXBgY8CsW5VQKxGdQjvJXtjXwr5G4U', + 'api_url': 'http://10.23.19.16:30773/MAAS/api/2.0/', }, } @@ -41,4 +41,4 @@ class DrydockConfig(object): 'oob': 'drydock_provisioner.drivers.oob.pyghmi_driver.PyghmiDriver', 'node': 'drydock_provisioner.drivers.node.maasdriver.driver.MaasNodeDriver', } - } + } \ No newline at end of file diff --git a/drydock_provisioner/drivers/node/maasdriver/models/__init__.py b/drydock_provisioner/drivers/node/maasdriver/models/__init__.py index f10bbbf6..2a385a45 100644 --- a/drydock_provisioner/drivers/node/maasdriver/models/__init__.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/__init__.py @@ -10,4 +10,4 @@ # 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. +# limitations under the License. \ No newline at end of file diff --git a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py index 2a218659..73c2bd5c 100644 --- a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py +++ b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py @@ -300,4 +300,4 @@ class PyghmiTaskRunner(drivers.DriverTaskRunner): result=hd_fields.ActionResult.Success, status=hd_fields.TaskStatus.Complete, result_detail=mci_id) - return + return \ No newline at end of file diff --git a/helm_drydock/control/api.py b/helm_drydock/control/api.py deleted file mode 100644 index a7134693..00000000 --- a/helm_drydock/control/api.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2017 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. -import falcon - -from .designs import * -from .tasks import * - -from .base import DrydockRequest -from .middleware import AuthMiddleware, ContextMiddleware, LoggingMiddleware - -def start_api(state_manager=None, ingester=None, orchestrator=None): - """ - Start the Drydock API service - - :param state_manager: Instance of helm_drydock.statemgmt.manager.DesignState for accessing - state persistence - :param ingester: Instance of helm_drydock.ingester.ingester.Ingester for handling design - part input - """ - control_api = falcon.API(request_type=DrydockRequest, - middleware=[AuthMiddleware(), ContextMiddleware(), LoggingMiddleware()]) - - # v1.0 of Drydock API - v1_0_routes = [ - # API for managing orchestrator tasks - ('/tasks', TasksResource(state_manager=state_manager, orchestrator=orchestrator)), - ('/tasks/{task_id}', TaskResource(state_manager=state_manager)), - - # API for managing site design data - ('/designs', DesignsResource(state_manager=state_manager)), - ('/designs/{design_id}', DesignResource(state_manager=state_manager, orchestrator=orchestrator)), - ('/designs/{design_id}/parts', DesignsPartsResource(state_manager=state_manager, ingester=ingester)), - ('/designs/{design_id}/parts/{kind}', DesignsPartsKindsResource(state_manager=state_manager)), - ('/designs/{design_id}/parts/{kind}/{name}', DesignsPartResource(state_manager=state_manager, orchestrator=orchestrator)) - ] - - for path, res in v1_0_routes: - control_api.add_route('/api/v1.0' + path, res) - - return control_api diff --git a/helm_drydock/control/base.py b/helm_drydock/control/base.py deleted file mode 100644 index 2337ef42..00000000 --- a/helm_drydock/control/base.py +++ /dev/null @@ -1,144 +0,0 @@ -# Copyright 2017 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. -import falcon.request as request -import uuid -import json -import logging - -import helm_drydock.error as errors - -class BaseResource(object): - - def __init__(self): - self.logger = logging.getLogger('control') - self.authorized_roles = [] - - def on_options(self, req, resp): - self_attrs = dir(self) - methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH'] - allowed_methods = [] - - for m in methods: - if 'on_' + m.lower() in self_attrs: - allowed_methods.append(m) - - resp.headers['Allow'] = ','.join(allowed_methods) - resp.status = falcon.HTTP_200 - - # For authorizing access at the Resource level. A Resource requiring - # finer grained authorization at the method or instance level must - # implement that in the request handlers - def authorize_roles(self, role_list): - authorized = set(self.authorized_roles) - applied = set(role_list) - - if authorized.isdisjoint(applied): - return False - else: - return True - - def req_json(self, req): - if req.content_length is None or req.content_length == 0: - return None - - if req.content_type is not None and req.content_type.lower() == 'application/json': - raw_body = req.stream.read(req.content_length or 0) - - if raw_body is None: - return None - - try: - json_body = json.loads(raw_body.decode('utf-8')) - return json_body - except json.JSONDecodeError as jex: - raise errors.InvalidFormat("%s: Invalid JSON in body: %s" % (req.path, jex)) - else: - raise errors.InvalidFormat("Requires application/json payload") - - def return_error(self, resp, status_code, message="", retry=False): - resp.body = json.dumps({'type': 'error', 'message': message, 'retry': retry}) - resp.status = status_code - - def log_error(self, ctx, level, msg): - extra = { - 'user': 'N/A', - 'req_id': 'N/A', - 'external_ctx': 'N/A' - } - - if ctx is not None: - extra = { - 'user': ctx.user, - 'req_id': ctx.request_id, - 'external_ctx': ctx.external_marker, - } - - self.logger.log(level, msg, extra=extra) - - def debug(self, ctx, msg): - self.log_error(ctx, logging.DEBUG, msg) - - def info(self, ctx, msg): - self.log_error(ctx, logging.INFO, msg) - - def warn(self, ctx, msg): - self.log_error(ctx, logging.WARN, msg) - - def error(self, ctx, msg): - self.log_error(ctx, logging.ERROR, msg) - - -class StatefulResource(BaseResource): - - def __init__(self, state_manager=None): - super(StatefulResource, self).__init__() - - if state_manager is None: - self.error(None, "StatefulResource:init - StatefulResources require a state manager be set") - raise ValueError("StatefulResources require a state manager be set") - - self.state_manager = state_manager - - -class DrydockRequestContext(object): - - def __init__(self): - self.log_level = 'ERROR' - self.user = None - self.roles = ['anyone'] - self.req_id = str(uuid.uuid4()) - self.external_marker = None - - def set_log_level(self, level): - if level in ['error', 'info', 'debug']: - self.log_level = level - - def set_user(self, user): - self.user = user - - def add_role(self, role): - self.roles.append(role) - - def add_roles(self, roles): - self.roles.extend(roles) - - def remove_role(self, role): - self.roles = [x for x in self.roles - if x != role] - - def set_external_marker(self, marker): - self.external_marker = str(marker)[:20] - -class DrydockRequest(request.Request): - context_type = DrydockRequestContext \ No newline at end of file diff --git a/helm_drydock/control/designs.py b/helm_drydock/control/designs.py deleted file mode 100644 index 402b23ad..00000000 --- a/helm_drydock/control/designs.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright 2017 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. -import falcon -import json -import uuid -import logging - -import helm_drydock.objects as hd_objects -import helm_drydock.error as errors - -from .base import StatefulResource - -class DesignsResource(StatefulResource): - - def __init__(self, **kwargs): - super(DesignsResource, self).__init__(**kwargs) - self.authorized_roles = ['user'] - - def on_get(self, req, resp): - state = self.state_manager - - designs = list(state.designs.keys()) - - resp.body = json.dumps(designs) - resp.status = falcon.HTTP_200 - - def on_post(self, req, resp): - try: - json_data = self.req_json(req) - design = None - if json_data is not None: - base_design = json_data.get('base_design_id', None) - - if base_design is not None: - base_design = uuid.UUID(base_design) - design = hd_objects.SiteDesign(base_design_id=base_design_uuid) - else: - design = hd_objects.SiteDesign() - design.assign_id() - design.create(req.context, self.state_manager) - - resp.body = json.dumps(design.obj_to_simple()) - resp.status = falcon.HTTP_201 - except errors.StateError as stex: - self.error(req.context, "Error updating persistence") - self.return_error(resp, falcon.HTTP_500, message="Error updating persistence", retry=True) - except errors.InvalidFormat as fex: - self.error(req.context, str(fex)) - self.return_error(resp, falcon.HTTP_400, message=str(fex), retry=False) - - -class DesignResource(StatefulResource): - - def __init__(self, orchestrator=None, **kwargs): - super(DesignResource, self).__init__(**kwargs) - self.authorized_roles = ['user'] - self.orchestrator = orchestrator - - def on_get(self, req, resp, design_id): - source = req.params.get('source', 'designed') - - try: - design = None - if source == 'compiled': - design = self.orchestrator.get_effective_site(design_id) - elif source == 'designed': - design = self.orchestrator.get_described_site(design_id) - - resp.body = json.dumps(design.obj_to_simple()) - except errors.DesignError: - self.error(req.context, "Design %s not found" % design_id) - self.return_error(resp, falcon.HTTP_404, message="Design %s not found" % design_id, retry=False) - -class DesignsPartsResource(StatefulResource): - - def __init__(self, ingester=None, **kwargs): - super(DesignsPartsResource, self).__init__(**kwargs) - self.ingester = ingester - self.authorized_roles = ['user'] - - if ingester is None: - self.error(None, "DesignsPartsResource requires a configured Ingester instance") - raise ValueError("DesignsPartsResource requires a configured Ingester instance") - - def on_post(self, req, resp, design_id): - ingester_name = req.params.get('ingester', None) - - if ingester_name is None: - self.error(None, "DesignsPartsResource POST requires parameter 'ingester'") - self.return_error(resp, falcon.HTTP_400, message="POST requires parameter 'ingester'", retry=False) - else: - try: - raw_body = req.stream.read(req.content_length or 0) - if raw_body is not None and len(raw_body) > 0: - parsed_items = self.ingester.ingest_data(plugin_name=ingester_name, design_state=self.state_manager, - content=raw_body, design_id=design_id, context=req.context) - resp.status = falcon.HTTP_201 - resp.body = json.dumps([x.obj_to_simple() for x in parsed_items]) - else: - self.return_error(resp, falcon.HTTP_400, message="Empty body not supported", retry=False) - except ValueError: - self.return_error(resp, falcon.HTTP_500, message="Error processing input", retry=False) - except LookupError: - self.return_error(resp, falcon.HTTP_400, message="Ingester %s not registered" % ingester_name, retry=False) - - -class DesignsPartsKindsResource(StatefulResource): - def __init__(self, **kwargs): - super(DesignsPartsKindsResource, self).__init__(**kwargs) - self.authorized_roles = ['user'] - - def on_get(self, req, resp, design_id, kind): - pass - -class DesignsPartResource(StatefulResource): - - def __init__(self, orchestrator=None, **kwargs): - super(DesignsPartResource, self).__init__(**kwargs) - self.authorized_roles = ['user'] - self.orchestrator = orchestrator - - def on_get(self, req , resp, design_id, kind, name): - source = req.params.get('source', 'designed') - - try: - design = None - if source == 'compiled': - design = self.orchestrator.get_effective_site(design_id) - elif source == 'designed': - design = self.orchestrator.get_described_site(design_id) - - part = None - if kind == 'Site': - part = design.get_site() - elif kind == 'Network': - part = design.get_network(name) - elif kind == 'NetworkLink': - part = design.get_network_link(name) - elif kind == 'HardwareProfile': - part = design.get_hardware_profile(name) - elif kind == 'HostProfile': - part = design.get_host_profile(name) - elif kind == 'BaremetalNode': - part = design.get_baremetal_node(name) - else: - self.error(req.context, "Kind %s unknown" % kind) - self.return_error(resp, falcon.HTTP_404, message="Kind %s unknown" % kind, retry=False) - return - - resp.body = json.dumps(part.obj_to_simple()) - except errors.DesignError as dex: - self.error(req.context, str(dex)) - self.return_error(resp, falcon.HTTP_404, message=str(dex), retry=False) \ No newline at end of file diff --git a/helm_drydock/control/middleware.py b/helm_drydock/control/middleware.py deleted file mode 100644 index 157a677e..00000000 --- a/helm_drydock/control/middleware.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright 2017 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. - -import falcon -import logging -import uuid - -import helm_drydock.config as config - -class AuthMiddleware(object): - - # Authentication - def process_request(self, req, resp): - ctx = req.context - token = req.get_header('X-Auth-Token') - - user = self.validate_token(token) - - if user is not None: - ctx.set_user(user) - user_roles = self.role_list(user) - ctx.add_roles(user_roles) - else: - ctx.add_role('anyone') - - # Authorization - def process_resource(self, req, resp, resource, params): - ctx = req.context - - if not resource.authorize_roles(ctx.roles): - raise falcon.HTTPUnauthorized('Authentication required', - ('This resource requires an authorized role.')) - - # Return the username associated with an authenticated token or None - def validate_token(self, token): - if token == '42': - return 'scott' - elif token == 'bigboss': - return 'admin' - else: - return None - - # Return the list of roles assigned to the username - # Roles need to be an enum - def role_list(self, username): - if username == 'scott': - return ['user'] - elif username == 'admin': - return ['user', 'admin'] - -class ContextMiddleware(object): - - def process_request(self, req, resp): - ctx = req.context - - requested_logging = req.get_header('X-Log-Level') - - if (config.DrydockConfig.global_config.get('log_level', '') == 'DEBUG' or - (requested_logging == 'DEBUG' and 'admin' in ctx.roles)): - ctx.set_log_level('DEBUG') - elif requested_logging == 'INFO': - ctx.set_log_level('INFO') - - ext_marker = req.get_header('X-Context-Marker') - - ctx.set_external_marker(ext_marker if ext_marker is not None else '') - -class LoggingMiddleware(object): - - def __init__(self): - self.logger = logging.getLogger('drydock.control') - - def process_response(self, req, resp, resource, req_succeeded): - ctx = req.context - extra = { - 'user': ctx.user, - 'req_id': ctx.req_id, - 'external_ctx': ctx.external_marker, - } - resp.append_header('X-Drydock-Req', ctx.req_id) - self.logger.info("%s - %s" % (req.uri, resp.status), extra=extra) diff --git a/helm_drydock/control/readme.md b/helm_drydock/control/readme.md deleted file mode 100644 index d09f2d0e..00000000 --- a/helm_drydock/control/readme.md +++ /dev/null @@ -1,30 +0,0 @@ -# Control # - -This is the external facing API service to control the rest -of Drydock and query Drydock-managed data. - -## v1.0 Endpoints ## - -### /api/v1.0/tasks ### - -POST - Create a new orchestration task and submit it for execution -GET - Get status of a task -DELETE - Cancel execution of a task if permitted - -### /api/v1.0/designs ### - -POST - Create a new site design so design parts can be added - -### /api/v1.0/designs/{id} - -GET - Get a current design if available. Param 'source=compiled' to calculate the inheritance chain and compile the effective design. - -### /api/v1.0/designs/{id}/parts - -POST - Submit a new design part to be ingested and added to this design -GET - View a currently defined design part -PUT - Replace an existing design part *Not Implemented* - -### /api/v1.0/designs/{id}/parts/{kind}/{name} - -GET - View a single design part. param 'source=compiled' to calculate the inheritance chain and compile the effective configuration for the design part. \ No newline at end of file diff --git a/helm_drydock/control/tasks.py b/helm_drydock/control/tasks.py deleted file mode 100644 index bf35861f..00000000 --- a/helm_drydock/control/tasks.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2017 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. -import falcon -import json -import threading -import traceback - -import helm_drydock.objects.task as obj_task -from .base import StatefulResource - -class TasksResource(StatefulResource): - - def __init__(self, orchestrator=None, **kwargs): - super(TasksResource, self).__init__(**kwargs) - self.authorized_roles = ['user'] - self.orchestrator = orchestrator - - def on_get(self, req, resp): - task_id_list = [str(x.get_id()) for x in self.state_manager.tasks] - resp.body = json.dumps(task_id_list) - - def on_post(self, req, resp): - try: - json_data = self.req_json(req) - - sitename = json_data.get('sitename', None) - design_id = json_data.get('design_id', None) - action = json_data.get('action', None) - - if sitename is None or design_id is None or action is None: - self.info(req.context, "Task creation requires fields sitename, design_id, action") - self.return_error(resp, falcon.HTTP_400, message="Task creation requires fields sitename, design_id, action", retry=False) - return - - task = self.orchestrator.create_task(obj_task.OrchestratorTask, site=sitename, - design_id=design_id, action=action) - - task_thread = threading.Thread(target=self.orchestrator.execute_task, args=[task.get_id()]) - task_thread.start() - - resp.body = json.dumps(task.to_dict()) - resp.status = falcon.HTTP_201 - except Exception as ex: - self.error(req.context, "Unknown error: %s\n%s" % (str(ex), traceback.format_exc())) - self.return_error(resp, falcon.HTTP_500, message="Unknown error", retry=False) - - -class TaskResource(StatefulResource): - - def __init__(self, orchestrator=None, **kwargs): - super(TaskResource, self).__init__(**kwargs) - self.authorized_roles = ['user'] - self.orchestrator = orchestrator - - def on_get(self, req, resp, task_id): - try: - task = self.state_manager.get_task(task_id) - - if task is None: - self.info(req.context, "Task %s does not exist" % task_id ) - self.return_error(resp, falcon.HTTP_404, message="Task %s does not exist" % task_id, retry=False) - return - - resp.body = json.dumps(task.to_dict()) - resp.status = falcon.HTTP_200 - except Exception as ex: - self.error(req.context, "Unknown error: %s" % (str(ex))) - self.return_error(resp, falcon.HTTP_500, message="Unknown error", retry=False) diff --git a/helm_drydock/drivers/node/maasdriver/driver.py b/helm_drydock/drivers/node/maasdriver/driver.py deleted file mode 100644 index 32216169..00000000 --- a/helm_drydock/drivers/node/maasdriver/driver.py +++ /dev/null @@ -1,317 +0,0 @@ -# Copyright 2017 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. -import helm_drydock.error as errors -import helm_drydock.config as config -import helm_drydock.drivers as drivers -import helm_drydock.objects.fields as hd_fields -import helm_drydock.objects.task as task_model - -from helm_drydock.drivers.node import NodeDriver -from .api_client import MaasRequestFactory -import helm_drydock.drivers.node.maasdriver.models.fabric as maas_fabric -import helm_drydock.drivers.node.maasdriver.models.vlan as maas_vlan -import helm_drydock.drivers.node.maasdriver.models.subnet as maas_subnet - -class MaasNodeDriver(NodeDriver): - - def __init__(self, **kwargs): - super(MaasNodeDriver, self).__init__(**kwargs) - - self.driver_name = "maasdriver" - self.driver_key = "maasdriver" - self.driver_desc = "MaaS Node Provisioning Driver" - - self.config = config.DrydockConfig.node_driver[self.driver_key] - - def execute_task(self, task_id): - task = self.state_manager.get_task(task_id) - - if task is None: - raise errors.DriverError("Invalid task %s" % (task_id)) - - if task.action not in self.supported_actions: - raise errors.DriverError("Driver %s doesn't support task action %s" - % (self.driver_desc, task.action)) - - if task.action == hd_fields.OrchestratorAction.ValidateNodeServices: - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Running) - maas_client = MaasRequestFactory(self.config['api_url'], self.config['api_key']) - - try: - if maas_client.test_connectivity(): - if maas_client.test_authentication(): - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Success) - return - except errors.TransientDriverError(ex): - result = { - 'retry': True, - 'detail': str(ex), - } - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Failure, - result_details=result) - return - except errors.PersistentDriverError(ex): - result = { - 'retry': False, - 'detail': str(ex), - } - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Failure, - result_details=result) - return - except Exception(ex): - result = { - 'retry': False, - 'detail': str(ex), - } - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Failure, - result_details=result) - return - - design_id = getattr(task, 'design_id', None) - - if design_id is None: - raise errors.DriverError("No design ID specified in task %s" % - (task_id)) - - - if task.site_name is None: - raise errors.DriverError("No site specified for task %s." % - (task_id)) - - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Running) - - site_design = self.orchestrator.get_effective_site(design_id) - - if task.action == hd_fields.OrchestratorAction.CreateNetworkTemplate: - subtask = self.orchestrator.create_task(task_model.DriverTask, - parent_task_id=task.get_id(), design_id=design_id, - action=task.action, site_name=task.site_name, - task_scope={'site': task.site_name}) - runner = MaasTaskRunner(state_manager=self.state_manager, - orchestrator=self.orchestrator, - task_id=subtask.get_id(),config=self.config) - runner.start() - - runner.join(timeout=120) - - if runner.is_alive(): - result = { - 'retry': False, - 'detail': 'MaaS Network creation timed-out' - } - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=hd_fields.ActionResult.Failure, - result_detail=result) - else: - subtask = self.state_manager.get_task(subtask.get_id()) - self.orchestrator.task_field_update(task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=subtask.get_result()) - - return - -class MaasTaskRunner(drivers.DriverTaskRunner): - - def __init__(self, config=None, **kwargs): - super(MaasTaskRunner, self).__init__(**kwargs) - - self.driver_config = config - - def execute_task(self): - task_action = self.task.action - - self.orchestrator.task_field_update(self.task.get_id(), - status=hd_fields.TaskStatus.Running, - result=hd_fields.ActionResult.Incomplete) - - self.maas_client = MaasRequestFactory(self.driver_config['api_url'], - self.driver_config['api_key']) - - site_design = self.orchestrator.get_effective_site(self.task.design_id) - - if task_action == hd_fields.OrchestratorAction.CreateNetworkTemplate: - # Try to true up MaaS definitions of fabrics/vlans/subnets - # with the networks defined in Drydock - design_networks = site_design.networks - - subnets = maas_subnet.Subnets(self.maas_client) - subnets.refresh() - - result_detail = { - 'detail': [] - } - - for n in design_networks: - try: - subnet = subnets.singleton({'cidr': n.cidr}) - - if subnet is not None: - subnet.name = n.name - subnet.dns_servers = n.dns_servers - - vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=subnet.fabric) - vlan_list.refresh() - - vlan = vlan_list.select(subnet.vlan) - - if vlan is not None: - if ((n.vlan_id is None and vlan.vid != 0) or - (n.vlan_id is not None and vlan.vid != n.vlan_id)): - - # if the VLAN name matches, assume this is the correct resource - # and it needs to be updated - if vlan.name == n.name: - vlan.set_vid(n.vlan_id) - vlan.mtu = n.mtu - vlan.update() - result_detail['detail'].append("VLAN %s found for network %s, updated attributes" - % (vlan.resource_id, n.name)) - else: - # Found a VLAN with the correct VLAN tag, update subnet to use it - target_vlan = vlan_list.singleton({'vid': n.vlan_id if n.vlan_id is not None else 0}) - if target_vlan is not None: - subnet.vlan = target_vlan.resource_id - else: - # This is a flag that after creating a fabric and - # VLAN below, update the subnet - subnet.vlan = None - else: - subnet.vlan = None - - # Check if the routes have a default route - subnet.gateway_ip = n.get_default_gateway() - - - result_detail['detail'].append("Subnet %s found for network %s, updated attributes" - % (subnet.resource_id, n.name)) - - # Need to find or create a Fabric/Vlan for this subnet - if (subnet is None or (subnet is not None and subnet.vlan is None)): - fabric_list = maas_fabric.Fabrics(self.maas_client) - fabric_list.refresh() - fabric = fabric_list.singleton({'name': n.name}) - - vlan = None - - if fabric is not None: - vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=fabric.resource_id) - vlan_list.refresh() - - vlan = vlan_list.singleton({'vid': n.vlan_id if n.vlan_id is not None else 0}) - - if vlan is not None: - vlan = matching_vlans[0] - - vlan.name = n.name - if getattr(n, 'mtu', None) is not None: - vlan.mtu = n.mtu - - if subnet is not None: - subnet.vlan = vlan.resource_id - subnet.update() - - vlan.update() - result_detail['detail'].append("VLAN %s found for network %s, updated attributes" - % (vlan.resource_id, n.name)) - else: - # Create a new VLAN in this fabric and assign subnet to it - vlan = maas_vlan.Vlan(self.maas_client, name=n.name, vid=vlan_id, - mtu=getattr(n, 'mtu', None),fabric_id=fabric.resource_id) - vlan = vlan_list.add(vlan) - - result_detail['detail'].append("VLAN %s created for network %s" - % (vlan.resource_id, n.name)) - if subnet is not None: - subnet.vlan = vlan.resource_id - subnet.update() - - else: - # Create new fabric and VLAN - fabric = maas_fabric.Fabric(self.maas_client, name=n.name) - fabric = fabric_list.add(fabric) - fabric_list.refresh() - - result_detail['detail'].append("Fabric %s created for network %s" - % (fabric.resource_id, n.name)) - - vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=new_fabric.resource_id) - vlan_list.refresh() - - # A new fabric comes with a single default VLAN. Retrieve it and update attributes - vlan = vlan_list.single() - - vlan.name = n.name - vlan.vid = n.vlan_id if n.vlan_id is not None else 0 - if getattr(n, 'mtu', None) is not None: - vlan.mtu = n.mtu - - vlan.update() - result_detail['detail'].append("VLAN %s updated for network %s" - % (vlan.resource_id, n.name)) - if subnet is not None: - # If subnet was found above, but needed attached to a new fabric/vlan then - # attach it - subnet.vlan = vlan.resource_id - subnet.update() - - if subnet is None: - # If subnet did not exist, create it here and attach it to the fabric/VLAN - subnet = maas_subnet.Subnet(self.maas_client, name=n.name, cidr=n.cidr, fabric=fabric.resource_id, - vlan=vlan.resource_id, gateway_ip=n.get_default_gateway()) - - subnet_list = maas_subnet.Subnets(self.maas_client) - subnet = subnet_list.add(subnet) - except ValueError as vex: - raise errors.DriverError("Inconsistent data from MaaS") - - subnet_list = maas_subnet.Subnets(self.maas_client) - subnet_list.refresh() - - action_result = hd_fields.ActionResult.Incomplete - - success_rate = 0 - - for n in design_networks: - exists = subnet_list.query({'cidr': n.cidr}) - if len(exists) > 0: - subnet = exists[0] - if subnet.name == n.name: - success_rate = success_rate + 1 - else: - success_rate = success_rate + 1 - else: - success_rate = success_rate + 1 - - if success_rate == len(design_networks): - action_result = hd_fields.ActionResult.Success - elif success_rate == - (len(design_networks)): - action_result = hd_fields.ActionResult.Failure - else: - action_result = hd_fields.ActionResult.PartialSuccess - - self.orchestrator.task_field_update(self.task.get_id(), - status=hd_fields.TaskStatus.Complete, - result=action_result, - result_detail=result_detail) \ No newline at end of file diff --git a/helm_drydock/drydock.py b/helm_drydock/drydock.py deleted file mode 100644 index 4be2340e..00000000 --- a/helm_drydock/drydock.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2017 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. -import logging - -import helm_drydock.config as config -import helm_drydock.objects as objects -import helm_drydock.ingester as ingester -import helm_drydock.statemgmt as statemgmt -import helm_drydock.orchestrator as orch -import helm_drydock.control.api as api - -def start_drydock(): - objects.register_all() - - # Setup root logger - logger = logging.getLogger('drydock') - - logger.setLevel(config.DrydockConfig.global_config.get('log_level')) - ch = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') - ch.setFormatter(formatter) - logger.addHandler(ch) - - # Specalized format for API logging - logger = logging.getLogger('drydock.control') - logger.propagate = False - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(user)s - %(req_id)s - %(external_ctx)s - %(message)s') - - ch = logging.StreamHandler() - ch.setFormatter(formatter) - logger.addHandler(ch) - - state = statemgmt.DesignState() - - orchestrator = orch.Orchestrator(config.DrydockConfig.orchestrator_config.get('drivers', {}), - state_manager=state) - input_ingester = ingester.Ingester() - input_ingester.enable_plugins(config.DrydockConfig.ingester_config.get('plugins', [])) - - return api.start_api(state_manager=state, ingester=input_ingester, - orchestrator=orchestrator) - -drydock = start_drydock() - diff --git a/helm_drydock/ingester/__init__.py b/helm_drydock/ingester/__init__.py deleted file mode 100644 index 07862605..00000000 --- a/helm_drydock/ingester/__init__.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2017 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. -# -# ingester - Ingest host topologies to define site design and -# persist design to helm-drydock's statemgmt service - -import logging -import yaml -import uuid -import importlib - -import helm_drydock.objects as objects -import helm_drydock.objects.site as site -import helm_drydock.objects.network as network -import helm_drydock.objects.hwprofile as hwprofile -import helm_drydock.objects.node as node -import helm_drydock.objects.hostprofile as hostprofile - -from helm_drydock.statemgmt import DesignState - -class Ingester(object): - - def __init__(self): - self.logger = logging.getLogger("drydock.ingester") - self.registered_plugins = {} - - def enable_plugins(self, plugins=[]): - """ - enable_plugins - - :params plugins: - A list of strings naming class objects denoting the ingester plugins to be enabled - - Enable plugins that can be used for ingest_data calls. Each plugin should use - helm_drydock.ingester.plugins.IngesterPlugin as its base class. As long as one - enabled plugin successfully initializes, the call is considered successful. Otherwise - it will throw an exception - """ - if len(plugins) == 0: - self.log.error("Cannot have an empty plugin list.") - - for plugin in plugins: - try: - (module, x, classname) = plugin.rpartition('.') - - if module == '': - raise Exception() - mod = importlib.import_module(module) - klass = getattr(mod, classname) - new_plugin = klass() - plugin_name = new_plugin.get_name() - self.registered_plugins[plugin_name] = new_plugin - except Exception as ex: - self.logger.error("Could not enable plugin %s - %s" % (plugin, str(ex))) - - if len(self.registered_plugins) == 0: - self.logger.error("Could not enable at least one plugin") - raise Exception("Could not enable at least one plugin") - - - def ingest_data(self, plugin_name='', design_state=None, design_id=None, context=None, **kwargs): - if design_state is None: - self.logger.error("Ingester:ingest_data called without valid DesignState handler") - raise ValueError("Invalid design_state handler") - - # If no design_id is specified, instantiate a new one - if 'design_id' is None: - self.logger.error("Ingester:ingest_data required kwarg 'design_id' missing") - raise ValueError("Ingester:ingest_data required kwarg 'design_id' missing") - - design_data = design_state.get_design(design_id) - - self.logger.debug("Ingester:ingest_data ingesting design parts for design %s" % design_id) - - if plugin_name in self.registered_plugins: - design_items = self.registered_plugins[plugin_name].ingest_data(**kwargs) - self.logger.debug("Ingester:ingest_data parsed %s design parts" % str(len(design_items))) - for m in design_items: - if context is not None: - m.set_create_fields(context) - if type(m) is site.Site: - design_data.set_site(m) - elif type(m) is network.Network: - design_data.add_network(m) - elif type(m) is network.NetworkLink: - design_data.add_network_link(m) - elif type(m) is hostprofile.HostProfile: - design_data.add_host_profile(m) - elif type(m) is hwprofile.HardwareProfile: - design_data.add_hardware_profile(m) - elif type(m) is node.BaremetalNode: - design_data.add_baremetal_node(m) - design_state.put_design(design_data) - return design_items - else: - self.logger.error("Could not find plugin %s to ingest data." % (plugin_name)) - raise LookupError("Could not find plugin %s" % plugin_name) - """ - ingest_data - - params: plugin_name - Which plugin should be used for ingestion - params: params - A map of parameters that will be passed to the plugin's ingest_data method - - Execute a data ingestion using the named plugin (assuming it is enabled) - """ - From 842ad35bb8fb84c67a39dcd577e164a91c6fbbea Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Wed, 28 Jun 2017 11:24:24 -0500 Subject: [PATCH 03/16] Update Dockerfile to better pattern Add requirements layer Add entrypoint.sh script to support future CLI Change Dockerfile to use ENTRYPOINT --- Dockerfile | 29 +++++++++++++++++++++++------ entrypoint.sh | 12 ++++++++++++ requirements-direct.txt | 10 ++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100755 entrypoint.sh create mode 100644 requirements-direct.txt diff --git a/Dockerfile b/Dockerfile index a2bac38d..434e6c37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,20 +13,37 @@ # limitations under the License. FROM ubuntu:16.04 +ARG VERSION + ENV DEBIAN_FRONTEND noninteractive ENV container docker RUN apt -qq update && \ - apt -y install git netbase python3-minimal python3-setuptools python3-pip python3-dev ca-certificates gcc g++ make libffi-dev libssl-dev --no-install-recommends + apt -y install git \ + netbase \ + python3-minimal \ + python3-setuptools \ + python3-pip \ + python3-dev \ + ca-certificates \ + gcc \ + g++ \ + make \ + libffi-dev \ + libssl-dev --no-install-recommends -# Need to configure proxies? - -RUN git clone https://github.com/sh8121att/drydock /tmp/drydock +# Copy direct dependency requirements only to build a dependency layer +COPY ./requirements-direct.txt /tmp/drydock/ +RUN pip3 install -r /tmp/drydock/requirements-direct.txt +COPY . /tmp/drydock WORKDIR /tmp/drydock - RUN python3 setup.py install EXPOSE 9000 -CMD ["/usr/bin/uwsgi","--http",":9000","-w","drydock_provisioner.drydock","--callable","drydock","--enable-threads","-L"] \ No newline at end of file +COPY examples/drydock.conf /etc/drydock/drydock.conf + +ENTRYPOINT ["./entrypoint.sh"] + +CMD ["server"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..c9933790 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +CMD="drydock" +PORT="8000" + +set -e + +if [ "$1" = 'server' ]; then + exec uwsgi --http :${PORT} -w drydock_provisioner.drydock --callable drydock --enable-threads -L --pyargv "--config-file /etc/drydock/drydock.conf" +fi + +exec ${CMD} $@ diff --git a/requirements-direct.txt b/requirements-direct.txt new file mode 100644 index 00000000..bf099b84 --- /dev/null +++ b/requirements-direct.txt @@ -0,0 +1,10 @@ +PyYAML +pyghmi>=1.0.18 +netaddr +falcon +oslo.versionedobjects>=1.23.0 +requests +oauthlib +uwsgi>1.4 +bson===0.4.7 +oslo.config From 4efe40d184c28a9b8ec174f299b6523a80941c2a Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Wed, 28 Jun 2017 11:24:24 -0500 Subject: [PATCH 04/16] Update Dockerfile to better pattern Add requirements layer Add entrypoint.sh script to support future CLI Change Dockerfile to use ENTRYPOINT --- Dockerfile | 29 +++++++++++++++++++++++------ entrypoint.sh | 12 ++++++++++++ requirements-direct.txt | 10 ++++++++++ requirements.txt | 5 ----- 4 files changed, 45 insertions(+), 11 deletions(-) create mode 100755 entrypoint.sh create mode 100644 requirements-direct.txt delete mode 100644 requirements.txt diff --git a/Dockerfile b/Dockerfile index a2bac38d..434e6c37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,20 +13,37 @@ # limitations under the License. FROM ubuntu:16.04 +ARG VERSION + ENV DEBIAN_FRONTEND noninteractive ENV container docker RUN apt -qq update && \ - apt -y install git netbase python3-minimal python3-setuptools python3-pip python3-dev ca-certificates gcc g++ make libffi-dev libssl-dev --no-install-recommends + apt -y install git \ + netbase \ + python3-minimal \ + python3-setuptools \ + python3-pip \ + python3-dev \ + ca-certificates \ + gcc \ + g++ \ + make \ + libffi-dev \ + libssl-dev --no-install-recommends -# Need to configure proxies? - -RUN git clone https://github.com/sh8121att/drydock /tmp/drydock +# Copy direct dependency requirements only to build a dependency layer +COPY ./requirements-direct.txt /tmp/drydock/ +RUN pip3 install -r /tmp/drydock/requirements-direct.txt +COPY . /tmp/drydock WORKDIR /tmp/drydock - RUN python3 setup.py install EXPOSE 9000 -CMD ["/usr/bin/uwsgi","--http",":9000","-w","drydock_provisioner.drydock","--callable","drydock","--enable-threads","-L"] \ No newline at end of file +COPY examples/drydock.conf /etc/drydock/drydock.conf + +ENTRYPOINT ["./entrypoint.sh"] + +CMD ["server"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..c9933790 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +CMD="drydock" +PORT="8000" + +set -e + +if [ "$1" = 'server' ]; then + exec uwsgi --http :${PORT} -w drydock_provisioner.drydock --callable drydock --enable-threads -L --pyargv "--config-file /etc/drydock/drydock.conf" +fi + +exec ${CMD} $@ diff --git a/requirements-direct.txt b/requirements-direct.txt new file mode 100644 index 00000000..bf099b84 --- /dev/null +++ b/requirements-direct.txt @@ -0,0 +1,10 @@ +PyYAML +pyghmi>=1.0.18 +netaddr +falcon +oslo.versionedobjects>=1.23.0 +requests +oauthlib +uwsgi>1.4 +bson===0.4.7 +oslo.config diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1a33ec11..00000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -PyYAML==3.12 -oauth==1.0.1 -requests-oauthlib==0.8.0 -netaddr==0.7.19 -python-libmaas==0.4.1 \ No newline at end of file From 1ae3c813fa7918da57c1127fd94b2d3cdb89e05f Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Thu, 15 Jun 2017 16:59:54 -0500 Subject: [PATCH 05/16] DRYD-2 MVP Phase 3 - Network interface configuration Implemented Orhcestrator task DeployNode (only network config for now) Implemented Driver task ApplyNodeNetworking Refactored Driver task CreateNetworkTemplate to fix design that left network in a state that wouldn't support node configs Updated the YAML example with some changes to support network refactoring - HostProfile field 'primary_network' specifies the network a node should use for default gateway - NetworkLinks now must list all allowed networks for that link and a Network is allowed only on a single link Updated YAML ingester to accept schema changes --- .../drivers/node/maasdriver/driver.py | 447 ++++++++++++++---- .../drivers/node/maasdriver/models/base.py | 4 +- .../node/maasdriver/models/interface.py | 228 ++++++++- .../drivers/node/maasdriver/models/iprange.py | 73 +++ .../drivers/node/maasdriver/models/machine.py | 1 + .../drivers/node/maasdriver/models/subnet.py | 53 ++- .../drivers/node/maasdriver/models/vlan.py | 5 +- drydock_provisioner/ingester/plugins/yaml.py | 7 +- drydock_provisioner/objects/hostprofile.py | 8 +- drydock_provisioner/objects/network.py | 3 +- drydock_provisioner/orchestrator/__init__.py | 58 +++ drydock_provisioner/orchestrator/readme.md | 3 +- examples/designparts_v1.0.yaml | 9 +- 13 files changed, 774 insertions(+), 125 deletions(-) create mode 100644 drydock_provisioner/drivers/node/maasdriver/models/iprange.py diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index e7800529..9da0f9c3 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -13,6 +13,8 @@ # limitations under the License. import time import logging +import traceback +import sys import drydock_provisioner.error as errors import drydock_provisioner.config as config @@ -290,7 +292,76 @@ class MaasNodeDriver(NodeDriver): status=hd_fields.TaskStatus.Complete, result=result, result_detail=result_detail) + elif task.action == hd_fields.OrchestratorAction.ApplyNodeNetworking: + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Running) + self.logger.debug("Starting subtask to configure networking on %s nodes." % (len(task.node_list))) + + subtasks = [] + + result_detail = { + 'detail': [], + 'failed_nodes': [], + 'successful_nodes': [], + } + + for n in task.node_list: + subtask = self.orchestrator.create_task(task_model.DriverTask, + parent_task_id=task.get_id(), design_id=design_id, + action=hd_fields.OrchestratorAction.ApplyNodeNetworking, + site_name=task.site_name, + task_scope={'site': task.site_name, 'node_names': [n]}) + runner = MaasTaskRunner(state_manager=self.state_manager, + orchestrator=self.orchestrator, + task_id=subtask.get_id(),config=self.config) + + self.logger.info("Starting thread for task %s to configure networking on node %s" % (subtask.get_id(), n)) + + runner.start() + subtasks.append(subtask.get_id()) + + running_subtasks = len(subtasks) + attempts = 0 + worked = failed = False + + #TODO Add timeout to config + while running_subtasks > 0 and attempts < 2: + for t in subtasks: + subtask = self.state_manager.get_task(t) + + if subtask.status == hd_fields.TaskStatus.Complete: + self.logger.info("Task %s to apply networking on node %s complete - status %s" % + (subtask.get_id(), n, subtask.get_result())) + running_subtasks = running_subtasks - 1 + + if subtask.result == hd_fields.ActionResult.Success: + result_detail['successful_nodes'].extend(subtask.node_list) + worked = True + elif subtask.result == hd_fields.ActionResult.Failure: + result_detail['failed_nodes'].extend(subtask.node_list) + failed = True + elif subtask.result == hd_fields.ActionResult.PartialSuccess: + worked = failed = True + + time.sleep(1 * 60) + attempts = attempts + 1 + + if running_subtasks > 0: + self.logger.warning("Time out for task %s before all subtask threads complete" % (task.get_id())) + result = hd_fields.ActionResult.DependentFailure + result_detail['detail'].append('Some subtasks did not complete before the timeout threshold') + elif worked and failed: + result = hd_fields.ActionResult.PartialSuccess + elif worked: + result = hd_fields.ActionResult.Success + else: + result = hd_fields.ActionResult.Failure + + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=result, + result_detail=result_detail) class MaasTaskRunner(drivers.DriverTaskRunner): def __init__(self, config=None, **kwargs): @@ -315,6 +386,10 @@ class MaasTaskRunner(drivers.DriverTaskRunner): # Try to true up MaaS definitions of fabrics/vlans/subnets # with the networks defined in Drydock design_networks = site_design.networks + design_links = site_design.network_links + + fabrics = maas_fabric.Fabrics(self.maas_client) + fabrics.refresh() subnets = maas_subnet.Subnets(self.maas_client) subnets.refresh() @@ -323,128 +398,171 @@ class MaasTaskRunner(drivers.DriverTaskRunner): 'detail': [] } - for n in design_networks: - try: - subnet = subnets.singleton({'cidr': n.cidr}) + for l in design_links: + fabrics_found = set() - if subnet is not None: - subnet.name = n.name - subnet.dns_servers = n.dns_servers + # First loop through the possible Networks on this NetworkLink + # and validate that MaaS's self-discovered networking matches + # our design. This means all self-discovered networks that are matched + # to a link need to all be part of the same fabric. Otherwise there is no + # way to reconcile the discovered topology with the designed topology + for net_name in l.allowed_networks: + n = site_design.get_network(net_name) - vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=subnet.fabric) - vlan_list.refresh() + if n is None: + self.logger.warning("Network %s allowed on link %s, but not defined." % (net_name, l.name)) + continue - vlan = vlan_list.select(subnet.vlan) + maas_net = subnets.singleton({'cidr': n.cidr}) - if vlan is not None: - if ((n.vlan_id is None and vlan.vid != 0) or - (n.vlan_id is not None and vlan.vid != n.vlan_id)): + if maas_net is not None: + fabrics_found.add(maas_net.fabric) + + if len(fabrics_found) > 1: + self.logger.warning("MaaS self-discovered network incompatible with NetworkLink %s" % l.name) + continue + elif len(fabrics_found) == 1: + link_fabric_id = fabrics_found.pop() + link_fabric = fabrics.select(link_fabric_id) + link_fabric.name = l.name + link_fabric.update() + else: + link_fabric = fabrics.singleton({'name': l.name}) + + if link_fabric is None: + link_fabric = maas_fabric.Fabric(self.maas_client, name=l.name) + fabrics.add(link_fabric) + + + # Now that we have the fabrics sorted out, check + # that VLAN tags and subnet attributes are correct + for net_name in l.allowed_networks: + n = site_design.get_network(net_name) + + if n is None: + continue + + try: + subnet = subnets.singleton({'cidr': n.cidr}) + + if subnet is None: + self.logger.info("Subnet for network %s not found, creating..." % (n.name)) + + fabric_list = maas_fabric.Fabrics(self.maas_client) + fabric_list.refresh() + fabric = fabric_list.singleton({'name': l.name}) + + if fabric is not None: + vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=fabric.resource_id) + vlan_list.refresh() + + vlan = vlan_list.singleton({'vid': n.vlan_id if n.vlan_id is not None else 0}) + + if vlan is not None: + vlan.name = n.name + + if getattr(n, 'mtu', None) is not None: + vlan.mtu = n.mtu - # if the VLAN name matches, assume this is the correct resource - # and it needs to be updated - if vlan.name == n.name: - vlan.set_vid(n.vlan_id) - vlan.mtu = n.mtu vlan.update() result_detail['detail'].append("VLAN %s found for network %s, updated attributes" - % (vlan.resource_id, n.name)) + % (vlan.resource_id, n.name)) else: - # Found a VLAN with the correct VLAN tag, update subnet to use it - target_vlan = vlan_list.singleton({'vid': n.vlan_id if n.vlan_id is not None else 0}) - if target_vlan is not None: - subnet.vlan = target_vlan.resource_id - else: - # This is a flag that after creating a fabric and - # VLAN below, update the subnet - subnet.vlan = None - else: - subnet.vlan = None - - # Check if the routes have a default route - subnet.gateway_ip = n.get_default_gateway() + # Create a new VLAN in this fabric and assign subnet to it + vlan = maas_vlan.Vlan(self.maas_client, name=n.name, vid=vlan_id, + mtu=getattr(n, 'mtu', None),fabric_id=fabric.resource_id) + vlan = vlan_list.add(vlan) - - result_detail['detail'].append("Subnet %s found for network %s, updated attributes" - % (subnet.resource_id, n.name)) - - # Need to find or create a Fabric/Vlan for this subnet - if (subnet is None or (subnet is not None and subnet.vlan is None)): - fabric_list = maas_fabric.Fabrics(self.maas_client) - fabric_list.refresh() - fabric = fabric_list.singleton({'name': n.name}) - - vlan = None + result_detail['detail'].append("VLAN %s created for network %s" + % (vlan.resource_id, n.name)) - if fabric is not None: - vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=fabric.resource_id) + # If subnet did not exist, create it here and attach it to the fabric/VLAN + subnet = maas_subnet.Subnet(self.maas_client, name=n.name, cidr=n.cidr, fabric=fabric.resource_id, + vlan=vlan.resource_id, gateway_ip=n.get_default_gateway()) + + subnet_list = maas_subnet.Subnets(self.maas_client) + subnet = subnet_list.add(subnet) + self.logger.info("Created subnet %s for CIDR %s on VLAN %s" % + (subnet.resource_id, subnet.cidr, subnet.vlan)) + + result_detail['detail'].append("Subnet %s created for network %s" % (subnet.resource_id, n.name)) + else: + self.logger.error("Fabric %s should be created, but cannot locate it." % (l.name)) + else: + subnet.name = n.name + subnet.dns_servers = n.dns_servers + + result_detail['detail'].append("Subnet %s found for network %s, updated attributes" + % (subnet.resource_id, n.name)) + self.logger.info("Updating existing MaaS subnet %s" % (subnet.resource_id)) + + vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=subnet.fabric) vlan_list.refresh() - - vlan = vlan_list.singleton({'vid': n.vlan_id if n.vlan_id is not None else 0}) + + vlan = vlan_list.select(subnet.vlan) if vlan is not None: - vlan = matching_vlans[0] - vlan.name = n.name + vlan.set_vid(n.vlan_id) + if getattr(n, 'mtu', None) is not None: vlan.mtu = n.mtu - if subnet is not None: - subnet.vlan = vlan.resource_id - subnet.update() - vlan.update() result_detail['detail'].append("VLAN %s found for network %s, updated attributes" - % (vlan.resource_id, n.name)) + % (vlan.resource_id, n.name)) else: - # Create a new VLAN in this fabric and assign subnet to it - vlan = maas_vlan.Vlan(self.maas_client, name=n.name, vid=vlan_id, - mtu=getattr(n, 'mtu', None),fabric_id=fabric.resource_id) - vlan = vlan_list.add(vlan) + self.logger.error("MaaS subnet %s does not have a matching VLAN" % (subnet.resource_id)) + continue + + # Check if the routes have a default route + subnet.gateway_ip = n.get_default_gateway() + subnet.update() - result_detail['detail'].append("VLAN %s created for network %s" - % (vlan.resource_id, n.name)) - if subnet is not None: - subnet.vlan = vlan.resource_id - subnet.update() + dhcp_on = False - else: - # Create new fabric and VLAN - fabric = maas_fabric.Fabric(self.maas_client, name=n.name) - fabric = fabric_list.add(fabric) - fabric_list.refresh() + for r in n.ranges: + subnet.add_address_range(r) + if r.get('type', None) == 'dhcp': + dhcp_on = True - result_detail['detail'].append("Fabric %s created for network %s" - % (fabric.resource_id, n.name)) + vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=subnet.fabric) + vlan_list.refresh() + vlan = vlan_list.select(subnet.vlan) - vlan_list = maas_vlan.Vlans(self.maas_client, fabric_id=new_fabric.resource_id) - vlan_list.refresh() + if dhcp_on and not vlan.dhcp_on: + self.logger.info("DHCP enabled for subnet %s, activating in MaaS" % (subnet.name)) - # A new fabric comes with a single default VLAN. Retrieve it and update attributes - vlan = vlan_list.single() - vlan.name = n.name - vlan.vid = n.vlan_id if n.vlan_id is not None else 0 - if getattr(n, 'mtu', None) is not None: - vlan.mtu = n.mtu + # TODO Ugly hack assuming a single rack controller for now until we implement multirack + resp = self.maas_client.get("rackcontrollers/") - vlan.update() - result_detail['detail'].append("VLAN %s updated for network %s" - % (vlan.resource_id, n.name)) - if subnet is not None: - # If subnet was found above, but needed attached to a new fabric/vlan then - # attach it - subnet.vlan = vlan.resource_id - subnet.update() + if resp.ok: + resp_json = resp.json() - if subnet is None: - # If subnet did not exist, create it here and attach it to the fabric/VLAN - subnet = maas_subnet.Subnet(self.maas_client, name=n.name, cidr=n.cidr, fabric=fabric.resource_id, - vlan=vlan.resource_id, gateway_ip=n.get_default_gateway()) + if not isinstance(resp_json, list): + self.logger.warning("Unexpected response when querying list of rack controllers") + self.logger.debug("%s" % resp.text) + else: + if len(resp_json) > 1: + self.logger.warning("Received more than one rack controller, defaulting to first") - subnet_list = maas_subnet.Subnets(self.maas_client) - subnet = subnet_list.add(subnet) - except ValueError as vex: - raise errors.DriverError("Inconsistent data from MaaS") + rackctl_id = resp_json[0]['system_id'] + + vlan.dhcp_on = True + vlan.primary_rack = rackctl_id + vlan.update() + self.logger.debug("Enabling DHCP on VLAN %s managed by rack ctlr %s" % + (vlan.resource_id, rackctl_id)) + elif dhcp_on and vlan.dhcp_on: + self.logger.info("DHCP already enabled for subnet %s" % (subnet.resource_id)) + + + # TODO sort out static route support as MaaS seems to require the destination + # network be defined in MaaS as well + + except ValueError as vex: + raise errors.DriverError("Inconsistent data from MaaS") subnet_list = maas_subnet.Subnets(self.maas_client) subnet_list.refresh() @@ -542,7 +660,7 @@ class MaasTaskRunner(drivers.DriverTaskRunner): node = site_design.get_baremetal_node(n) machine = machine_list.identify_baremetal_node(node, update_name=False) if machine is not None: - if machine.status_name == 'New': + if machine.status_name == ['New', 'Broken']: self.logger.debug("Located node %s in MaaS, starting commissioning" % (n)) machine.commission() @@ -595,3 +713,144 @@ class MaasTaskRunner(drivers.DriverTaskRunner): status=hd_fields.TaskStatus.Complete, result=result, result_detail=result_detail) + elif task_action == hd_fields.OrchestratorAction.ApplyNodeNetworking: + try: + machine_list = maas_machine.Machines(self.maas_client) + machine_list.refresh() + + fabrics = maas_fabric.Fabrics(self.maas_client) + fabrics.refresh() + + subnets = maas_subnet.Subnets(self.maas_client) + subnets.refresh() + except Exception as ex: + self.logger.error("Error applying node networking, cannot access MaaS: %s" % str(ex)) + traceback.print_tb(sys.last_traceback) + self.orchestrator.task_field_update(self.task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=hd_fields.ActionResult.Failure, + result_detail={'detail': 'Error accessing MaaS API', 'retry': True}) + return + + nodes = self.task.node_list + + result_detail = {'detail': []} + + worked = failed = False + + # TODO Better way of representing the node statuses than static strings + for n in nodes: + try: + self.logger.debug("Locating node %s for network configuration" % (n)) + + node = site_design.get_baremetal_node(n) + machine = machine_list.identify_baremetal_node(node, update_name=False) + + if machine is not None: + if machine.status_name == 'Ready': + self.logger.debug("Located node %s in MaaS, starting interface configuration" % (n)) + + for i in node.interfaces: + nl = site_design.get_network_link(i.network_link) + + fabric = fabrics.singleton({'name': nl.name}) + + if fabric is None: + self.logger.error("No fabric found for NetworkLink %s" % (nl.name)) + failed = True + continue + + # TODO HardwareProfile device alias integration + iface = machine.get_network_interface(i.device_name) + + if iface is None: + self.logger.warning("Interface %s not found on node %s, skipping configuration" % + (i.device_name, machine.resource_id)) + continue + + if iface.fabric_id == fabric.resource_id: + self.logger.debug("Interface %s already attached to fabric_id %s" % + (i.device_name, fabric.resource_id)) + else: + self.logger.debug("Attaching node %s interface %s to fabric_id %s" % + (node.name, i.device_name, fabric.resource_id)) + iface.attach_fabric(fabric_id=fabric.resource_id) + + for iface_net in getattr(i, 'networks', []): + dd_net = site_design.get_network(iface_net) + + if dd_net is not None: + link_iface = None + if iface_net == getattr(nl, 'native_network', None): + # If a node interface is attached to the native network for a link + # then the interface itself should be linked to network, not a VLAN + # tagged interface + self.logger.debug("Attaching node %s interface %s to untagged VLAN on fabric %s" % + (node.name, i.device_name, fabric.resource_id)) + link_iface = iface + else: + # For non-native networks, we create VLAN tagged interfaces as children + # of this interface + vlan_options = { 'vlan_tag': dd_net.vlan_id, + 'parent_name': iface.name, + } + + if dd_net.mtu is not None: + vlan_options['mtu'] = dd_net.mtu + + self.logger.debug("Creating tagged interface for VLAN %s on system %s interface %s" % + (dd_net.vlan_id, node.name, i.device_name)) + + link_iface = machine.interfaces.create_vlan(**vlan_options) + + link_options = {} + link_options['primary'] = True if iface_net == getattr(node, 'primary_network', None) else False + link_options['subnet_cidr'] = dd_net.cidr + + found = False + for a in getattr(node, 'addressing', []): + if a.network == iface_net: + link_options['ip_address'] = None if a.address == 'dhcp' else a.address + found = True + + if not found: + self.logger.error("No addressed assigned to network %s for node %s, cannot link." % + (iface_net, node.name)) + continue + + self.logger.debug("Linking system %s interface %s to subnet %s" % + (node.name, i.device_name, dd_net.cidr)) + + link_iface.link_subnet(**link_options) + worked = True + else: + failed=True + self.logger.error("Did not find a defined Network %s to attach to interface" % iface_net) + + elif machine.status_name == 'Broken': + self.logger.info("Located node %s in MaaS, status broken. Run ConfigureHardware before configurating network" % (n)) + result_detail['detail'].append("Located node %s in MaaS, status 'Broken'. Skipping..." % (n)) + failed = True + else: + self.logger.warning("Located node %s in MaaS, unknown status %s. Skipping..." % (n, machine.status_name)) + result_detail['detail'].append("Located node %s in MaaS, unknown status %s. Skipping..." % (n, machine.status_name)) + failed = True + else: + self.logger.warning("Node %s not found in MaaS" % n) + failed = True + result_detail['detail'].append("Node %s not found in MaaS" % n) + + except Exception as ex: + failed = True + self.logger.error("Error configuring network for node %s: %s" % (n, str(ex))) + result_detail['detail'].append("Error configuring network for node %s: %s" % (n, str(ex))) + + if failed: + final_result = hd_fields.ActionResult.Failure + else: + final_result = hd_fields.ActionResult.Success + + self.orchestrator.task_field_update(self.task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=final_result, + result_detail=result_detail) diff --git a/drydock_provisioner/drivers/node/maasdriver/models/base.py b/drydock_provisioner/drivers/node/maasdriver/models/base.py index 2b3df5df..84090240 100644 --- a/drydock_provisioner/drivers/node/maasdriver/models/base.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/base.py @@ -29,7 +29,7 @@ class ResourceBase(object): def __init__(self, api_client, **kwargs): self.api_client = api_client - self.logger = logging.getLogger('drydock.drivers.maasdriver') + self.logger = logging.getLogger('drydock.nodedriver.maasdriver') for f in self.fields: if f in kwargs.keys(): @@ -161,7 +161,7 @@ class ResourceCollectionBase(object): def __init__(self, api_client): self.api_client = api_client self.resources = {} - self.logger = logging.getLogger('drydock.drivers.maasdriver') + self.logger = logging.getLogger('drydock.nodedriver.maasdriver') def interpolate_url(self): """ diff --git a/drydock_provisioner/drivers/node/maasdriver/models/interface.py b/drydock_provisioner/drivers/node/maasdriver/models/interface.py index 5c257279..db2a9d85 100644 --- a/drydock_provisioner/drivers/node/maasdriver/models/interface.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/interface.py @@ -11,18 +11,170 @@ # 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. +import logging import drydock_provisioner.drivers.node.maasdriver.models.base as model_base +import drydock_provisioner.drivers.node.maasdriver.models.fabric as maas_fabric +import drydock_provisioner.drivers.node.maasdriver.models.subnet as maas_subnet +import drydock_provisioner.drivers.node.maasdriver.models.vlan as maas_vlan + +import drydock_provisioner.error as errors class Interface(model_base.ResourceBase): resource_url = 'nodes/{system_id}/interfaces/{resource_id}/' fields = ['resource_id', 'system_id', 'name', 'type', 'mac_address', 'vlan', - 'links', 'effective_mtu'] + 'links', 'effective_mtu', 'fabric_id'] json_fields = ['name', 'type', 'mac_address', 'vlan', 'links', 'effective_mtu'] def __init__(self, api_client, **kwargs): super(Interface, self).__init__(api_client, **kwargs) + self.logger = logging.getLogger('drydock.nodedriver.maasdriver') + + def attach_fabric(self, fabric_id=None, fabric_name=None): + """ + Attach this interface to a MaaS fabric. Only one of fabric_id + or fabric_name should be specified. If both are, fabric_id rules + + :param fabric_id: The MaaS resource ID of a network Fabric to connect to + :param fabric_name: The name of a MaaS fabric to connect to + """ + + fabric = None + + fabrics = maas_fabric.Fabrics(self.api_client) + fabrics.refresh() + + if fabric_id is not None: + fabric = fabrics.select(fabric_id) + elif fabric_name is not None: + fabric = fabrics.singleton({'name': fabric_name}) + else: + self.logger.warning("Must specify fabric_id or fabric_name") + raise ValueError("Must specify fabric_id or fabric_name") + + if fabric is None: + self.logger.warning("Fabric not found in MaaS for fabric_id %s, fabric_name %s" % + (fabric_id, fabric_name)) + raise errors.DriverError("Fabric not found in MaaS for fabric_id %s, fabric_name %s" % + (fabric_id, fabric_name)) + + # Locate the untagged VLAN for this fabric. + fabric_vlan = fabric.vlans.singleton({'vid': 0}) + + if fabric_vlan is None: + self.logger.warning("Cannot locate untagged VLAN on fabric %s" % (fabric_id)) + raise errors.DriverError("Cannot locate untagged VLAN on fabric %s" % (fabric_id)) + + self.vlan = fabric_vlan.resource_id + self.logger.info("Attaching interface %s on system %s to VLAN %s on fabric %s" % + (self.resource_id, self.system_id, fabric_vlan.resource_id, fabric.resource_id)) + self.update() + + def is_linked(self, subnet_id): + for l in self.links: + if l.get('subnet_id', None) == subnet_id: + return True + + return False + + def link_subnet(self, subnet_id=None, subnet_cidr=None, ip_address=None, primary=False): + """ + Link this interface to a MaaS subnet. One of subnet_id or subnet_cidr + should be specified. If both are, subnet_id rules. + + :param subnet_id: The MaaS resource ID of a network subnet to connect to + :param subnet_cidr: The CIDR of a MaaS subnet to connect to + :param ip_address: The IP address to assign this interface. Should be a string with + a static IP or None. If None, DHCP will be used. + :param primary: Boolean of whether this interface is the primary interface of the node. This + sets the node default gateway to the gateway of the subnet + """ + + subnet = None + + subnets = maas_subnet.Subnets(self.api_client) + subnets.refresh() + + if subnet_id is not None: + subnet = subnets.select(subnet_id) + elif subnet_cidr is not None: + subnet = subnets.singleton({'cidr': subnet_cidr}) + else: + self.logger.warning("Must specify subnet_id or subnet_cidr") + raise ValueError("Must specify subnet_id or subnet_cidr") + + if subnet is None: + self.logger.warning("Subnet not found in MaaS for subnet_id %s, subnet_cidr %s" % + (subnet_id, subnet_cidr)) + raise errors.DriverError("Subnet not found in MaaS for subnet_id %s, subnet_cidr %s" % + (subnet_id, subnet_cidr)) + + # TODO Possibly add logic to true up link attributes, may be overkill + if self.is_linked(subnet.resource_id): + self.logger.info("Interface %s already linked to subnet %s, skipping." % + (self.resource_id, subnet.resource_id)) + return + + url = self.interpolate_url() + + # TODO Probably need to enumerate link mode + options = { 'subnet': subnet.resource_id, + 'mode': 'dhcp' if ip_address is None else 'static', + 'default_gateway': primary, + } + + if ip_address is not None: + options['ip_address'] = ip_address + + self.logger.debug("Linking interface %s to subnet: subnet=%s, mode=%s, address=%s, primary=%s" % + (self.resource_id, subnet.resource_id, options['mode'], ip_address, primary)) + + resp = self.api_client.post(url, op='link_subnet', files=options) + + if not resp.ok: + self.logger.error("Error linking interface %s to subnet %s - MaaS response %s: %s" % + (self.resouce_id, subnet.resource_id, resp.status_code, resp.text)) + raise errors.DriverError("Error linking interface %s to subnet %s - MaaS response %s" % + (self.resouce_id, subnet.resource_id, resp.status_code)) + + self.refresh() + + return + + @classmethod + def from_dict(cls, api_client, obj_dict): + """ + Because MaaS decides to replace the resource ids with the + representation of the resource, we must reverse it for a true + representation of the Interface + """ + refined_dict = {k: obj_dict.get(k, None) for k in cls.fields} + if 'id' in obj_dict.keys(): + refined_dict['resource_id'] = obj_dict.get('id') + + if isinstance(refined_dict.get('vlan', None), dict): + refined_dict['fabric_id'] = refined_dict['vlan']['fabric_id'] + refined_dict['vlan'] = refined_dict['vlan']['id'] + + link_list = [] + if isinstance(refined_dict.get('links', None), list): + for l in refined_dict['links']: + if isinstance(l, dict): + link = { 'resource_id': l['id'], + 'mode': l['mode'] + } + + if l.get('subnet', None) is not None: + link['subnet_id'] = l['subnet']['id'] + link['ip_address'] = l.get('ip_address', None) + + link_list.append(link) + + refined_dict['links'] = link_list + + i = cls(api_client, **refined_dict) + return i class Interfaces(model_base.ResourceCollectionBase): @@ -31,4 +183,76 @@ class Interfaces(model_base.ResourceCollectionBase): def __init__(self, api_client, **kwargs): super(Interfaces, self).__init__(api_client) - self.system_id = kwargs.get('system_id', None) \ No newline at end of file + self.system_id = kwargs.get('system_id', None) + + def create_vlan(self, vlan_tag, parent_name, mtu=None, tags=[]): + """ + Create a new VLAN interface on this node + + :param vlan_tag: The VLAN ID (not MaaS resource id of a VLAN) to create interface for + :param parent_name: The name of a MaaS interface to build the VLAN interface on top of + :param mtu: Optional configuration of the interface MTU + :param tags: Optional list of string tags to apply to the VLAN interface + """ + + self.refresh() + + parent_iface = self.singleton({'name': parent_name}) + + if parent_iface is None: + self.logger.error("Cannot locate parent interface %s" % (parent_name)) + raise errors.DriverError("Cannot locate parent interface %s" % (parent_name)) + + if parent_iface.type != 'physical': + self.logger.error("Cannot create VLAN interface on parent of type %s" % (parent_iface.type)) + raise errors.DriverError("Cannot create VLAN interface on parent of type %s" % (parent_iface.type)) + + if parent_iface.vlan is None: + self.logger.error("Cannot create VLAN interface on disconnected parent %s" % (parent_iface.resource_id)) + raise errors.DriverError("Cannot create VLAN interface on disconnected parent %s" % (parent_iface.resource_id)) + + vlans = maas_vlan.Vlans(self.api_client, fabric_id=parent_iface.fabric_id) + vlans.refresh() + + vlan = vlans.singleton({'vid': vlan_tag}) + + if vlan is None: + self.logger.error("Cannot locate VLAN %s on fabric %s to attach interface" % + (vlan_tag, parent_iface.fabric_id)) + + exists = self.singleton({'vlan': vlan.resource_id}) + + if exists is not None: + self.logger.info("Interface for VLAN %s already exists on node %s, skipping" % + (vlan_tag, self.system_id)) + return None + + url = self.interpolate_url() + + + options = { 'tags': ','.join(tags), + 'vlan': vlan.resource_id, + 'parent': parent_iface.resource_id, + } + + if mtu is not None: + options['mtu'] = mtu + + resp = self.api_client.post(url, op='create_vlan', files=options) + + + if resp.status_code == 200: + resp_json = resp.json() + vlan_iface = Interface.from_dict(self.api_client, resp_json) + self.logger.debug("Created VLAN interface %s for parent %s attached to VLAN %s" % + (vlan_iface.resource_id, parent_iface.resource_id, vlan.resource_id)) + return vlan_iface + else: + self.logger.error("Error creating VLAN interface to VLAN %s on system %s - MaaS response %s: %s" % + (vlan.resource_id, self.system_id, resp.status_code, resp.text)) + raise errors.DriverError("Error creating VLAN interface to VLAN %s on system %s - MaaS response %s" % + (vlan.resource_id, self.system_id, resp.status_code)) + + self.refresh() + + return \ No newline at end of file diff --git a/drydock_provisioner/drivers/node/maasdriver/models/iprange.py b/drydock_provisioner/drivers/node/maasdriver/models/iprange.py new file mode 100644 index 00000000..3840fea4 --- /dev/null +++ b/drydock_provisioner/drivers/node/maasdriver/models/iprange.py @@ -0,0 +1,73 @@ +# Copyright 2017 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. + +import drydock_provisioner.drivers.node.maasdriver.models.base as model_base + +class IpRange(model_base.ResourceBase): + + resource_url = 'iprange/{resource_id}/' + fields = ['resource_id', 'comment', 'subnet', 'type', 'start_ip', 'end_ip'] + json_fields = ['comment','start_ip', 'end_ip'] + + def __init__(self, api_client, **kwargs): + super(IpRange, self).__init__(api_client, **kwargs) + + @classmethod + def from_dict(cls, api_client, obj_dict): + refined_dict = {k: obj_dict.get(k, None) for k in cls.fields} + if 'id' in obj_dict.keys(): + refined_dict['resource_id'] = obj_dict.get('id') + + if isinstance(refined_dict.get('subnet', None), dict): + refined_dict['subnet'] = refined_dict['subnet']['id'] + + i = cls(api_client, **refined_dict) + return i + +class IpRanges(model_base.ResourceCollectionBase): + + collection_url = 'ipranges/' + collection_resource = IpRange + + def __init__(self, api_client, **kwargs): + super(IpRanges, self).__init__(api_client) + + def add(self, res): + """ + Custom add to include a subnet id and type which can't be + updated in a PUT + """ + data_dict = res.to_dict() + + subnet = getattr(res, 'subnet', None) + + if subnet is not None: + data_dict['subnet'] = subnet + + range_type = getattr(res, 'type', None) + + if range_type is not None: + data_dict['type'] = range_type + + url = self.interpolate_url() + + resp = self.api_client.post(url, files=data_dict) + + if resp.status_code == 200: + resp_json = resp.json() + res.set_resource_id(resp_json.get('id')) + return res + + raise errors.DriverError("Failed updating MAAS url %s - return code %s" + % (url, resp.status_code)) \ No newline at end of file diff --git a/drydock_provisioner/drivers/node/maasdriver/models/machine.py b/drydock_provisioner/drivers/node/maasdriver/models/machine.py index e06263ab..088ca986 100644 --- a/drydock_provisioner/drivers/node/maasdriver/models/machine.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/machine.py @@ -14,6 +14,7 @@ import drydock_provisioner.drivers.node.maasdriver.models.base as model_base import drydock_provisioner.drivers.node.maasdriver.models.interface as maas_interface + import bson import yaml diff --git a/drydock_provisioner/drivers/node/maasdriver/models/subnet.py b/drydock_provisioner/drivers/node/maasdriver/models/subnet.py index 8aec521e..a9e6104a 100644 --- a/drydock_provisioner/drivers/node/maasdriver/models/subnet.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/subnet.py @@ -11,30 +11,61 @@ # 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. - import drydock_provisioner.drivers.node.maasdriver.models.base as model_base +import drydock_provisioner.drivers.node.maasdriver.models.iprange as maas_iprange class Subnet(model_base.ResourceBase): resource_url = 'subnets/{resource_id}/' - fields = ['resource_id', 'name', 'description', 'fabric', 'vlan', 'vid', 'dhcp_on', - 'space', 'cidr', 'gateway_ip', 'rdns_mode', 'allow_proxy', 'dns_servers'] - json_fields = ['name', 'description','vlan', 'space', 'cidr', 'gateway_ip', 'rdns_mode', + fields = ['resource_id', 'name', 'description', 'fabric', 'vlan', 'vid', + 'cidr', 'gateway_ip', 'rdns_mode', 'allow_proxy', 'dns_servers'] + json_fields = ['name', 'description','vlan', 'cidr', 'gateway_ip', 'rdns_mode', 'allow_proxy', 'dns_servers'] def __init__(self, api_client, **kwargs): super(Subnet, self).__init__(api_client, **kwargs) - # For now all subnets will be part of the default space - self.space = 0 + def add_address_range(self, addr_range): + """ + Add a reserved or dynamic (DHCP) address range to this subnet + + :param addr_range: Dict with keys 'type', 'start', 'end' + """ + + # TODO Do better overlap detection. For now we just check if the exact range exists + current_ranges = maas_iprange.IpRanges(self.api_client) + current_ranges.refresh() + + exists = current_ranges.query({'start_ip': addr_range.get('start', None), + 'end_ip': addr_range.get('end', None)}) + + if len(exists) > 0: + self.logger.info('Address range from %s to %s already exists, skipping.' % + (addr_range.get('start', None), addr_range.get('end', None))) + return + + # Static ranges are what is left after reserved (not assigned by MaaS) + # and DHCP ranges are removed from a subnet + if addr_range.get('type', None) in ['reserved','dhcp']: + range_type = addr_range('type', None) + + if range_type == 'dhcp': + range_type = 'dynamic' + + maas_range = maas_iprange.IpRange(self.api_client, comment="Configured by Drydock", subnet=self.resource_id, + type=range_type, start_ip=addr_range.get('start', None), + end_ip=addr_range.get('end', None)) + maas_ranges = maas_iprange.IpRanges(self.api_client) + maas_ranges.add(maas_range) + - """ - Because MaaS decides to replace the VLAN id with the - representation of the VLAN, we must reverse it for a true - representation of the resource - """ @classmethod def from_dict(cls, api_client, obj_dict): + """ + Because MaaS decides to replace the VLAN id with the + representation of the VLAN, we must reverse it for a true + representation of the resource + """ refined_dict = {k: obj_dict.get(k, None) for k in cls.fields} if 'id' in obj_dict.keys(): refined_dict['resource_id'] = obj_dict.get('id') diff --git a/drydock_provisioner/drivers/node/maasdriver/models/vlan.py b/drydock_provisioner/drivers/node/maasdriver/models/vlan.py index 36acceac..ffe9711d 100644 --- a/drydock_provisioner/drivers/node/maasdriver/models/vlan.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/vlan.py @@ -19,8 +19,9 @@ import drydock_provisioner.drivers.node.maasdriver.models.base as model_base class Vlan(model_base.ResourceBase): resource_url = 'fabrics/{fabric_id}/vlans/{api_id}/' - fields = ['resource_id', 'name', 'description', 'vid', 'fabric_id', 'dhcp_on', 'mtu'] - json_fields = ['name', 'description', 'vid', 'dhcp_on', 'mtu'] + fields = ['resource_id', 'name', 'description', 'vid', 'fabric_id', 'dhcp_on', 'mtu', + 'primary_rack', 'secondary_rack'] + json_fields = ['name', 'description', 'vid', 'dhcp_on', 'mtu', 'primary_rack', 'secondary_rack'] def __init__(self, api_client, **kwargs): super(Vlan, self).__init__(api_client, **kwargs) diff --git a/drydock_provisioner/ingester/plugins/yaml.py b/drydock_provisioner/ingester/plugins/yaml.py index fce62c32..060ce38a 100644 --- a/drydock_provisioner/ingester/plugins/yaml.py +++ b/drydock_provisioner/ingester/plugins/yaml.py @@ -143,6 +143,8 @@ class YamlIngester(IngesterPlugin): model.trunk_mode = trunking.get('mode', hd_fields.NetworkLinkTrunkingMode.Disabled) model.native_network = trunking.get('default_network', None) + model.allowed_networks = spec.get('allowed_networks', None) + models.append(model) else: raise ValueError('Unknown API version of object') @@ -160,7 +162,7 @@ class YamlIngester(IngesterPlugin): model.cidr = spec.get('cidr', None) model.allocation_strategy = spec.get('allocation', 'static') - model.vlan_id = spec.get('vlan_id', None) + model.vlan_id = spec.get('vlan', None) model.mtu = spec.get('mtu', None) dns = spec.get('dns', {}) @@ -285,7 +287,6 @@ class YamlIngester(IngesterPlugin): int_model.device_name = i.get('device_name', None) int_model.network_link = i.get('device_link', None) - int_model.primary_netowrk = i.get('primary', False) int_model.hardware_slaves = [] slaves = i.get('slaves', []) @@ -301,6 +302,8 @@ class YamlIngester(IngesterPlugin): model.interfaces.append(int_model) + model.primary_network = spec.get('primary_network', None) + node_metadata = spec.get('metadata', {}) metadata_tags = node_metadata.get('tags', []) model.tags = [] diff --git a/drydock_provisioner/objects/hostprofile.py b/drydock_provisioner/objects/hostprofile.py index 36aad003..083a0cfc 100644 --- a/drydock_provisioner/objects/hostprofile.py +++ b/drydock_provisioner/objects/hostprofile.py @@ -51,6 +51,7 @@ class HostProfile(base.DrydockPersistentObject, base.DrydockObject): 'base_os': obj_fields.StringField(nullable=True), 'kernel': obj_fields.StringField(nullable=True), 'kernel_params': obj_fields.StringField(nullable=True), + 'primary_network': obj_fields.StringField(nullable=False), } def __init__(self, **kwargs): @@ -93,7 +94,7 @@ class HostProfile(base.DrydockPersistentObject, base.DrydockObject): 'hardware_profile', 'oob_type', 'oob_network', 'oob_credential', 'oob_account', 'storage_layout', 'bootdisk_device', 'bootdisk_root_size', 'bootdisk_boot_size', - 'rack', 'base_os', 'kernel', 'kernel_params'] + 'rack', 'base_os', 'kernel', 'kernel_params', 'primary_network'] # Create applied data from self design values and parent # applied values @@ -134,7 +135,6 @@ class HostInterface(base.DrydockObject): fields = { 'device_name': obj_fields.StringField(), - 'primary_network': obj_fields.BooleanField(nullable=False, default=False), 'source': hd_fields.ModelSourceField(), 'network_link': obj_fields.StringField(nullable=True), 'hardware_slaves': obj_fields.ListOfStringsField(nullable=True), @@ -212,10 +212,6 @@ class HostInterface(base.DrydockObject): elif j.get_name() == parent_name: m = objects.HostInterface() m.device_name = j.get_name() - m.primary_network = \ - objects.Utils.apply_field_inheritance( - getattr(j, 'primary_network', None), - getattr(i, 'primary_network', None)) m.network_link = \ objects.Utils.apply_field_inheritance( diff --git a/drydock_provisioner/objects/network.py b/drydock_provisioner/objects/network.py index 028161da..65329e71 100644 --- a/drydock_provisioner/objects/network.py +++ b/drydock_provisioner/objects/network.py @@ -44,6 +44,7 @@ class NetworkLink(base.DrydockPersistentObject, base.DrydockObject): 'trunk_mode': hd_fields.NetworkLinkTrunkingModeField( default=hd_fields.NetworkLinkTrunkingMode.Disabled), 'native_network': ovo_fields.StringField(nullable=True), + 'allowed_networks': ovo_fields.ListOfStringsField(), } def __init__(self, **kwargs): @@ -104,8 +105,6 @@ class Network(base.DrydockPersistentObject, base.DrydockObject): return None - - @base.DrydockObjectRegistry.register class NetworkList(base.DrydockObjectListBase, base.DrydockObject): diff --git a/drydock_provisioner/orchestrator/__init__.py b/drydock_provisioner/orchestrator/__init__.py index 3e55cbf2..d893fafe 100644 --- a/drydock_provisioner/orchestrator/__init__.py +++ b/drydock_provisioner/orchestrator/__init__.py @@ -214,6 +214,9 @@ class Orchestrator(object): self.task_field_update(task_id, status=hd_fields.TaskStatus.Running) + # NOTE Should we attempt to interrogate the node via Node Driver to see if + # it is in a deployed state before we start rebooting? Or do we just leverage + # Drydock internal state via site build data (when implemented)? oob_driver = self.enabled_drivers['oob'] if oob_driver is None: @@ -357,6 +360,61 @@ class Orchestrator(object): result=final_result) return + elif task.action == hd_fields.OrchestratorAction.DeployNode: + failed = worked = False + + self.task_field_update(task_id, + status=hd_fields.TaskStatus.Running) + + node_driver = self.enabled_drivers['node'] + + if node_driver is None: + self.task_field_update(task_id, + status=hd_fields.TaskStatus.Errored, + result=hd_fields.ActionResult.Failure, + result_detail={'detail': 'Error: No node driver configured', 'retry': False}) + return + + site_design = self.get_effective_site(design_id) + + node_filter = task.node_filter + + target_nodes = self.process_node_filter(node_filter, site_design) + + target_names = [x.get_name() for x in target_nodes] + + task_scope = {'site' : task_site, + 'node_names' : target_names} + + node_networking_task = self.create_task(tasks.DriverTask, + parent_task_id=task.get_id(), design_id=design_id, + action=hd_fields.OrchestratorAction.ApplyNodeNetworking, + task_scope=task_scope) + + self.logger.info("Starting node driver task %s to apply networking on nodes." % (node_networking_task.get_id())) + node_driver.execute_task(node_networking_task.get_id()) + + node_networking_task = self.state_manager.get_task(node_networking_task.get_id()) + + if node_networking_task.get_result() in [hd_fields.ActionResult.Success, + hd_fields.ActionResult.PartialSuccess]: + worked = True + elif node_networking_task.get_result() in [hd_fields.ActionResult.Failure, + hd_fields.ActionResult.PartialSuccess]: + failed = True + + final_result = None + if worked and failed: + final_result = hd_fields.ActionResult.PartialSuccess + elif worked: + final_result = hd_fields.ActionResult.Success + else: + final_result = hd_fields.ActionResult.Failure + + self.task_field_update(task_id, + status=hd_fields.TaskStatus.Complete, + result=final_result) + else: raise errors.OrchestratorError("Action %s not supported" % (task.action)) diff --git a/drydock_provisioner/orchestrator/readme.md b/drydock_provisioner/orchestrator/readme.md index abb48068..4cdb852b 100644 --- a/drydock_provisioner/orchestrator/readme.md +++ b/drydock_provisioner/orchestrator/readme.md @@ -27,7 +27,8 @@ is compatible with the physical state of the site. * All baremetal nodes have an address, either static or DHCP, for all networks they are attached to. * No static IP assignments are duplicated * No static IP assignments are outside of the network they are targetted for -* No network MTU mismatches due to a network riding different links on different nodes +* All IP assignments are within declared ranges on the network +* Networks assigned to each node's interface are within the set of of the attached link's allowed_networks * Boot drive is above minimum size ### VerifySite ### diff --git a/examples/designparts_v1.0.yaml b/examples/designparts_v1.0.yaml index 1f669fab..0ca7011c 100644 --- a/examples/designparts_v1.0.yaml +++ b/examples/designparts_v1.0.yaml @@ -67,6 +67,9 @@ spec: mode: disabled # If disabled, what network is this port on. If '802.1q' what is the default network for the port. No default. default_network: oob + # List of Network names that are supported on this link. A Network can be listed on only one NetworkLink + allowed_networks: + - 'oob' --- apiVersion: 'v1.0' kind: Network @@ -93,7 +96,7 @@ spec: # Defined IP address ranges. All node IP address assignments must fall into a defined range # of the correct type ranges: - # Type of range. Supports 'static' or 'dhcp'. No default + # Type of range. Supports 'reserved', 'static' or 'dhcp'. No default - type: 'dhcp' # Start of the address range, inclusive. No default start: '172.16.1.100' @@ -202,14 +205,14 @@ spec: fs_uuid: # A filesystem label. Defaults to None fs_label: + # Network name of the primary network (default gateway, DNS, etc...) + primary_network: 'mgmt' # Physical and logical network interfaces interfaces: # What the interface should be named in the operating system. May not match a hardware device name device_name: bond0 # The NetworkLink connected to this interface. Must be the name of a NetworkLink design part device_link: 'gp' - # Whether this interface is considered the primary interface on the server. Supports true and false. Defaults to false - primary: true # Hardware devices that support this interface. For configurating a physical device, this would be a list of one # For bonds, this would be a list of all the physical devices in the bond. These can refer to HardwareProfile device aliases # or explicit device names From b803875c903c908000c20eddb685d5838ce735fa Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Fri, 16 Jun 2017 09:34:36 -0500 Subject: [PATCH 06/16] Add another design verification facet --- drydock_provisioner/orchestrator/readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/drydock_provisioner/orchestrator/readme.md b/drydock_provisioner/orchestrator/readme.md index 4cdb852b..2bc1d25f 100644 --- a/drydock_provisioner/orchestrator/readme.md +++ b/drydock_provisioner/orchestrator/readme.md @@ -29,6 +29,7 @@ is compatible with the physical state of the site. * No static IP assignments are outside of the network they are targetted for * All IP assignments are within declared ranges on the network * Networks assigned to each node's interface are within the set of of the attached link's allowed_networks +* No network is allowed on multiple network links * Boot drive is above minimum size ### VerifySite ### From 3c3ecd83f3c33692981d1c66dc3986dfdaf8bffa Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Sat, 17 Jun 2017 10:15:26 -0500 Subject: [PATCH 07/16] DRYD-2 MVP Final Phase - Node deployment Add driver task for DeployNode Add a OOB driver for manual operation to aid in using VMs for testing Drydock Add boot_mac field to YAML schema for identifying a VM (no IPMI IP) --- .../drivers/node/maasdriver/api_client.py | 2 +- .../drivers/node/maasdriver/driver.py | 130 ++++++++++- .../node/maasdriver/models/interface.py | 2 +- .../drivers/node/maasdriver/models/machine.py | 112 ++++++++-- drydock_provisioner/drivers/oob/__init__.py | 15 ++ .../drivers/oob/manual_driver/__init__.py | 13 ++ .../drivers/oob/manual_driver/driver.py | 74 +++++++ .../drivers/oob/pyghmi_driver/__init__.py | 2 + drydock_provisioner/ingester/plugins/yaml.py | 12 +- drydock_provisioner/objects/hostprofile.py | 9 +- drydock_provisioner/objects/node.py | 3 +- drydock_provisioner/orchestrator/__init__.py | 209 ++++++++++++------ setup.py | 1 + 13 files changed, 487 insertions(+), 97 deletions(-) create mode 100644 drydock_provisioner/drivers/oob/manual_driver/__init__.py create mode 100644 drydock_provisioner/drivers/oob/manual_driver/driver.py diff --git a/drydock_provisioner/drivers/node/maasdriver/api_client.py b/drydock_provisioner/drivers/node/maasdriver/api_client.py index fa463109..01930a4e 100644 --- a/drydock_provisioner/drivers/node/maasdriver/api_client.py +++ b/drydock_provisioner/drivers/node/maasdriver/api_client.py @@ -100,7 +100,7 @@ class MaasRequestFactory(object): if 'Accept' not in headers.keys(): headers['Accept'] = 'application/json' - if 'files' in kwargs.keys(): + if kwargs.get('files', None) is not None: files = kwargs.pop('files') files_tuples = {} diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index 9da0f9c3..90cc3c76 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -362,6 +362,77 @@ class MaasNodeDriver(NodeDriver): status=hd_fields.TaskStatus.Complete, result=result, result_detail=result_detail) + elif task.action ==hd_fields.OrchestratorAction.DeployNode: + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Running) + + self.logger.debug("Starting subtask to deploy %s nodes." % (len(task.node_list))) + + subtasks = [] + + result_detail = { + 'detail': [], + 'failed_nodes': [], + 'successful_nodes': [], + } + + for n in task.node_list: + subtask = self.orchestrator.create_task(task_model.DriverTask, + parent_task_id=task.get_id(), design_id=design_id, + action=hd_fields.OrchestratorAction.DeployNode, + site_name=task.site_name, + task_scope={'site': task.site_name, 'node_names': [n]}) + runner = MaasTaskRunner(state_manager=self.state_manager, + orchestrator=self.orchestrator, + task_id=subtask.get_id(),config=self.config) + + self.logger.info("Starting thread for task %s to deploy node %s" % (subtask.get_id(), n)) + + runner.start() + subtasks.append(subtask.get_id()) + + running_subtasks = len(subtasks) + attempts = 0 + worked = failed = False + + #TODO Add timeout to config + while running_subtasks > 0 and attempts < 30: + for t in subtasks: + subtask = self.state_manager.get_task(t) + + if subtask.status == hd_fields.TaskStatus.Complete: + self.logger.info("Task %s to deploy node %s complete - status %s" % + (subtask.get_id(), n, subtask.get_result())) + running_subtasks = running_subtasks - 1 + + if subtask.result == hd_fields.ActionResult.Success: + result_detail['successful_nodes'].extend(subtask.node_list) + worked = True + elif subtask.result == hd_fields.ActionResult.Failure: + result_detail['failed_nodes'].extend(subtask.node_list) + failed = True + elif subtask.result == hd_fields.ActionResult.PartialSuccess: + worked = failed = True + + time.sleep(1 * 60) + attempts = attempts + 1 + + if running_subtasks > 0: + self.logger.warning("Time out for task %s before all subtask threads complete" % (task.get_id())) + result = hd_fields.ActionResult.DependentFailure + result_detail['detail'].append('Some subtasks did not complete before the timeout threshold') + elif worked and failed: + result = hd_fields.ActionResult.PartialSuccess + elif worked: + result = hd_fields.ActionResult.Success + else: + result = hd_fields.ActionResult.Failure + + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=result, + result_detail=result_detail) + class MaasTaskRunner(drivers.DriverTaskRunner): def __init__(self, config=None, **kwargs): @@ -469,7 +540,7 @@ class MaasTaskRunner(drivers.DriverTaskRunner): % (vlan.resource_id, n.name)) else: # Create a new VLAN in this fabric and assign subnet to it - vlan = maas_vlan.Vlan(self.maas_client, name=n.name, vid=vlan_id, + vlan = maas_vlan.Vlan(self.maas_client, name=n.name, vid=n.vlan_id, mtu=getattr(n, 'mtu', None),fabric_id=fabric.resource_id) vlan = vlan_list.add(vlan) @@ -660,7 +731,7 @@ class MaasTaskRunner(drivers.DriverTaskRunner): node = site_design.get_baremetal_node(n) machine = machine_list.identify_baremetal_node(node, update_name=False) if machine is not None: - if machine.status_name == ['New', 'Broken']: + if machine.status_name in ['New', 'Broken']: self.logger.debug("Located node %s in MaaS, starting commissioning" % (n)) machine.commission() @@ -854,3 +925,58 @@ class MaasTaskRunner(drivers.DriverTaskRunner): status=hd_fields.TaskStatus.Complete, result=final_result, result_detail=result_detail) + elif task_action == hd_fields.OrchestratorAction.DeployNode: + try: + machine_list = maas_machine.Machines(self.maas_client) + machine_list.refresh() + + fabrics = maas_fabric.Fabrics(self.maas_client) + fabrics.refresh() + + subnets = maas_subnet.Subnets(self.maas_client) + subnets.refresh() + except Exception as ex: + self.logger.error("Error deploying node, cannot access MaaS: %s" % str(ex)) + traceback.print_tb(sys.last_traceback) + self.orchestrator.task_field_update(self.task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=hd_fields.ActionResult.Failure, + result_detail={'detail': 'Error accessing MaaS API', 'retry': True}) + return + + nodes = self.task.node_list + + result_detail = {'detail': []} + + worked = failed = False + + for n in nodes: + self.logger.info("Acquiring node %s for deployment" % (n)) + + try: + machine = machine_list.acquire_node(n) + except DriverError as dex: + self.logger.warning("Error acquiring node %s, skipping" % n) + failed = True + continue + + self.logger.info("Deploying node %s" % (n)) + + try: + machine.deploy() + except DriverError as dex: + self.logger.warning("Error deploying node %s, skipping" % n) + failed = True + continue + + self.logger.info("Node %s deployed" % (n)) + + if failed: + final_result = hd_fields.ActionResult.Failure + else: + final_result = hd_fields.ActionResult.Success + + self.orchestrator.task_field_update(self.task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=final_result, + result_detail=result_detail) diff --git a/drydock_provisioner/drivers/node/maasdriver/models/interface.py b/drydock_provisioner/drivers/node/maasdriver/models/interface.py index db2a9d85..135f1204 100644 --- a/drydock_provisioner/drivers/node/maasdriver/models/interface.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/interface.py @@ -225,7 +225,7 @@ class Interfaces(model_base.ResourceCollectionBase): if exists is not None: self.logger.info("Interface for VLAN %s already exists on node %s, skipping" % (vlan_tag, self.system_id)) - return None + return exists url = self.interpolate_url() diff --git a/drydock_provisioner/drivers/node/maasdriver/models/machine.py b/drydock_provisioner/drivers/node/maasdriver/models/machine.py index 088ca986..aca826fd 100644 --- a/drydock_provisioner/drivers/node/maasdriver/models/machine.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/machine.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import drydock_provisioner.error as errors import drydock_provisioner.drivers.node.maasdriver.models.base as model_base import drydock_provisioner.drivers.node.maasdriver.models.interface as maas_interface @@ -22,7 +23,7 @@ class Machine(model_base.ResourceBase): resource_url = 'machines/{resource_id}/' fields = ['resource_id', 'hostname', 'power_type', 'power_state', 'power_parameters', 'interfaces', - 'boot_interface', 'memory', 'cpu_count', 'tag_names', 'status_name'] + 'boot_interface', 'memory', 'cpu_count', 'tag_names', 'status_name', 'boot_mac'] json_fields = ['hostname', 'power_type'] def __init__(self, api_client, **kwargs): @@ -55,7 +56,30 @@ class Machine(model_base.ResourceBase): # Need to sort out how to handle exceptions if not resp.ok: - raise Exception() + self.logger.error("Error commissioning node, received HTTP %s from MaaS" % resp.status_code) + self.logger.debug("MaaS response: %s" % resp.text) + raise errors.DriverError("Error commissioning node, received HTTP %s from MaaS" % resp.status_code) + + def deploy(self, user_data=None, platform=None, kernel=None): + deploy_options = {} + + if user_data is not None: + deploy_options['user_data'] = user_data + + if platform is not None: + deploy_options['distro_series'] = platform + + if kernel is not None: + deploy_options['hwe_kernel'] = kernel + + url = self.interpolate_url() + resp = self.api_client.post(url, op='deploy', + files=deploy_options if len(deploy_options) > 0 else None) + + if not resp.ok: + self.logger.error("Error deploying node, received HTTP %s from MaaS" % resp.status_code) + self.logger.debug("MaaS response: %s" % resp.text) + raise errors.DriverError("Error deploying node, received HTTP %s from MaaS" % resp.status_code) def get_network_interface(self, iface_name): if self.interfaces is not None: @@ -106,6 +130,11 @@ class Machine(model_base.ResourceBase): if 'system_id' in obj_dict.keys(): refined_dict['resource_id'] = obj_dict.get('system_id') + # Capture the boot interface MAC to allow for node id of VMs + if 'boot_interface' in obj_dict.keys(): + if isinstance(obj_dict['boot_interface'], dict): + refined_dict['boot_mac'] = obj_dict['boot_interface']['mac_address'] + i = cls(api_client, **refined_dict) return i @@ -122,6 +151,37 @@ class Machines(model_base.ResourceCollectionBase): for k, v in self.resources.items(): v.get_power_params() + def acquire_node(self, node_name): + """ + Acquire a commissioned node fro deployment + + :param node_name: The hostname of a node to acquire + """ + + self.refresh() + + node = self.singleton({'hostname': node_name}) + + if node is None: + self.logger.info("Node %s not found" % (node_name)) + raise errors.DriverError("Node %s not found" % (node_name)) + + if node.status_name != 'Ready': + self.logger.info("Node %s status '%s' does not allow deployment, should be 'Ready'." % + (node_name, node.status_name)) + raise errors.DriverError("Node %s status '%s' does not allow deployment, should be 'Ready'." % + (node_name, node.status_name)) + + url = self.interpolate_url() + + resp = self.api_client.post(url, op='allocate', files={'system_id': node.resource_id}) + + if not resp.ok: + self.logger.error("Error acquiring node, MaaS returned %s" % resp.status_code) + self.logger.debug("MaaS response: %s" % resp.text) + raise errors.DriverError("Error acquiring node, MaaS returned %s" % resp.status_code) + + return node def identify_baremetal_node(self, node_model, update_name=True): """ @@ -132,30 +192,44 @@ class Machines(model_base.ResourceCollectionBase): :param node_model: Instance of objects.node.BaremetalNode to search MaaS for matching resource :param update_name: Whether Drydock should update the MaaS resource name to match the Drydock design """ - node_oob_network = node_model.oob_network - node_oob_ip = node_model.get_network_address(node_oob_network) + + maas_node = None - if node_oob_ip is None: - self.logger.warn("Node model missing OOB IP address") - raise ValueError('Node model missing OOB IP address') + if node_model.oob_type == 'ipmi': + node_oob_network = node_model.oob_network + node_oob_ip = node_model.get_network_address(node_oob_network) - try: - self.collect_power_params() + if node_oob_ip is None: + self.logger.warn("Node model missing OOB IP address") + raise ValueError('Node model missing OOB IP address') - maas_node = self.singleton({'power_params.power_address': node_oob_ip}) + try: + self.collect_power_params() - self.logger.debug("Found MaaS resource %s matching Node %s" % (maas_node.resource_id, node_model.get_id())) + maas_node = self.singleton({'power_params.power_address': node_oob_ip}) + except ValueError as ve: + self.logger.warn("Error locating matching MaaS resource for OOB IP %s" % (node_oob_ip)) + return None + else: + # Use boot_mac for node's not using IPMI + node_boot_mac = node_model.boot_mac - if maas_node.hostname != node_model.name and update_name: - maas_node.hostname = node_model.name - maas_node.update() - self.logger.debug("Updated MaaS resource %s hostname to %s" % (maas_node.resource_id, node_model.name)) + if node_boot_mac is not None: + maas_node = self.singleton({'boot_mac': node_model.boot_mac}) - return maas_node - except ValueError as ve: - self.logger.warn("Error locating matching MaaS resource for OOB IP %s" % (node_oob_ip)) + if maas_node is None: + self.logger.info("Could not locate node %s in MaaS" % node_model.name) return None + self.logger.debug("Found MaaS resource %s matching Node %s" % (maas_node.resource_id, node_model.get_id())) + + if maas_node.hostname != node_model.name and update_name: + maas_node.hostname = node_model.name + maas_node.update() + self.logger.debug("Updated MaaS resource %s hostname to %s" % (maas_node.resource_id, node_model.name)) + + return maas_node + def query(self, query): """ Custom query method to deal with complex fields @@ -190,4 +264,4 @@ class Machines(model_base.ResourceCollectionBase): return res raise errors.DriverError("Failed updating MAAS url %s - return code %s" - % (url, resp.status_code)) \ No newline at end of file + % (url, resp.status_code)) diff --git a/drydock_provisioner/drivers/oob/__init__.py b/drydock_provisioner/drivers/oob/__init__.py index 8e7a9c26..2dc1d13f 100644 --- a/drydock_provisioner/drivers/oob/__init__.py +++ b/drydock_provisioner/drivers/oob/__init__.py @@ -19,6 +19,8 @@ from drydock_provisioner.drivers import ProviderDriver class OobDriver(ProviderDriver): + oob_types_supported = [''] + def __init__(self, **kwargs): super(OobDriver, self).__init__(**kwargs) @@ -43,3 +45,16 @@ class OobDriver(ProviderDriver): else: raise DriverError("Unsupported action %s for driver %s" % (task_action, self.driver_desc)) + + @classmethod + def oob_type_support(cls, type_string): + """ + Does this driver support a particular OOB type + + :param type_string: OOB type to check + """ + + if type_string in cls.oob_types_supported: + return True + + return False \ No newline at end of file diff --git a/drydock_provisioner/drivers/oob/manual_driver/__init__.py b/drydock_provisioner/drivers/oob/manual_driver/__init__.py new file mode 100644 index 00000000..2a385a45 --- /dev/null +++ b/drydock_provisioner/drivers/oob/manual_driver/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2017 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. \ No newline at end of file diff --git a/drydock_provisioner/drivers/oob/manual_driver/driver.py b/drydock_provisioner/drivers/oob/manual_driver/driver.py new file mode 100644 index 00000000..e53f9e30 --- /dev/null +++ b/drydock_provisioner/drivers/oob/manual_driver/driver.py @@ -0,0 +1,74 @@ +# Copyright 2017 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. +import time +import logging + +import drydock_provisioner.error as errors +import drydock_provisioner.config as config + +import drydock_provisioner.objects.fields as hd_fields +import drydock_provisioner.objects.task as task_model + +import drydock_provisioner.drivers.oob as oob +import drydock_provisioner.drivers as drivers + + +class ManualDriver(oob.OobDriver): + + oob_types_supported = ['manual'] + + def __init__(self, **kwargs): + super(ManualDriver, self).__init__(**kwargs) + + self.driver_name = "manual_driver" + self.driver_key = "manual_driver" + self.driver_desc = "Manual (Noop) OOB Driver" + + self.logger = logging.getLogger('drydock.oobdriver.pyghmi') + self.config = config.DrydockConfig.node_driver.get(self.driver_key, {}) + + def execute_task(self, task_id): + task = self.state_manager.get_task(task_id) + + if task is None: + self.logger.error("Invalid task %s" % (task_id)) + raise errors.DriverError("Invalid task %s" % (task_id)) + + if task.action not in self.supported_actions: + self.logger.error("Driver %s doesn't support task action %s" + % (self.driver_desc, task.action)) + raise errors.DriverError("Driver %s doesn't support task action %s" + % (self.driver_desc, task.action)) + + design_id = getattr(task, 'design_id', None) + + if design_id is None: + raise errors.DriverError("No design ID specified in task %s" % + (task_id)) + + + if task.site_name is None: + raise errors.DriverError("Not site specified for task %s." % + (task_id)) + + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Running) + + self.logger.info("Sleeping 60s to allow time for manual OOB %s action" % task.action) + + time.sleep(60) + + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=hd_fields.ActionResult.Success) diff --git a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py index 9010fbcf..655241ac 100644 --- a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py +++ b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py @@ -28,6 +28,8 @@ import drydock_provisioner.drivers as drivers class PyghmiDriver(oob.OobDriver): + oob_types_supported = ['ipmi'] + def __init__(self, **kwargs): super(PyghmiDriver, self).__init__(**kwargs) diff --git a/drydock_provisioner/ingester/plugins/yaml.py b/drydock_provisioner/ingester/plugins/yaml.py index 060ce38a..b179bb99 100644 --- a/drydock_provisioner/ingester/plugins/yaml.py +++ b/drydock_provisioner/ingester/plugins/yaml.py @@ -247,10 +247,12 @@ class YamlIngester(IngesterPlugin): oob = spec.get('oob', {}) - model.oob_type = oob.get('type', None) - model.oob_network = oob.get('network', None) - model.oob_account = oob.get('account', None) - model.oob_credential = oob.get('credential', None) + model.oob_parameters = {} + for k,v in oob.items(): + if k == 'type': + model.oob_type = oob.get('type', None) + else: + model.oob_parameters[k] = v storage = spec.get('storage', {}) model.storage_layout = storage.get('layout', 'lvm') @@ -320,6 +322,8 @@ class YamlIngester(IngesterPlugin): model.rack = node_metadata.get('rack', None) if kind == 'BaremetalNode': + model.boot_mac = node_metadata.get('boot_mac', None) + addresses = spec.get('addressing', []) if len(addresses) == 0: diff --git a/drydock_provisioner/objects/hostprofile.py b/drydock_provisioner/objects/hostprofile.py index 083a0cfc..bc367932 100644 --- a/drydock_provisioner/objects/hostprofile.py +++ b/drydock_provisioner/objects/hostprofile.py @@ -33,9 +33,7 @@ class HostProfile(base.DrydockPersistentObject, base.DrydockObject): 'parent_profile': obj_fields.StringField(nullable=True), 'hardware_profile': obj_fields.StringField(nullable=True), 'oob_type': obj_fields.StringField(nullable=True), - 'oob_network': obj_fields.StringField(nullable=True), - 'oob_account': obj_fields.StringField(nullable=True), - 'oob_credential': obj_fields.StringField(nullable=True), + 'oob_parameters': obj_fields.DictOfStringsField(nullable=True), 'storage_layout': obj_fields.StringField(nullable=True), 'bootdisk_device': obj_fields.StringField(nullable=True), # Consider a custom field for storage size @@ -91,8 +89,7 @@ class HostProfile(base.DrydockPersistentObject, base.DrydockObject): # First compute inheritance for simple fields inheritable_field_list = [ - 'hardware_profile', 'oob_type', 'oob_network', - 'oob_credential', 'oob_account', 'storage_layout', + 'hardware_profile', 'oob_type', 'storage_layout', 'bootdisk_device', 'bootdisk_root_size', 'bootdisk_boot_size', 'rack', 'base_os', 'kernel', 'kernel_params', 'primary_network'] @@ -105,6 +102,8 @@ class HostProfile(base.DrydockPersistentObject, base.DrydockObject): getattr(parent, f, None))) # Now compute inheritance for complex types + self.oob_parameters = objects.Utils.merge_dicts(self.oob_parameters, parent.oob_parameters) + self.tags = objects.Utils.merge_lists(self.tags, parent.tags) self.owner_data = objects.Utils.merge_dicts(self.owner_data, parent.owner_data) diff --git a/drydock_provisioner/objects/node.py b/drydock_provisioner/objects/node.py index 7854b346..7f7c1f57 100644 --- a/drydock_provisioner/objects/node.py +++ b/drydock_provisioner/objects/node.py @@ -31,7 +31,8 @@ class BaremetalNode(drydock_provisioner.objects.hostprofile.HostProfile): VERSION = '1.0' fields = { - 'addressing': ovo_fields.ObjectField('IpAddressAssignmentList') + 'addressing': ovo_fields.ObjectField('IpAddressAssignmentList'), + 'boot_mac': ovo_fields.StringField(nullable=True), } # A BaremetalNode is really nothing more than a physical diff --git a/drydock_provisioner/orchestrator/__init__.py b/drydock_provisioner/orchestrator/__init__.py index d893fafe..8d86cff9 100644 --- a/drydock_provisioner/orchestrator/__init__.py +++ b/drydock_provisioner/orchestrator/__init__.py @@ -37,14 +37,18 @@ class Orchestrator(object): self.logger = logging.getLogger('drydock.orchestrator') if enabled_drivers is not None: - oob_driver_name = enabled_drivers.get('oob', None) - if oob_driver_name is not None: - m, c = oob_driver_name.rsplit('.', 1) - oob_driver_class = \ - getattr(importlib.import_module(m), c, None) - if oob_driver_class is not None: - self.enabled_drivers['oob'] = oob_driver_class(state_manager=state_manager, - orchestrator=self) + oob_drivers = enabled_drivers.get('oob', []) + + for d in oob_drivers: + if d is not None: + m, c = d.rsplit('.', 1) + oob_driver_class = \ + getattr(importlib.import_module(m), c, None) + if oob_driver_class is not None: + if self.enabled_drivers.get('oob', None) is None: + self.enabled_drivers['oob'] = [] + self.enabled_drivers['oob'].append(oob_driver_class(state_manager=state_manager, + orchestrator=self)) node_driver_name = enabled_drivers.get('node', None) if node_driver_name is not None: @@ -174,39 +178,75 @@ class Orchestrator(object): self.task_field_update(task_id, status=hd_fields.TaskStatus.Running) - oob_driver = self.enabled_drivers['oob'] - - if oob_driver is None: - self.task_field_update(task_id, - status=hd_fields.TaskStatus.Errored, - result=hd_fields.ActionResult.Failure, - result_detail={'detail': 'Error: No oob driver configured', 'retry': False}) - return - site_design = self.get_effective_site(design_id) node_filter = task.node_filter + oob_type_partition = {} + target_nodes = self.process_node_filter(node_filter, site_design) - target_names = [x.get_name() for x in target_nodes] + for n in target_nodes: + if n.oob_type not in oob_type_partition.keys(): + oob_type_partition[n.oob_type] = [] - task_scope = {'site' : task_site, - 'node_names' : target_names} + oob_type_partition[n.oob_type].append(n) - oob_driver_task = self.create_task(tasks.DriverTask, + result_detail = {'detail': []} + worked = failed = False + + # TODO Need to multithread tasks for different OOB types + for oob_type, oob_nodes in oob_type_partition.items(): + oob_driver = None + for d in self.enabled_drivers['oob']: + if d.oob_type_support(oob_type): + oob_driver = d + break + + if oob_driver is None: + self.logger.warning("Node OOB type %s has no enabled driver." % oob_type) + result_detail['detail'].append("Error: No oob driver configured for type %s" % oob_type) + continue + + + target_names = [x.get_name() for x in oob_nodes] + + task_scope = {'site' : task_site, + 'node_names' : target_names} + + oob_driver_task = self.create_task(tasks.DriverTask, parent_task_id=task.get_id(), design_id=design_id, action=hd_fields.OrchestratorAction.InterrogateOob, task_scope=task_scope) - oob_driver.execute_task(oob_driver_task.get_id()) + self.logger.info("Starting task %s for node verification via OOB type %s" % + (oob_driver_task.get_id(), oob_type)) - oob_driver_task = self.state_manager.get_task(oob_driver_task.get_id()) + oob_driver.execute_task(oob_driver_task.get_id()) + + oob_driver_task = self.state_manager.get_task(oob_driver_task.get_id()) + + if oob_driver_task.get_result() in [hd_fields.ActionResult.Success, + hd_fields.ActionResult.PartialSuccess]: + worked = True + if oob_driver_task.get_result() in [hd_fields.ActionResult.Failure, + hd_fields.ActionResult.PartialSuccess]: + failed = True + + final_result = None + + if worked and failed: + final_result = hd_fields.ActionResult.PartialSuccess + elif worked: + final_result = hd_fields.ActionResult.Success + else: + final_result = hd_fields.ActionResult.Failure self.task_field_update(task_id, status=hd_fields.TaskStatus.Complete, - result=oob_driver_task.get_result()) + result=final_result, + result_detail=result_detail) return elif task.action == hd_fields.OrchestratorAction.PrepareNode: failed = worked = False @@ -217,15 +257,6 @@ class Orchestrator(object): # NOTE Should we attempt to interrogate the node via Node Driver to see if # it is in a deployed state before we start rebooting? Or do we just leverage # Drydock internal state via site build data (when implemented)? - oob_driver = self.enabled_drivers['oob'] - - if oob_driver is None: - self.task_field_update(task_id, - status=hd_fields.TaskStatus.Errored, - result=hd_fields.ActionResult.Failure, - result_detail={'detail': 'Error: No oob driver configured', 'retry': False}) - return - node_driver = self.enabled_drivers['node'] if node_driver is None: @@ -241,53 +272,79 @@ class Orchestrator(object): target_nodes = self.process_node_filter(node_filter, site_design) - target_names = [x.get_name() for x in target_nodes] + oob_type_partition = {} - task_scope = {'site' : task_site, - 'node_names' : target_names} + for n in target_nodes: + if n.oob_type not in oob_type_partition.keys(): + oob_type_partition[n.oob_type] = [] - setboot_task = self.create_task(tasks.DriverTask, - parent_task_id=task.get_id(), - design_id=design_id, - action=hd_fields.OrchestratorAction.SetNodeBoot, - task_scope=task_scope) + oob_type_partition[n.oob_type].append(n) - self.logger.info("Starting OOB driver task %s to set PXE boot" % (setboot_task.get_id())) + result_detail = {'detail': []} + worked = failed = False - oob_driver.execute_task(setboot_task.get_id()) + # TODO Need to multithread tasks for different OOB types + for oob_type, oob_nodes in oob_type_partition.items(): + oob_driver = None + for d in self.enabled_drivers['oob']: + if d.oob_type_support(oob_type): + oob_driver = d + break - self.logger.info("OOB driver task %s complete" % (setboot_task.get_id())) + if oob_driver is None: + self.logger.warning("Node OOB type %s has no enabled driver." % oob_type) + result_detail['detail'].append("Error: No oob driver configured for type %s" % oob_type) + continue + - setboot_task = self.state_manager.get_task(setboot_task.get_id()) + target_names = [x.get_name() for x in oob_nodes] - if setboot_task.get_result() == hd_fields.ActionResult.Success: - worked = True - elif setboot_task.get_result() == hd_fields.ActionResult.PartialSuccess: - worked = failed = True - elif setboot_task.get_result() == hd_fields.ActionResult.Failure: - failed = True + task_scope = {'site' : task_site, + 'node_names' : target_names} - cycle_task = self.create_task(tasks.DriverTask, + setboot_task = self.create_task(tasks.DriverTask, + parent_task_id=task.get_id(), + design_id=design_id, + action=hd_fields.OrchestratorAction.SetNodeBoot, + task_scope=task_scope) + + self.logger.info("Starting OOB driver task %s to set PXE boot for OOB type %s" % + (setboot_task.get_id(), oob_type)) + + oob_driver.execute_task(setboot_task.get_id()) + + self.logger.info("OOB driver task %s complete" % (setboot_task.get_id())) + + setboot_task = self.state_manager.get_task(setboot_task.get_id()) + + if setboot_task.get_result() == hd_fields.ActionResult.Success: + worked = True + elif setboot_task.get_result() == hd_fields.ActionResult.PartialSuccess: + worked = failed = True + elif setboot_task.get_result() == hd_fields.ActionResult.Failure: + failed = True + + cycle_task = self.create_task(tasks.DriverTask, parent_task_id=task.get_id(), design_id=design_id, action=hd_fields.OrchestratorAction.PowerCycleNode, task_scope=task_scope) - self.logger.info("Starting OOB driver task %s to power cycle nodes" % (cycle_task.get_id())) + self.logger.info("Starting OOB driver task %s to power cycle nodes for OOB type %s" % + (cycle_task.get_id(), oob_type)) - oob_driver.execute_task(cycle_task.get_id()) + oob_driver.execute_task(cycle_task.get_id()) - self.logger.info("OOB driver task %s complete" % (cycle_task.get_id())) + self.logger.info("OOB driver task %s complete" % (cycle_task.get_id())) - cycle_task = self.state_manager.get_task(cycle_task.get_id()) - - if cycle_task.get_result() == hd_fields.ActionResult.Success: - worked = True - elif cycle_task.get_result() == hd_fields.ActionResult.PartialSuccess: - worked = failed = True - elif cycle_task.get_result() == hd_fields.ActionResult.Failure: - failed = True + cycle_task = self.state_manager.get_task(cycle_task.get_id()) + if cycle_task.get_result() == hd_fields.ActionResult.Success: + worked = True + elif cycle_task.get_result() == hd_fields.ActionResult.PartialSuccess: + worked = failed = True + elif cycle_task.get_result() == hd_fields.ActionResult.Failure: + failed = True # IdentifyNode success will take some time after PowerCycleNode finishes # Retry the operation a few times if it fails before considering it a final failure @@ -402,7 +459,31 @@ class Orchestrator(object): elif node_networking_task.get_result() in [hd_fields.ActionResult.Failure, hd_fields.ActionResult.PartialSuccess]: failed = True - + + + if len(node_networking_task.result_detail['successful_nodes']) > 0: + self.logger.info("Found %s successfully networked nodes, starting deployment." % + (len(node_networking_task.result_detail['successful_nodes']))) + node_deploy_task = self.create_task(tasks.DriverTask, + parent_task_id=task.get_id(), design_id=design_id, + action=hd_fields.OrchestratorAction.DeployNode, + task_scope={'site': task_site, + 'node_names': node_networking_task.result_detail['successful_nodes']}) + + self.logger.info("Starting node driver task %s to deploy nodes." % (node_deploy_task.get_id())) + node_driver.execute_task(node_deploy_task.get_id()) + + node_deploy_task = self.state_manager.get_task(node_deploy_task.get_id()) + + if node_deploy_task.get_result() in [hd_fields.ActionResult.Success, + hd_fields.ActionResult.PartialSuccess]: + worked = True + elif node_deploy_task.get_result() in [hd_fields.ActionResult.Failure, + hd_fields.ActionResult.PartialSuccess]: + failed = True + else: + self.logger.warning("No nodes successfully networked, skipping deploy subtask") + final_result = None if worked and failed: final_result = hd_fields.ActionResult.PartialSuccess diff --git a/setup.py b/setup.py index b090c19c..3aa5c1ae 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ setup(name='drydock_provisioner', 'drydock_provisioner.drivers', 'drydock_provisioner.drivers.oob', 'drydock_provisioner.drivers.oob.pyghmi_driver', + 'drydock_provisioner.drivers.oob.manual_driver', 'drydock_provisioner.drivers.node', 'drydock_provisioner.drivers.node.maasdriver', 'drydock_provisioner.drivers.node.maasdriver.models', From 3f2f734b1e733ecec3b303582a9da3d41e1d58dd Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Sun, 18 Jun 2017 20:06:28 -0500 Subject: [PATCH 08/16] Updates to fix issues found in testing --- .../drivers/node/maasdriver/driver.py | 33 +++++++++++++++---- .../node/maasdriver/models/interface.py | 8 +++-- .../drivers/node/maasdriver/models/subnet.py | 2 +- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index 90cc3c76..4984527d 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -396,7 +396,7 @@ class MaasNodeDriver(NodeDriver): worked = failed = False #TODO Add timeout to config - while running_subtasks > 0 and attempts < 30: + while running_subtasks > 0 and attempts < 120: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -738,7 +738,7 @@ class MaasTaskRunner(drivers.DriverTaskRunner): # Poll machine status attempts = 0 - while attempts < 20 and machine.status_name != 'Ready': + while attempts < 30 and machine.status_name != 'Ready': attempts = attempts + 1 time.sleep(1 * 60) try: @@ -881,13 +881,13 @@ class MaasTaskRunner(drivers.DriverTaskRunner): found = False for a in getattr(node, 'addressing', []): if a.network == iface_net: - link_options['ip_address'] = None if a.address == 'dhcp' else a.address + link_options['ip_address'] = 'dhcp' if a.address == 'dhcp' else a.address found = True if not found: - self.logger.error("No addressed assigned to network %s for node %s, cannot link." % + self.logger.error("No addressed assigned to network %s for node %s, link is L2 only." % (iface_net, node.name)) - continue + link_options['ip_address'] = None self.logger.debug("Linking system %s interface %s to subnet %s" % (node.name, i.device_name, dd_net.cidr)) @@ -969,9 +969,28 @@ class MaasTaskRunner(drivers.DriverTaskRunner): failed = True continue - self.logger.info("Node %s deployed" % (n)) + attempts = 0 + while attempts < 120 and not machine.status_name.startswith('Deployed'): + attempts = attempts + 1 + time.sleep(1 * 60) + try: + machine.refresh() + self.logger.debug("Polling node %s status attempt %d: %s" % (n, attempts, machine.status_name)) + except: + self.logger.warning("Error updating node %s status during commissioning, will re-attempt." % + (n)) + if machine.status_name.startswith('Deployed'): + result_detail['detail'].append("Node %s deployed" % (n)) + self.logger.info("Node %s deployed" % (n)) + worked = True + else: + result_detail['detail'].append("Node %s deployment timed out" % (n)) + self.logger.warning("Node %s deployment timed out." % (n)) + failed = True - if failed: + if worked and failed: + final_result = hd_fields.ActionResult.PartialSuccess + elif failed: final_result = hd_fields.ActionResult.Failure else: final_result = hd_fields.ActionResult.Success diff --git a/drydock_provisioner/drivers/node/maasdriver/models/interface.py b/drydock_provisioner/drivers/node/maasdriver/models/interface.py index 135f1204..35802949 100644 --- a/drydock_provisioner/drivers/node/maasdriver/models/interface.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/interface.py @@ -120,12 +120,16 @@ class Interface(model_base.ResourceBase): # TODO Probably need to enumerate link mode options = { 'subnet': subnet.resource_id, - 'mode': 'dhcp' if ip_address is None else 'static', 'default_gateway': primary, } - if ip_address is not None: + if ip_address == 'dhcp': + options['mode'] = 'dhcp' + elif ip_address is not None: options['ip_address'] = ip_address + options['mode'] = 'static' + else: + options['mode'] = 'link_up' self.logger.debug("Linking interface %s to subnet: subnet=%s, mode=%s, address=%s, primary=%s" % (self.resource_id, subnet.resource_id, options['mode'], ip_address, primary)) diff --git a/drydock_provisioner/drivers/node/maasdriver/models/subnet.py b/drydock_provisioner/drivers/node/maasdriver/models/subnet.py index a9e6104a..a7340b05 100644 --- a/drydock_provisioner/drivers/node/maasdriver/models/subnet.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/subnet.py @@ -47,7 +47,7 @@ class Subnet(model_base.ResourceBase): # Static ranges are what is left after reserved (not assigned by MaaS) # and DHCP ranges are removed from a subnet if addr_range.get('type', None) in ['reserved','dhcp']: - range_type = addr_range('type', None) + range_type = addr_range.get('type', None) if range_type == 'dhcp': range_type = 'dynamic' From 28d460590c642765d2ac844d581962923d4926ea Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Mon, 19 Jun 2017 16:14:35 -0500 Subject: [PATCH 09/16] DRYD-31 Drydock driven by a config file Config file support via oslo_config Example config file in examples/drydock.conf --- drydock_provisioner/__init__.py | 7 +- drydock_provisioner/config.py | 68 +++++++++++++------ drydock_provisioner/control/middleware.py | 6 +- drydock_provisioner/drivers/__init__.py | 2 +- .../drivers/node/maasdriver/api_client.py | 2 + .../drivers/node/maasdriver/driver.py | 57 +++++++++------- .../drivers/oob/manual_driver/driver.py | 7 +- .../drivers/oob/pyghmi_driver/__init__.py | 18 ++--- drydock_provisioner/drydock.py | 29 +++++--- drydock_provisioner/orchestrator/__init__.py | 11 ++- examples/drydock.conf | 46 +++++++++++++ setup.py | 3 +- 12 files changed, 177 insertions(+), 79 deletions(-) create mode 100644 examples/drydock.conf diff --git a/drydock_provisioner/__init__.py b/drydock_provisioner/__init__.py index 2a385a45..6eda855f 100644 --- a/drydock_provisioner/__init__.py +++ b/drydock_provisioner/__init__.py @@ -10,4 +10,9 @@ # 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. \ No newline at end of file +# limitations under the License. + +from .config import DrydockConfig + +config_mgr = DrydockConfig() +conf = config_mgr.conf \ No newline at end of file diff --git a/drydock_provisioner/config.py b/drydock_provisioner/config.py index 112635a4..87228672 100644 --- a/drydock_provisioner/config.py +++ b/drydock_provisioner/config.py @@ -12,31 +12,55 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -# -# Read application configuration -# - -# configuration map with defaults +from oslo_config import cfg class DrydockConfig(object): + """ + Initialize all the core options + """ + # Core/General options + logging_options = [ + cfg.StrOpt('log_level', default='INFO', help='Global log level for Drydock'), + cfg.StrOpt('global_logger_name', default='drydock', help='Logger name for the top-level logger'), + cfg.StrOpt('oobdriver_logger_name', default='${global_logger_name}.oobdriver'), + cfg.StrOpt('nodedriver_logger_name', default='${global_logger_name}.nodedriver'), + cfg.StrOpt('control_logger_name', default='${global_logger_name}.control'), + ] - global_config = { - 'log_level': 'DEBUG', - } + # API Authentication options + auth_options = [ + cfg.StrOpt('admin_token', default='bigboss', help='X-Auth-Token value to bypass backend authentication'), + cfg.BoolOpt('bypass_enabled', default=False, help='Can backend authentication be bypassed?'), + ] - node_driver = { - 'maasdriver': { - }, - } + # Enabled plugins + plugin_options = [ + cfg.MultiStrOpt('ingester', + default=['drydock_provisioner.ingester.plugins.yaml.YamlIngester'], + help='Module path string of a input ingester to enable'), + cfg.MultiStrOpt('oob_driver', + default=['drydock_provisioner.drivers.oob.pyghmi_driver.PyghmiDriver'], + help='Module path string of a OOB driver to enable'), + cfg.StrOpt('node_driver', + default='drydock_provisioner.drivers.node.maasdriver.driver.MaasNodeDriver', + help='Module path string of the Node driver to enable'), + cfg.StrOpt('network_driver', + default=None, help='Module path string of the Network driver to enable'), + ] - ingester_config = { - 'plugins': ['drydock_provisioner.ingester.plugins.yaml.YamlIngester'], - } + # Timeouts for various tasks specified in minutes + timeout_options = [ + cfg.IntOpt('create_network_template',default=2,help='Timeout in minutes for creating site network templates'), + cfg.IntOpt('identify_node',default=10,help='Timeout in minutes for initial node identification'), + cfg.IntOpt('configure_hardware',default=30,help='Timeout in minutes for node commissioning and hardware configuration'), + cfg.IntOpt('apply_node_networking',default=5,help='Timeout in minutes for configuring node networking'), + cfg.IntOpt('deploy_node',default=45,help='Timeout in minutes for deploying a node'), + ] - orchestrator_config = { - 'drivers': { - 'oob': 'drydock_provisioner.drivers.oob.pyghmi_driver.PyghmiDriver', - 'node': 'drydock_provisioner.drivers.node.maasdriver.driver.MaasNodeDriver', - } - } \ No newline at end of file + def __init__(self): + self.conf = cfg.ConfigOpts() + + self.conf.register_opts(DrydockConfig.logging_options, group='logging') + self.conf.register_opts(DrydockConfig.auth_options, group='authentication') + self.conf.register_opts(DrydockConfig.plugin_options, group='plugins') + self.conf.register_opts(DrydockConfig.timeout_options, group='timeouts') diff --git a/drydock_provisioner/control/middleware.py b/drydock_provisioner/control/middleware.py index 28144c3b..00b807d9 100644 --- a/drydock_provisioner/control/middleware.py +++ b/drydock_provisioner/control/middleware.py @@ -16,7 +16,7 @@ import falcon import logging import uuid -import drydock_provisioner.config as config +import drydock_provisioner class AuthMiddleware(object): @@ -66,7 +66,7 @@ class ContextMiddleware(object): requested_logging = req.get_header('X-Log-Level') - if (config.DrydockConfig.global_config.get('log_level', '') == 'DEBUG' or + if (drydock_provisioner.conf.logging.log_level == 'DEBUG' or (requested_logging == 'DEBUG' and 'admin' in ctx.roles)): ctx.set_log_level('DEBUG') elif requested_logging == 'INFO': @@ -78,7 +78,7 @@ class ContextMiddleware(object): class LoggingMiddleware(object): def __init__(self): - self.logger = logging.getLogger('drydock.control') + self.logger = logging.getLogger(drydock_provisioner.conf.logging.control_logger_name) def process_response(self, req, resp, resource, req_succeeded): ctx = req.context diff --git a/drydock_provisioner/drivers/__init__.py b/drydock_provisioner/drivers/__init__.py index c3868af1..ec609412 100644 --- a/drydock_provisioner/drivers/__init__.py +++ b/drydock_provisioner/drivers/__init__.py @@ -35,7 +35,7 @@ class ProviderDriver(object): raise ValueError("ProviderDriver requires valid state manager") self.state_manager = state_manager - + # These are the actions that this driver supports self.supported_actions = [hd_fields.OrchestratorAction.Noop] diff --git a/drydock_provisioner/drivers/node/maasdriver/api_client.py b/drydock_provisioner/drivers/node/maasdriver/api_client.py index 01930a4e..789349b6 100644 --- a/drydock_provisioner/drivers/node/maasdriver/api_client.py +++ b/drydock_provisioner/drivers/node/maasdriver/api_client.py @@ -43,6 +43,8 @@ class MaasRequestFactory(object): def __init__(self, base_url, apikey): self.base_url = base_url self.apikey = apikey + + print("Creating MaaS API client for URL %s with key %s" % (base_url, apikey)) self.signer = MaasOauth(apikey) self.http_session = requests.Session() diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index 4984527d..1439b348 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -16,8 +16,10 @@ import logging import traceback import sys +from oslo_config import cfg + +import drydock_provisioner import drydock_provisioner.error as errors -import drydock_provisioner.config as config import drydock_provisioner.drivers as drivers import drydock_provisioner.objects.fields as hd_fields import drydock_provisioner.objects.task as task_model @@ -32,6 +34,11 @@ import drydock_provisioner.drivers.node.maasdriver.models.machine as maas_machin class MaasNodeDriver(NodeDriver): + maasdriver_options = [ + cfg.StrOpt('maas_api_key', help='The API key for accessing MaaS'), + cfg.StrOpt('maas_api_url', help='The URL for accessing MaaS API'), + ] + def __init__(self, **kwargs): super(MaasNodeDriver, self).__init__(**kwargs) @@ -39,9 +46,13 @@ class MaasNodeDriver(NodeDriver): self.driver_key = "maasdriver" self.driver_desc = "MaaS Node Provisioning Driver" - self.config = config.DrydockConfig.node_driver[self.driver_key] + self.setup_config_options(drydock_provisioner.conf) - self.logger = logging.getLogger('drydock.nodedriver.maasdriver') + self.logger = logging.getLogger("%s.%s" % + (drydock_provisioner.conf.logging.nodedriver_logger_name, self.driver_key)) + + def setup_config_options(self, conf): + conf.register_opts(MaasNodeDriver.maasdriver_options, group=self.driver_key) def execute_task(self, task_id): task = self.state_manager.get_task(task_id) @@ -56,7 +67,7 @@ class MaasNodeDriver(NodeDriver): if task.action == hd_fields.OrchestratorAction.ValidateNodeServices: self.orchestrator.task_field_update(task.get_id(), status=hd_fields.TaskStatus.Running) - maas_client = MaasRequestFactory(self.config['api_url'], self.config['api_key']) + maas_client = MaasRequestFactory(drydock_provisioner.conf.maasdriver.maas_api_url, drydock_provisioner.conf.maasdriver.maas_api_key) try: if maas_client.test_connectivity(): @@ -122,16 +133,13 @@ class MaasNodeDriver(NodeDriver): task_scope={'site': task.site_name}) runner = MaasTaskRunner(state_manager=self.state_manager, orchestrator=self.orchestrator, - task_id=subtask.get_id(),config=self.config) + task_id=subtask.get_id()) self.logger.info("Starting thread for task %s to create network templates" % (subtask.get_id())) runner.start() - # TODO Figure out coherent system for putting all the timeouts in - # the config - - runner.join(timeout=120) + runner.join(timeout=drydock_provisioner.conf.timeouts.create_network_template * 60) if runner.is_alive(): result = { @@ -174,7 +182,7 @@ class MaasNodeDriver(NodeDriver): task_scope={'site': task.site_name, 'node_names': [n]}) runner = MaasTaskRunner(state_manager=self.state_manager, orchestrator=self.orchestrator, - task_id=subtask.get_id(),config=self.config) + task_id=subtask.get_id()) self.logger.info("Starting thread for task %s to identify node %s" % (subtask.get_id(), n)) @@ -185,8 +193,7 @@ class MaasNodeDriver(NodeDriver): attempts = 0 worked = failed = False - #TODO Add timeout to config - while running_subtasks > 0 and attempts < 3: + while running_subtasks > 0 and attempts < drydock_provisioner.conf.timeouts.identify_node: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -244,7 +251,7 @@ class MaasNodeDriver(NodeDriver): task_scope={'site': task.site_name, 'node_names': [n]}) runner = MaasTaskRunner(state_manager=self.state_manager, orchestrator=self.orchestrator, - task_id=subtask.get_id(),config=self.config) + task_id=subtask.get_id()) self.logger.info("Starting thread for task %s to commission node %s" % (subtask.get_id(), n)) @@ -256,7 +263,7 @@ class MaasNodeDriver(NodeDriver): worked = failed = False #TODO Add timeout to config - while running_subtasks > 0 and attempts < 20: + while running_subtasks > 0 and attempts < drydock_provisioner.conf.timeouts.configure_hardware: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -314,7 +321,7 @@ class MaasNodeDriver(NodeDriver): task_scope={'site': task.site_name, 'node_names': [n]}) runner = MaasTaskRunner(state_manager=self.state_manager, orchestrator=self.orchestrator, - task_id=subtask.get_id(),config=self.config) + task_id=subtask.get_id()) self.logger.info("Starting thread for task %s to configure networking on node %s" % (subtask.get_id(), n)) @@ -325,8 +332,7 @@ class MaasNodeDriver(NodeDriver): attempts = 0 worked = failed = False - #TODO Add timeout to config - while running_subtasks > 0 and attempts < 2: + while running_subtasks > 0 and attempts < drydock_provisioner.conf.timeouts.apply_node_networking: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -384,7 +390,7 @@ class MaasNodeDriver(NodeDriver): task_scope={'site': task.site_name, 'node_names': [n]}) runner = MaasTaskRunner(state_manager=self.state_manager, orchestrator=self.orchestrator, - task_id=subtask.get_id(),config=self.config) + task_id=subtask.get_id()) self.logger.info("Starting thread for task %s to deploy node %s" % (subtask.get_id(), n)) @@ -395,8 +401,7 @@ class MaasNodeDriver(NodeDriver): attempts = 0 worked = failed = False - #TODO Add timeout to config - while running_subtasks > 0 and attempts < 120: + while running_subtasks > 0 and attempts < drydock_provisioner.conf.timeouts.deploy_node: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -435,10 +440,10 @@ class MaasNodeDriver(NodeDriver): class MaasTaskRunner(drivers.DriverTaskRunner): - def __init__(self, config=None, **kwargs): + def __init__(self, **kwargs): super(MaasTaskRunner, self).__init__(**kwargs) - self.driver_config = config + # TODO Need to build this name from configs self.logger = logging.getLogger('drydock.nodedriver.maasdriver') def execute_task(self): @@ -448,8 +453,8 @@ class MaasTaskRunner(drivers.DriverTaskRunner): status=hd_fields.TaskStatus.Running, result=hd_fields.ActionResult.Incomplete) - self.maas_client = MaasRequestFactory(self.driver_config['api_url'], - self.driver_config['api_key']) + self.maas_client = MaasRequestFactory(drydock_provisioner.conf.maasdriver.maas_api_url, + drydock_provisioner.conf.maasdriver.maas_api_key) site_design = self.orchestrator.get_effective_site(self.task.design_id) @@ -738,7 +743,7 @@ class MaasTaskRunner(drivers.DriverTaskRunner): # Poll machine status attempts = 0 - while attempts < 30 and machine.status_name != 'Ready': + while attempts < drydock_provisioner.conf.timeouts.configure_hardware and machine.status_name != 'Ready': attempts = attempts + 1 time.sleep(1 * 60) try: @@ -970,7 +975,7 @@ class MaasTaskRunner(drivers.DriverTaskRunner): continue attempts = 0 - while attempts < 120 and not machine.status_name.startswith('Deployed'): + while attempts < drydock_provisioner.conf.timeouts.deploy_node and not machine.status_name.startswith('Deployed'): attempts = attempts + 1 time.sleep(1 * 60) try: diff --git a/drydock_provisioner/drivers/oob/manual_driver/driver.py b/drydock_provisioner/drivers/oob/manual_driver/driver.py index e53f9e30..6e483aef 100644 --- a/drydock_provisioner/drivers/oob/manual_driver/driver.py +++ b/drydock_provisioner/drivers/oob/manual_driver/driver.py @@ -14,8 +14,9 @@ import time import logging +import drydock_provisioner import drydock_provisioner.error as errors -import drydock_provisioner.config as config + import drydock_provisioner.objects.fields as hd_fields import drydock_provisioner.objects.task as task_model @@ -35,8 +36,8 @@ class ManualDriver(oob.OobDriver): self.driver_key = "manual_driver" self.driver_desc = "Manual (Noop) OOB Driver" - self.logger = logging.getLogger('drydock.oobdriver.pyghmi') - self.config = config.DrydockConfig.node_driver.get(self.driver_key, {}) + self.logger = logging.getLogger("%s.%s" % + (drydock_provisioner.conf.logging.oobdriver_logger_name, self.driver_key)) def execute_task(self, task_id): task = self.state_manager.get_task(task_id) diff --git a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py index 655241ac..5e9b3ee0 100644 --- a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py +++ b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py @@ -16,8 +16,8 @@ import logging from pyghmi.ipmi.command import Command +import drydock_provisioner import drydock_provisioner.error as errors -import drydock_provisioner.config as config import drydock_provisioner.objects.fields as hd_fields import drydock_provisioner.objects.task as task_model @@ -37,8 +37,8 @@ class PyghmiDriver(oob.OobDriver): self.driver_key = "pyghmi_driver" self.driver_desc = "Pyghmi OOB Driver" - self.logger = logging.getLogger('drydock.oobdriver.pyghmi') - self.config = config.DrydockConfig.node_driver.get(self.driver_key, {}) + self.logger = logging.getLogger("%s.%s" % + (drydock_provisioner.conf.logging.oobdriver_logger_name, self.driver_key)) def execute_task(self, task_id): task = self.state_manager.get_task(task_id) @@ -99,20 +99,16 @@ class PyghmiDriver(oob.OobDriver): task_id=subtask.get_id(), node=n) runner.start() - # Wait for subtasks to complete - # TODO need some kind of timeout - i = 0 - while len(incomplete_subtasks) > 0: + attempts = 0 + while len(incomplete_subtasks) > 0 and attempts <= getattr(drydock_provisioner.conf.timeouts, task.action, 5): for n in incomplete_subtasks: t = self.state_manager.get_task(n) if t.get_status() in [hd_fields.TaskStatus.Terminated, hd_fields.TaskStatus.Complete, hd_fields.TaskStatus.Errored]: incomplete_subtasks.remove(n) - time.sleep(2) - i = i+1 - if i == 5: - break + time.sleep(1 * 60) + attempts = attempts + 1 task = self.state_manager.get_task(task.get_id()) subtasks = map(self.state_manager.get_task, task.get_subtasks()) diff --git a/drydock_provisioner/drydock.py b/drydock_provisioner/drydock.py index 18e53a13..06259816 100644 --- a/drydock_provisioner/drydock.py +++ b/drydock_provisioner/drydock.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from oslo_config import cfg +import sys -import drydock_provisioner.config as config +import drydock_provisioner import drydock_provisioner.objects as objects import drydock_provisioner.ingester as ingester import drydock_provisioner.statemgmt as statemgmt @@ -22,18 +24,29 @@ import drydock_provisioner.control.api as api def start_drydock(): objects.register_all() - - # Setup root logger - logger = logging.getLogger('drydock') - logger.setLevel(config.DrydockConfig.global_config.get('log_level')) + # Setup configuration parsing + cli_options = [ + cfg.BoolOpt('debug', short='d', default=False, help='Enable debug logging'), + ] + + drydock_provisioner.conf.register_cli_opts(cli_options) + drydock_provisioner.conf(sys.argv[1:]) + + if drydock_provisioner.conf.debug: + drydock_provisioner.conf.logging.log_level = 'DEBUG' + + # Setup root logger + logger = logging.getLogger(drydock_provisioner.conf.logging.global_logger_name) + + logger.setLevel(drydock_provisioner.conf.logging.log_level) ch = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s:%(funcName)s - %(message)s') ch.setFormatter(formatter) logger.addHandler(ch) # Specalized format for API logging - logger = logging.getLogger('drydock.control') + logger = logging.getLogger(drydock_provisioner.conf.logging.control_logger_name) logger.propagate = False formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(user)s - %(req_id)s - %(external_ctx)s - %(message)s') @@ -43,10 +56,10 @@ def start_drydock(): state = statemgmt.DesignState() - orchestrator = orch.Orchestrator(config.DrydockConfig.orchestrator_config.get('drivers', {}), + orchestrator = orch.Orchestrator(drydock_provisioner.conf.plugins, state_manager=state) input_ingester = ingester.Ingester() - input_ingester.enable_plugins(config.DrydockConfig.ingester_config.get('plugins', [])) + input_ingester.enable_plugins(drydock_provisioner.conf.plugins.ingester) return api.start_api(state_manager=state, ingester=input_ingester, orchestrator=orchestrator) diff --git a/drydock_provisioner/orchestrator/__init__.py b/drydock_provisioner/orchestrator/__init__.py index 8d86cff9..c639d8d0 100644 --- a/drydock_provisioner/orchestrator/__init__.py +++ b/drydock_provisioner/orchestrator/__init__.py @@ -37,9 +37,14 @@ class Orchestrator(object): self.logger = logging.getLogger('drydock.orchestrator') if enabled_drivers is not None: - oob_drivers = enabled_drivers.get('oob', []) + oob_drivers = enabled_drivers.oob_driver + + # This is because oslo_config changes the option value + # for multiopt depending on if multiple values are actually defined + print("%s" % (oob_drivers)) for d in oob_drivers: + print("Enabling OOB driver %s" % d) if d is not None: m, c = d.rsplit('.', 1) oob_driver_class = \ @@ -50,7 +55,7 @@ class Orchestrator(object): self.enabled_drivers['oob'].append(oob_driver_class(state_manager=state_manager, orchestrator=self)) - node_driver_name = enabled_drivers.get('node', None) + node_driver_name = enabled_drivers.node_driver if node_driver_name is not None: m, c = node_driver_name.rsplit('.', 1) node_driver_class = \ @@ -59,7 +64,7 @@ class Orchestrator(object): self.enabled_drivers['node'] = node_driver_class(state_manager=state_manager, orchestrator=self) - network_driver_name = enabled_drivers.get('network', None) + network_driver_name = enabled_drivers.network_driver if network_driver_name is not None: m, c = network_driver_name.rsplit('.', 1) network_driver_class = \ diff --git a/examples/drydock.conf b/examples/drydock.conf new file mode 100644 index 00000000..48db0e8d --- /dev/null +++ b/examples/drydock.conf @@ -0,0 +1,46 @@ +# Copyright 2017 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. + +[DEFAULT] +# No global options yet + +[logging] +log_level = 'DEBUG' + +[authentication] +bypass_enabled = True + +[plugins] +# All the config ingesters that are active +# Supports multiple values +ingester = 'drydock_provisioner.ingester.plugins.yaml.YamlIngester' + +# OOB drivers that are enabled +# Supports multiple values +oob_driver = 'drydock_provisioner.drivers.oob.pyghmi_driver.PyghmiDriver' +oob_driver = 'drydock_provisioner.drivers.oob.manual_driver.driver.ManualDriver' + +# Node driver that is enabled +node_driver = 'drydock_provisioner.drivers.node.maasdriver.driver.MaasNodeDriver' + +[timeouts] +create_network_template = 2 +identify_node = 10 +configure_hardware = 30 +apply_node_networking = 5 +deploy_node = 45 + +[maasdriver] +maas_api_url = 'http://localhost:8000/MAAS/api/2.0/' +maas_api_key = 'your:secret:key' \ No newline at end of file diff --git a/setup.py b/setup.py index 3aa5c1ae..bca84cf4 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,8 @@ setup(name='drydock_provisioner', 'requests', 'oauthlib', 'uwsgi>1.4', - 'bson===0.4.7' + 'bson===0.4.7', + 'oslo.config', ] ) From a7488c657ba048ea1467a7ebca6def98f7bb9696 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Mon, 19 Jun 2017 16:34:35 -0500 Subject: [PATCH 10/16] Add secret tag to sensitive options Add debug logging of options values on startup --- drydock_provisioner/config.py | 2 +- drydock_provisioner/drivers/node/maasdriver/driver.py | 2 +- drydock_provisioner/drydock.py | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/drydock_provisioner/config.py b/drydock_provisioner/config.py index 87228672..b827e298 100644 --- a/drydock_provisioner/config.py +++ b/drydock_provisioner/config.py @@ -29,7 +29,7 @@ class DrydockConfig(object): # API Authentication options auth_options = [ - cfg.StrOpt('admin_token', default='bigboss', help='X-Auth-Token value to bypass backend authentication'), + cfg.StrOpt('admin_token', default='bigboss', help='X-Auth-Token value to bypass backend authentication', secret=True), cfg.BoolOpt('bypass_enabled', default=False, help='Can backend authentication be bypassed?'), ] diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index 1439b348..34a69742 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -35,7 +35,7 @@ import drydock_provisioner.drivers.node.maasdriver.models.machine as maas_machin class MaasNodeDriver(NodeDriver): maasdriver_options = [ - cfg.StrOpt('maas_api_key', help='The API key for accessing MaaS'), + cfg.StrOpt('maas_api_key', help='The API key for accessing MaaS', secret=True), cfg.StrOpt('maas_api_url', help='The URL for accessing MaaS API'), ] diff --git a/drydock_provisioner/drydock.py b/drydock_provisioner/drydock.py index 06259816..de1e1b80 100644 --- a/drydock_provisioner/drydock.py +++ b/drydock_provisioner/drydock.py @@ -54,6 +54,7 @@ def start_drydock(): ch.setFormatter(formatter) logger.addHandler(ch) + state = statemgmt.DesignState() orchestrator = orch.Orchestrator(drydock_provisioner.conf.plugins, @@ -61,6 +62,9 @@ def start_drydock(): input_ingester = ingester.Ingester() input_ingester.enable_plugins(drydock_provisioner.conf.plugins.ingester) + # Now that loggers are configured, log the effective config + drydock_provisioner.conf.log_opt_values(logging.getLogger(drydock_provisioner.conf.logging.global_logger_name), logging.DEBUG) + return api.start_api(state_manager=state, ingester=input_ingester, orchestrator=orchestrator) From a4357c11988a07a3addc80f511ea082d44c5bada Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Wed, 21 Jun 2017 10:38:16 -0500 Subject: [PATCH 11/16] Update oslo_config integration to support auto-generation of files --- drydock_provisioner/__init__.py | 5 - drydock_provisioner/config.py | 96 +++++++++++++++++-- drydock_provisioner/drivers/__init__.py | 8 +- drydock_provisioner/drivers/node/__init__.py | 8 +- .../drivers/node/maasdriver/driver.py | 41 ++++---- .../drivers/oob/pyghmi_driver/__init__.py | 3 +- drydock_provisioner/drydock.py | 23 ++--- setup.py | 20 +--- 8 files changed, 135 insertions(+), 69 deletions(-) diff --git a/drydock_provisioner/__init__.py b/drydock_provisioner/__init__.py index 6eda855f..f10bbbf6 100644 --- a/drydock_provisioner/__init__.py +++ b/drydock_provisioner/__init__.py @@ -11,8 +11,3 @@ # 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. - -from .config import DrydockConfig - -config_mgr = DrydockConfig() -conf = config_mgr.conf \ No newline at end of file diff --git a/drydock_provisioner/config.py b/drydock_provisioner/config.py index b827e298..4cc7d0ff 100644 --- a/drydock_provisioner/config.py +++ b/drydock_provisioner/config.py @@ -12,19 +12,42 @@ # See the License for the specific language governing permissions and # limitations under the License. # + +"""Single point of entry to generate the sample configuration file. + +This module collects all the necessary info from the other modules in this +package. It is assumed that: + +* Every other module in this package has a 'list_opts' function which + returns a dict where: + + * The keys are strings which are the group names. + + * The value of each key is a list of config options for that group. + +* The conf package doesn't have further packages with config options. + +* This module is only used in the context of sample file generation. + +""" +import collections +import importlib +import os +import pkgutil + from oslo_config import cfg class DrydockConfig(object): """ Initialize all the core options """ - # Core/General options + # Logging options logging_options = [ cfg.StrOpt('log_level', default='INFO', help='Global log level for Drydock'), cfg.StrOpt('global_logger_name', default='drydock', help='Logger name for the top-level logger'), - cfg.StrOpt('oobdriver_logger_name', default='${global_logger_name}.oobdriver'), - cfg.StrOpt('nodedriver_logger_name', default='${global_logger_name}.nodedriver'), - cfg.StrOpt('control_logger_name', default='${global_logger_name}.control'), + cfg.StrOpt('oobdriver_logger_name', default='${global_logger_name}.oobdriver', help='Logger name for OOB driver logging'), + cfg.StrOpt('nodedriver_logger_name', default='${global_logger_name}.nodedriver', help='Logger name for Node driver logging'), + cfg.StrOpt('control_logger_name', default='${global_logger_name}.control', help='Logger name for API server logging'), ] # API Authentication options @@ -50,17 +73,72 @@ class DrydockConfig(object): # Timeouts for various tasks specified in minutes timeout_options = [ - cfg.IntOpt('create_network_template',default=2,help='Timeout in minutes for creating site network templates'), - cfg.IntOpt('identify_node',default=10,help='Timeout in minutes for initial node identification'), - cfg.IntOpt('configure_hardware',default=30,help='Timeout in minutes for node commissioning and hardware configuration'), - cfg.IntOpt('apply_node_networking',default=5,help='Timeout in minutes for configuring node networking'), - cfg.IntOpt('deploy_node',default=45,help='Timeout in minutes for deploying a node'), + cfg.IntOpt('drydock_timeout', default=5, help='Fallback timeout when a specific one is not configured'), + cfg.IntOpt('create_network_template', default=2, help='Timeout in minutes for creating site network templates'), + cfg.IntOpt('identify_node', default=10, help='Timeout in minutes for initial node identification'), + cfg.IntOpt('configure_hardware', default=30, help='Timeout in minutes for node commissioning and hardware configuration'), + cfg.IntOpt('apply_node_networking', default=5, help='Timeout in minutes for configuring node networking'), + cfg.IntOpt('deploy_node', default=45, help='Timeout in minutes for deploying a node'), ] def __init__(self): self.conf = cfg.ConfigOpts() + def register_options(self): self.conf.register_opts(DrydockConfig.logging_options, group='logging') self.conf.register_opts(DrydockConfig.auth_options, group='authentication') self.conf.register_opts(DrydockConfig.plugin_options, group='plugins') self.conf.register_opts(DrydockConfig.timeout_options, group='timeouts') + +config_mgr = DrydockConfig() +conf = config_mgr.conf + +IGNORED_MODULES = ('drydock', 'config') + +def list_opts(): + opts = {'logging': DrydockConfig.logging_options, + 'authentication': DrydockConfig.auth_options, + 'plugins': DrydockConfig.plugin_options, + 'timeouts': DrydockConfig.timeout_options + } + + package_path = os.path.dirname(os.path.abspath(__file__)) + parent_module = ".".join(__name__.split('.')[:-1]) + module_names = _list_module_names(package_path, parent_module) + imported_modules = _import_modules(module_names) + _append_config_options(imported_modules, opts) + return _tupleize(opts) + +def _tupleize(d): + """Convert a dict of options to the 2-tuple format.""" + return [(key, value) for key, value in d.items()] + +def _list_module_names(pkg_path, parent_module): + module_names = [] + for _, module_name, ispkg in pkgutil.iter_modules(path=[pkg_path]): + if module_name in IGNORED_MODULES: + # Skip this module. + continue + elif ispkg: + module_names.extend(_list_module_names(pkg_path + "/" + module_name, parent_module + "." + module_name)) + else: + module_names.append(parent_module + "." + module_name) + return module_names + +def _import_modules(module_names): + imported_modules = [] + for module_name in module_names: + module = importlib.import_module(module_name) + if hasattr(module, 'list_opts'): + print("Pulling options from module %s" % module.__name__) + imported_modules.append(module) + return imported_modules + +def _append_config_options(imported_modules, config_options): + for module in imported_modules: + configs = module.list_opts() + for key, val in configs.items(): + if key not in config_options: + config_options[key] = val + else: + config_options[key].extend(val) diff --git a/drydock_provisioner/drivers/__init__.py b/drydock_provisioner/drivers/__init__.py index ec609412..169ad64b 100644 --- a/drydock_provisioner/drivers/__init__.py +++ b/drydock_provisioner/drivers/__init__.py @@ -25,6 +25,10 @@ import drydock_provisioner.error as errors # driver tasks and feed them via queue class ProviderDriver(object): + driver_name = "generic" + driver_key = "generic" + driver_desc = "Generic Provider Driver" + def __init__(self, orchestrator=None, state_manager=None, **kwargs): if orchestrator is None: raise ValueError("ProviderDriver requires valid orchestrator") @@ -39,9 +43,7 @@ class ProviderDriver(object): # These are the actions that this driver supports self.supported_actions = [hd_fields.OrchestratorAction.Noop] - self.driver_name = "generic" - self.driver_key = "generic" - self.driver_desc = "Generic Provider Driver" + def execute_task(self, task_id): task = self.state_manager.get_task(task_id) diff --git a/drydock_provisioner/drivers/node/__init__.py b/drydock_provisioner/drivers/node/__init__.py index a28c7cec..08939cb3 100644 --- a/drydock_provisioner/drivers/node/__init__.py +++ b/drydock_provisioner/drivers/node/__init__.py @@ -20,6 +20,10 @@ from drydock_provisioner.drivers import ProviderDriver class NodeDriver(ProviderDriver): + driver_name = "node_generic" + driver_key = "node_generic" + driver_desc = "Generic Node Driver" + def __init__(self, **kwargs): super(NodeDriver, self).__init__(**kwargs) @@ -37,10 +41,6 @@ class NodeDriver(ProviderDriver): hd_fields.OrchestratorAction.DeployNode, hd_fields.OrchestratorAction.DestroyNode] - self.driver_name = "node_generic" - self.driver_key = "node_generic" - self.driver_desc = "Generic Node Driver" - def execute_task(self, task_id): task = self.state_manager.get_task(task_id) task_action = task.action diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index 34a69742..cbef9b9b 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -18,7 +18,7 @@ import sys from oslo_config import cfg -import drydock_provisioner +import drydock_provisioner.config as config import drydock_provisioner.error as errors import drydock_provisioner.drivers as drivers import drydock_provisioner.objects.fields as hd_fields @@ -33,26 +33,22 @@ import drydock_provisioner.drivers.node.maasdriver.models.subnet as maas_subnet import drydock_provisioner.drivers.node.maasdriver.models.machine as maas_machine class MaasNodeDriver(NodeDriver): - maasdriver_options = [ cfg.StrOpt('maas_api_key', help='The API key for accessing MaaS', secret=True), cfg.StrOpt('maas_api_url', help='The URL for accessing MaaS API'), ] + driver_name = 'maasdriver' + driver_key = 'maasdriver' + driver_desc = 'MaaS Node Provisioning Driver' + def __init__(self, **kwargs): super(MaasNodeDriver, self).__init__(**kwargs) - self.driver_name = "maasdriver" - self.driver_key = "maasdriver" - self.driver_desc = "MaaS Node Provisioning Driver" - - self.setup_config_options(drydock_provisioner.conf) + config.conf.register_opts(maasdriver_options, group='maasdriver') self.logger = logging.getLogger("%s.%s" % - (drydock_provisioner.conf.logging.nodedriver_logger_name, self.driver_key)) - - def setup_config_options(self, conf): - conf.register_opts(MaasNodeDriver.maasdriver_options, group=self.driver_key) + (config.conf.logging.nodedriver_logger_name, self.driver_key)) def execute_task(self, task_id): task = self.state_manager.get_task(task_id) @@ -67,7 +63,7 @@ class MaasNodeDriver(NodeDriver): if task.action == hd_fields.OrchestratorAction.ValidateNodeServices: self.orchestrator.task_field_update(task.get_id(), status=hd_fields.TaskStatus.Running) - maas_client = MaasRequestFactory(drydock_provisioner.conf.maasdriver.maas_api_url, drydock_provisioner.conf.maasdriver.maas_api_key) + maas_client = MaasRequestFactory(config.conf.maasdriver.maas_api_url, config.conf.maasdriver.maas_api_key) try: if maas_client.test_connectivity(): @@ -139,7 +135,7 @@ class MaasNodeDriver(NodeDriver): runner.start() - runner.join(timeout=drydock_provisioner.conf.timeouts.create_network_template * 60) + runner.join(timeout=config.conf.timeouts.create_network_template * 60) if runner.is_alive(): result = { @@ -193,7 +189,7 @@ class MaasNodeDriver(NodeDriver): attempts = 0 worked = failed = False - while running_subtasks > 0 and attempts < drydock_provisioner.conf.timeouts.identify_node: + while running_subtasks > 0 and attempts < config.conf.timeouts.identify_node: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -263,7 +259,7 @@ class MaasNodeDriver(NodeDriver): worked = failed = False #TODO Add timeout to config - while running_subtasks > 0 and attempts < drydock_provisioner.conf.timeouts.configure_hardware: + while running_subtasks > 0 and attempts < config.conf.timeouts.configure_hardware: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -332,7 +328,7 @@ class MaasNodeDriver(NodeDriver): attempts = 0 worked = failed = False - while running_subtasks > 0 and attempts < drydock_provisioner.conf.timeouts.apply_node_networking: + while running_subtasks > 0 and attempts < config.conf.timeouts.apply_node_networking: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -401,7 +397,7 @@ class MaasNodeDriver(NodeDriver): attempts = 0 worked = failed = False - while running_subtasks > 0 and attempts < drydock_provisioner.conf.timeouts.deploy_node: + while running_subtasks > 0 and attempts < config.conf.timeouts.deploy_node: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -453,8 +449,8 @@ class MaasTaskRunner(drivers.DriverTaskRunner): status=hd_fields.TaskStatus.Running, result=hd_fields.ActionResult.Incomplete) - self.maas_client = MaasRequestFactory(drydock_provisioner.conf.maasdriver.maas_api_url, - drydock_provisioner.conf.maasdriver.maas_api_key) + self.maas_client = MaasRequestFactory(config.conf.maasdriver.maas_api_url, + config.conf.maasdriver.maas_api_key) site_design = self.orchestrator.get_effective_site(self.task.design_id) @@ -743,7 +739,7 @@ class MaasTaskRunner(drivers.DriverTaskRunner): # Poll machine status attempts = 0 - while attempts < drydock_provisioner.conf.timeouts.configure_hardware and machine.status_name != 'Ready': + while attempts < config.conf.timeouts.configure_hardware and machine.status_name != 'Ready': attempts = attempts + 1 time.sleep(1 * 60) try: @@ -975,7 +971,7 @@ class MaasTaskRunner(drivers.DriverTaskRunner): continue attempts = 0 - while attempts < drydock_provisioner.conf.timeouts.deploy_node and not machine.status_name.startswith('Deployed'): + while attempts < config.conf.timeouts.deploy_node and not machine.status_name.startswith('Deployed'): attempts = attempts + 1 time.sleep(1 * 60) try: @@ -1004,3 +1000,6 @@ class MaasTaskRunner(drivers.DriverTaskRunner): status=hd_fields.TaskStatus.Complete, result=final_result, result_detail=result_detail) + +def list_opts(): + return {MaasNodeDriver.driver_key: MaasNodeDriver.maasdriver_options} diff --git a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py index 5e9b3ee0..f1c8331d 100644 --- a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py +++ b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py @@ -100,7 +100,8 @@ class PyghmiDriver(oob.OobDriver): runner.start() attempts = 0 - while len(incomplete_subtasks) > 0 and attempts <= getattr(drydock_provisioner.conf.timeouts, task.action, 5): + while (len(incomplete_subtasks) > 0 and + attempts <= getattr(drydock_provisioner.conf.timeouts, task.action, drydock_provisioner.conf.timeouts.drydock_timeout)): for n in incomplete_subtasks: t = self.state_manager.get_task(n) if t.get_status() in [hd_fields.TaskStatus.Terminated, diff --git a/drydock_provisioner/drydock.py b/drydock_provisioner/drydock.py index de1e1b80..da4e9ce4 100644 --- a/drydock_provisioner/drydock.py +++ b/drydock_provisioner/drydock.py @@ -15,7 +15,7 @@ import logging from oslo_config import cfg import sys -import drydock_provisioner +import drydock_provisioner.config as config import drydock_provisioner.objects as objects import drydock_provisioner.ingester as ingester import drydock_provisioner.statemgmt as statemgmt @@ -30,23 +30,24 @@ def start_drydock(): cfg.BoolOpt('debug', short='d', default=False, help='Enable debug logging'), ] - drydock_provisioner.conf.register_cli_opts(cli_options) - drydock_provisioner.conf(sys.argv[1:]) + config.conf.register_cli_opts(cli_options) + config.config_mgr.register_options() + config.conf(sys.argv[1:]) - if drydock_provisioner.conf.debug: - drydock_provisioner.conf.logging.log_level = 'DEBUG' + if config.conf.debug: + config.conf.logging.log_level = 'DEBUG' # Setup root logger - logger = logging.getLogger(drydock_provisioner.conf.logging.global_logger_name) + logger = logging.getLogger(config.conf.logging.global_logger_name) - logger.setLevel(drydock_provisioner.conf.logging.log_level) + logger.setLevel(config.conf.logging.log_level) ch = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s:%(funcName)s - %(message)s') ch.setFormatter(formatter) logger.addHandler(ch) # Specalized format for API logging - logger = logging.getLogger(drydock_provisioner.conf.logging.control_logger_name) + logger = logging.getLogger(config.conf.logging.control_logger_name) logger.propagate = False formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(user)s - %(req_id)s - %(external_ctx)s - %(message)s') @@ -57,13 +58,13 @@ def start_drydock(): state = statemgmt.DesignState() - orchestrator = orch.Orchestrator(drydock_provisioner.conf.plugins, + orchestrator = orch.Orchestrator(config.conf.plugins, state_manager=state) input_ingester = ingester.Ingester() - input_ingester.enable_plugins(drydock_provisioner.conf.plugins.ingester) + input_ingester.enable_plugins(config.conf.plugins.ingester) # Now that loggers are configured, log the effective config - drydock_provisioner.conf.log_opt_values(logging.getLogger(drydock_provisioner.conf.logging.global_logger_name), logging.DEBUG) + config.conf.log_opt_values(logging.getLogger(config.conf.logging.global_logger_name), logging.DEBUG) return api.start_api(state_manager=state, ingester=input_ingester, orchestrator=orchestrator) diff --git a/setup.py b/setup.py index bca84cf4..88bef9fc 100644 --- a/setup.py +++ b/setup.py @@ -15,20 +15,7 @@ # drydock_provisioner - A tool to consume a host topology and orchestrate # and monitor the provisioning of those hosts and execution of bootstrap # scripts -# -# Modular services: -# smelter - A service to consume the host topology, will support multiple -# input formats. Initially supports a YAML schema as demonstrated -# in the examples folder -# tarot - A service for persisting the host topology and orchestration state -# and making the data available via API -# cockpit - The entrypoint API for users to control helm-drydock and query -# current state -# alchemist - The core orchestrator -# drivers - A tree with all of the plugins that alchemist uses to execute -# orchestrated tasks -# jabberwocky - An introspection API that newly provisioned nodes can use to -# ingest self-data and bootstrap their application deployment process + from setuptools import setup @@ -65,6 +52,9 @@ setup(name='drydock_provisioner', 'uwsgi>1.4', 'bson===0.4.7', 'oslo.config', - ] + ], + entry_points={ + 'oslo.config.opts': 'drydock_provisioner = drydock_provisioner.config:list_opts', + } ) From 2182c63557c49959e71583cb35690b56714d665e Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Wed, 21 Jun 2017 12:57:33 -0500 Subject: [PATCH 12/16] Update some references to old configuration object --- drydock_provisioner/control/middleware.py | 6 +++--- drydock_provisioner/drivers/node/maasdriver/driver.py | 2 +- drydock_provisioner/drivers/oob/manual_driver/driver.py | 4 ++-- drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/drydock_provisioner/control/middleware.py b/drydock_provisioner/control/middleware.py index 00b807d9..875e9a6c 100644 --- a/drydock_provisioner/control/middleware.py +++ b/drydock_provisioner/control/middleware.py @@ -16,7 +16,7 @@ import falcon import logging import uuid -import drydock_provisioner +import drydock_provisioner.config as config class AuthMiddleware(object): @@ -66,7 +66,7 @@ class ContextMiddleware(object): requested_logging = req.get_header('X-Log-Level') - if (drydock_provisioner.conf.logging.log_level == 'DEBUG' or + if (config.conf.logging.log_level == 'DEBUG' or (requested_logging == 'DEBUG' and 'admin' in ctx.roles)): ctx.set_log_level('DEBUG') elif requested_logging == 'INFO': @@ -78,7 +78,7 @@ class ContextMiddleware(object): class LoggingMiddleware(object): def __init__(self): - self.logger = logging.getLogger(drydock_provisioner.conf.logging.control_logger_name) + self.logger = logging.getLogger(config.conf.logging.control_logger_name) def process_response(self, req, resp, resource, req_succeeded): ctx = req.context diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index cbef9b9b..8742d83c 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -45,7 +45,7 @@ class MaasNodeDriver(NodeDriver): def __init__(self, **kwargs): super(MaasNodeDriver, self).__init__(**kwargs) - config.conf.register_opts(maasdriver_options, group='maasdriver') + config.conf.register_opts(MaasNodeDriver.maasdriver_options, group=MaasNodeDriver.driver_key) self.logger = logging.getLogger("%s.%s" % (config.conf.logging.nodedriver_logger_name, self.driver_key)) diff --git a/drydock_provisioner/drivers/oob/manual_driver/driver.py b/drydock_provisioner/drivers/oob/manual_driver/driver.py index 6e483aef..ab64d3a8 100644 --- a/drydock_provisioner/drivers/oob/manual_driver/driver.py +++ b/drydock_provisioner/drivers/oob/manual_driver/driver.py @@ -14,7 +14,7 @@ import time import logging -import drydock_provisioner +import drydock_provisioner.config as config import drydock_provisioner.error as errors @@ -37,7 +37,7 @@ class ManualDriver(oob.OobDriver): self.driver_desc = "Manual (Noop) OOB Driver" self.logger = logging.getLogger("%s.%s" % - (drydock_provisioner.conf.logging.oobdriver_logger_name, self.driver_key)) + (config.conf.logging.oobdriver_logger_name, self.driver_key)) def execute_task(self, task_id): task = self.state_manager.get_task(task_id) diff --git a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py index f1c8331d..4505af4a 100644 --- a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py +++ b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py @@ -16,7 +16,7 @@ import logging from pyghmi.ipmi.command import Command -import drydock_provisioner +import drydock_provisioner.config as config import drydock_provisioner.error as errors import drydock_provisioner.objects.fields as hd_fields @@ -38,7 +38,7 @@ class PyghmiDriver(oob.OobDriver): self.driver_desc = "Pyghmi OOB Driver" self.logger = logging.getLogger("%s.%s" % - (drydock_provisioner.conf.logging.oobdriver_logger_name, self.driver_key)) + (config.conf.logging.oobdriver_logger_name, self.driver_key)) def execute_task(self, task_id): task = self.state_manager.get_task(task_id) @@ -101,7 +101,7 @@ class PyghmiDriver(oob.OobDriver): attempts = 0 while (len(incomplete_subtasks) > 0 and - attempts <= getattr(drydock_provisioner.conf.timeouts, task.action, drydock_provisioner.conf.timeouts.drydock_timeout)): + attempts <= getattr(config.conf.timeouts, task.action, config.conf.timeouts.drydock_timeout)): for n in incomplete_subtasks: t = self.state_manager.get_task(n) if t.get_status() in [hd_fields.TaskStatus.Terminated, From 6facad6c06e1d93b6bc8d96cdd7fc64d423d8636 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Mon, 19 Jun 2017 16:34:35 -0500 Subject: [PATCH 13/16] Add secret tag to sensitive options Add debug logging of options values on startup --- drydock_provisioner/drydock.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drydock_provisioner/drydock.py b/drydock_provisioner/drydock.py index da4e9ce4..70c196ba 100644 --- a/drydock_provisioner/drydock.py +++ b/drydock_provisioner/drydock.py @@ -66,6 +66,9 @@ def start_drydock(): # Now that loggers are configured, log the effective config config.conf.log_opt_values(logging.getLogger(config.conf.logging.global_logger_name), logging.DEBUG) + # Now that loggers are configured, log the effective config + drydock_provisioner.conf.log_opt_values(logging.getLogger(drydock_provisioner.conf.logging.global_logger_name), logging.DEBUG) + return api.start_api(state_manager=state, ingester=input_ingester, orchestrator=orchestrator) From e9372826261721464246ba514e891e7d12a6e722 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Tue, 20 Jun 2017 14:51:47 -0500 Subject: [PATCH 14/16] DRYD-30 Support node tags and kernel parameters Create MaaS API model for Tags Update YAML schema to add 'platform' section to HostProfile/BaremetalNode Update YAML ingester to support platform section Add node_filter support to the /tasks API --- drydock_provisioner/control/tasks.py | 3 +- .../drivers/node/maasdriver/driver.py | 180 +++++++++++++++++- .../drivers/node/maasdriver/models/tag.py | 137 +++++++++++++ drydock_provisioner/ingester/plugins/yaml.py | 9 + drydock_provisioner/objects/hostprofile.py | 7 +- drydock_provisioner/orchestrator/__init__.py | 32 +++- examples/designparts_v1.0.yaml | 10 + examples/drydock.conf | 4 +- 8 files changed, 373 insertions(+), 9 deletions(-) create mode 100644 drydock_provisioner/drivers/node/maasdriver/models/tag.py diff --git a/drydock_provisioner/control/tasks.py b/drydock_provisioner/control/tasks.py index a4a49da9..fcca1eb9 100644 --- a/drydock_provisioner/control/tasks.py +++ b/drydock_provisioner/control/tasks.py @@ -37,6 +37,7 @@ class TasksResource(StatefulResource): sitename = json_data.get('sitename', None) design_id = json_data.get('design_id', None) action = json_data.get('action', None) + node_filter = json_data.get('node_filter', None) if sitename is None or design_id is None or action is None: self.info(req.context, "Task creation requires fields sitename, design_id, action") @@ -44,7 +45,7 @@ class TasksResource(StatefulResource): return task = self.orchestrator.create_task(obj_task.OrchestratorTask, site=sitename, - design_id=design_id, action=action) + design_id=design_id, action=action, node_filter=node_filter) task_thread = threading.Thread(target=self.orchestrator.execute_task, args=[task.get_id()]) task_thread.start() diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index 8742d83c..aec144a5 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -31,6 +31,7 @@ import drydock_provisioner.drivers.node.maasdriver.models.fabric as maas_fabric import drydock_provisioner.drivers.node.maasdriver.models.vlan as maas_vlan import drydock_provisioner.drivers.node.maasdriver.models.subnet as maas_subnet import drydock_provisioner.drivers.node.maasdriver.models.machine as maas_machine +import drydock_provisioner.drivers.node.maasdriver.models.tag as maas_tag class MaasNodeDriver(NodeDriver): maasdriver_options = [ @@ -364,7 +365,76 @@ class MaasNodeDriver(NodeDriver): status=hd_fields.TaskStatus.Complete, result=result, result_detail=result_detail) - elif task.action ==hd_fields.OrchestratorAction.DeployNode: + elif task.action == hd_fields.OrchestratorAction.ApplyNodePlatform: + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Running) + + self.logger.debug("Starting subtask to configure the platform on %s nodes." % (len(task.node_list))) + + subtasks = [] + + result_detail = { + 'detail': [], + 'failed_nodes': [], + 'successful_nodes': [], + } + + for n in task.node_list: + subtask = self.orchestrator.create_task(task_model.DriverTask, + parent_task_id=task.get_id(), design_id=design_id, + action=hd_fields.OrchestratorAction.ApplyNodePlatform, + site_name=task.site_name, + task_scope={'site': task.site_name, 'node_names': [n]}) + runner = MaasTaskRunner(state_manager=self.state_manager, + orchestrator=self.orchestrator, + task_id=subtask.get_id()) + + self.logger.info("Starting thread for task %s to config node %s platform" % (subtask.get_id(), n)) + + runner.start() + subtasks.append(subtask.get_id()) + + running_subtasks = len(subtasks) + attempts = 0 + worked = failed = False + + while running_subtasks > 0 and attempts < drydock_provisioner.conf.timeouts.apply_node_platform: + for t in subtasks: + subtask = self.state_manager.get_task(t) + + if subtask.status == hd_fields.TaskStatus.Complete: + self.logger.info("Task %s to configure node %s platform complete - status %s" % + (subtask.get_id(), n, subtask.get_result())) + running_subtasks = running_subtasks - 1 + + if subtask.result == hd_fields.ActionResult.Success: + result_detail['successful_nodes'].extend(subtask.node_list) + worked = True + elif subtask.result == hd_fields.ActionResult.Failure: + result_detail['failed_nodes'].extend(subtask.node_list) + failed = True + elif subtask.result == hd_fields.ActionResult.PartialSuccess: + worked = failed = True + + time.sleep(1 * 60) + attempts = attempts + 1 + + if running_subtasks > 0: + self.logger.warning("Time out for task %s before all subtask threads complete" % (task.get_id())) + result = hd_fields.ActionResult.DependentFailure + result_detail['detail'].append('Some subtasks did not complete before the timeout threshold') + elif worked and failed: + result = hd_fields.ActionResult.PartialSuccess + elif worked: + result = hd_fields.ActionResult.Success + else: + result = hd_fields.ActionResult.Failure + + self.orchestrator.task_field_update(task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=result, + result_detail=result_detail) + elif task.action == hd_fields.OrchestratorAction.DeployNode: self.orchestrator.task_field_update(task.get_id(), status=hd_fields.TaskStatus.Running) @@ -922,6 +992,114 @@ class MaasTaskRunner(drivers.DriverTaskRunner): else: final_result = hd_fields.ActionResult.Success + self.orchestrator.task_field_update(self.task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=final_result, + result_detail=result_detail) + elif task_action == hd_fields.OrchestratorAction.ApplyNodePlatform: + try: + machine_list = maas_machine.Machines(self.maas_client) + machine_list.refresh() + + tag_list = maas_tag.Tags(self.maas_client) + tag_list.refresh() + except Exception as ex: + self.logger.error("Error deploying node, cannot access MaaS: %s" % str(ex)) + traceback.print_tb(sys.last_traceback) + self.orchestrator.task_field_update(self.task.get_id(), + status=hd_fields.TaskStatus.Complete, + result=hd_fields.ActionResult.Failure, + result_detail={'detail': 'Error accessing MaaS API', 'retry': True}) + return + + nodes = self.task.node_list + + result_detail = {'detail': []} + + worked = failed = False + + for n in nodes: + try: + self.logger.debug("Locating node %s for platform configuration" % (n)) + + node = site_design.get_baremetal_node(n) + machine = machine_list.identify_baremetal_node(node, update_name=False) + + if machine is None: + self.logger.warning("Could not locate machine for node %s" % n) + result_detail['detail'].append("Could not locate machine for node %s" % n) + failed = True + continue + except Exception as ex1: + failed = True + self.logger.error("Error locating machine for node %s: %s" % (n, str(ex1))) + result_detail['detail'].append("Error locating machine for node %s" % (n)) + continue + + try: + # Render the string of all kernel params for the node + kp_string = "" + + for k,v in getattr(node, 'kernel_params', {}).items(): + if v == 'True': + kp_string = kp_string + " %s" % (k) + else: + kp_string = kp_string + " %s=%s" % (k, v) + + if kp_string: + # Check if the node has an existing kernel params tag + node_kp_tag = tag_list.select("%s_kp" % (node.name)) + self.logger.info("Configuring kernel parameters for node %s" % (node.name)) + + if node_kp_tag is None: + self.logger.debug("Creating kernel_params tag for node %s: %s" % (node.name, kp_string)) + node_kp_tag = maas_tag.Tag(self.maas_client, name="%s_kp" % (node.name), kernel_opts=kp_string) + node_kp_tag = tag_list.add(node_kp_tag) + node_kp_tag.apply_to_node(machine.resource_id) + else: + self.logger.debug("Updating tag %s for node %s: %s" % (node_kp_tag.resource_id, node.name, kp_string)) + node_kp_tag.kernel_opts = kp_string + node_kp_tag.update() + + self.logger.info("Applied kernel parameters to node %s" % n) + result_detail['detail'].append("Applied kernel parameters to node %s" % (node.name)) + worked = True + except Exception as ex2: + failed = True + result_detail['detail'].append("Error configuring kernel parameters for node %s" % (n)) + self.logger.error("Error configuring kernel parameters for node %s: %s" % (n, str(ex2))) + continue + + try: + if node.tags is not None and len(node.tags) > 0: + self.logger.info("Configuring static tags for node %s" % (node.name)) + + for t in node.tags: + tag_list.refresh() + tag = tag_list.select(t) + + if tag is None: + self.logger.debug("Creating static tag %s" % t) + tag = maas_tag.Tag(self.maas_client, name=t) + tag = tag_list.add(tag) + + self.logger.debug("Applying tag %s to node %s" % (tag.resource_id, machine.resource_id)) + tag.apply_to_node(machine.resource_id) + + self.logger.info("Applied static tags to node %s" % (node.name)) + result_detail['detail'].append("Applied static tags to node %s" % (node.name)) + worked = True + except Exception as ex3: + failed = True + result_detail['detail'].append("Error configuring static tags for node %s" % (node.name)) + self.logger.error("Error configuring static tags for node %s: %s" % (node.name, str(ex3))) + continue + + if failed: + final_result = hd_fields.ActionResult.Failure + else: + final_result = hd_fields.ActionResult.Success + self.orchestrator.task_field_update(self.task.get_id(), status=hd_fields.TaskStatus.Complete, result=final_result, diff --git a/drydock_provisioner/drivers/node/maasdriver/models/tag.py b/drydock_provisioner/drivers/node/maasdriver/models/tag.py new file mode 100644 index 00000000..f55b4f6a --- /dev/null +++ b/drydock_provisioner/drivers/node/maasdriver/models/tag.py @@ -0,0 +1,137 @@ +# Copyright 2017 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. + +import drydock_provisioner.error as errors +import drydock_provisioner.drivers.node.maasdriver.models.base as model_base + +import yaml + +class Tag(model_base.ResourceBase): + + resource_url = 'tags/{resource_id}/' + fields = ['resource_id', 'name', 'defintion', 'kernel_opts'] + json_fields = ['name','kernel_opts', 'comment', 'definition'] + + def __init__(self, api_client, **kwargs): + super(Tag, self).__init__(api_client, **kwargs) + + def get_applied_nodes(self): + """ + Query the list of nodes this tag is currently applied to + + :return: List of MaaS system_ids of nodes + """ + + url = self.interpolate_url() + + resp = self.api_client.get(url, op='nodes') + + if resp.status_code == 200: + resp_json = resp.json() + system_id_list = [] + + for n in resp_json: + system_id_list.append(n.get('system_id')) + + return system_id_list + else: + self.logger.error("Error retrieving node/tag pairs, received HTTP %s from MaaS" % resp.status_code) + self.logger.debug("MaaS response: %s" % resp.text) + raise errors.DriverError("Error retrieving node/tag pairs, received HTTP %s from MaaS" % resp.status_code) + + def apply_to_node(self, system_id): + """ + Apply this tag to a MaaS node + + :param system_id: MaaS system_id of the node + """ + + if system_id in self.get_applied_nodes(): + self.logger.debug("Tag %s already applied to node %s" % (self.name, system_id)) + else: + url = self.interpolate_url() + + resp = self.api_client.post(url, op='update_nodes', files={'add': system_id}) + + if not resp.ok: + self.logger.error("Error applying tag to node, received HTTP %s from MaaS" % resp.status_code) + self.logger.debug("MaaS response: %s" % resp.text) + raise errors.DriverError("Error applying tag to node, received HTTP %s from MaaS" % resp.status_code) + + def to_dict(self): + """ + Serialize this resource instance into a dict matching the + MAAS representation of the resource + """ + data_dict = {} + + for f in self.json_fields: + if getattr(self, f, None) is not None: + if f == 'resource_id': + data_dict['name'] = getattr(self, f) + else: + data_dict[f] = getattr(self, f) + + return data_dict + + @classmethod + def from_dict(cls, api_client, obj_dict): + """ + Create a instance of this resource class based on a dict + of MaaS type attributes + + Customized for Tag due to use of name instead of id + as resource key + + :param api_client: Instance of api_client.MaasRequestFactory for accessing MaaS API + :param obj_dict: Python dict as parsed from MaaS API JSON representing this resource type + """ + + refined_dict = {k: obj_dict.get(k, None) for k in cls.fields} + + if 'name' in obj_dict.keys(): + refined_dict['resource_id'] = obj_dict.get('name') + + i = cls(api_client, **refined_dict) + return i + +class Tags(model_base.ResourceCollectionBase): + + collection_url = 'tags/' + collection_resource = Tag + + def __init__(self, api_client, **kwargs): + super(Tags, self).__init__(api_client) + + def add(self, res): + """ + Create a new resource in this collection in MaaS + + Customize as Tag resources use 'name' instead of 'id' + + :param res: Instance of cls.collection_resource + """ + data_dict = res.to_dict() + url = self.interpolate_url() + + resp = self.api_client.post(url, files=data_dict) + + if resp.status_code == 200: + resp_json = resp.json() + res.set_resource_id(resp_json.get('name')) + return res + + raise errors.DriverError("Failed updating MAAS url %s - return code %s" + % (url, resp.status_code)) + diff --git a/drydock_provisioner/ingester/plugins/yaml.py b/drydock_provisioner/ingester/plugins/yaml.py index b179bb99..b566cf91 100644 --- a/drydock_provisioner/ingester/plugins/yaml.py +++ b/drydock_provisioner/ingester/plugins/yaml.py @@ -304,6 +304,15 @@ class YamlIngester(IngesterPlugin): model.interfaces.append(int_model) + platform = spec.get('platform', {}) + + model.image = platform.get('image', None) + model.kernel = platform.get('kernel', None) + + model.kernel_params = {} + for k,v in platform.get('kernel_params', {}).items(): + model.kernel_params[k] = v + model.primary_network = spec.get('primary_network', None) node_metadata = spec.get('metadata', {}) diff --git a/drydock_provisioner/objects/hostprofile.py b/drydock_provisioner/objects/hostprofile.py index bc367932..cc63a353 100644 --- a/drydock_provisioner/objects/hostprofile.py +++ b/drydock_provisioner/objects/hostprofile.py @@ -47,8 +47,9 @@ class HostProfile(base.DrydockPersistentObject, base.DrydockObject): 'owner_data': obj_fields.DictOfStringsField(nullable=True), 'rack': obj_fields.StringField(nullable=True), 'base_os': obj_fields.StringField(nullable=True), + 'image': obj_fields.StringField(nullable=True), 'kernel': obj_fields.StringField(nullable=True), - 'kernel_params': obj_fields.StringField(nullable=True), + 'kernel_params': obj_fields.DictOfStringsField(nullable=True), 'primary_network': obj_fields.StringField(nullable=False), } @@ -91,7 +92,7 @@ class HostProfile(base.DrydockPersistentObject, base.DrydockObject): inheritable_field_list = [ 'hardware_profile', 'oob_type', 'storage_layout', 'bootdisk_device', 'bootdisk_root_size', 'bootdisk_boot_size', - 'rack', 'base_os', 'kernel', 'kernel_params', 'primary_network'] + 'rack', 'base_os', 'image', 'kernel', 'primary_network'] # Create applied data from self design values and parent # applied values @@ -108,6 +109,8 @@ class HostProfile(base.DrydockPersistentObject, base.DrydockObject): self.owner_data = objects.Utils.merge_dicts(self.owner_data, parent.owner_data) + self.kernel_params = objects.Utils.merge_dicts(self.kernel_params, parent.kernel_params) + self.interfaces = HostInterfaceList.from_basic_list( HostInterface.merge_lists(self.interfaces, parent.interfaces)) diff --git a/drydock_provisioner/orchestrator/__init__.py b/drydock_provisioner/orchestrator/__init__.py index c639d8d0..037907c0 100644 --- a/drydock_provisioner/orchestrator/__init__.py +++ b/drydock_provisioner/orchestrator/__init__.py @@ -467,13 +467,37 @@ class Orchestrator(object): if len(node_networking_task.result_detail['successful_nodes']) > 0: - self.logger.info("Found %s successfully networked nodes, starting deployment." % - (len(node_networking_task.result_detail['successful_nodes']))) + self.logger.info("Found %s successfully networked nodes, configuring platform." % + (len(node_networking_task.result_detail['successful_nodes']))) + + node_platform_task = self.create_task(tasks.DriverTask, + parent_task_id=task.get_id(), design_id=design_id, + action=hd_fields.OrchestratorAction.ApplyNodePlatform, + task_scope={'site': task_site, + 'node_names': node_networking_task.result_detail['successful_nodes']}) + self.logger.info("Starting node driver task %s to configure node platform." % (node_platform_task.get_id())) + + node_driver.execute_task(node_platform_task.get_id()) + + node_platform_task = self.state_manager.get_task(node_platform_task.get_id()) + + if node_platform_task.get_result() in [hd_fields.ActionResult.Success, + hd_fields.ActionResult.PartialSuccess]: + worked = True + elif node_platform_task.get_result() in [hd_fields.ActionResult.Failure, + hd_fields.ActionResult.PartialSuccess]: + failed = True + else: + self.logger.warning("No nodes successfully networked, skipping platform configuration subtask") + + if len(node_platform_task.result_detail['successful_nodes']) > 0: + self.logger.info("Configured platform on %s nodes, starting deployment." % + (len(node_platform_task.result_detail['successful_nodes']))) node_deploy_task = self.create_task(tasks.DriverTask, parent_task_id=task.get_id(), design_id=design_id, action=hd_fields.OrchestratorAction.DeployNode, task_scope={'site': task_site, - 'node_names': node_networking_task.result_detail['successful_nodes']}) + 'node_names': node_platform_task.result_detail['successful_nodes']}) self.logger.info("Starting node driver task %s to deploy nodes." % (node_deploy_task.get_id())) node_driver.execute_task(node_deploy_task.get_id()) @@ -487,7 +511,7 @@ class Orchestrator(object): hd_fields.ActionResult.PartialSuccess]: failed = True else: - self.logger.warning("No nodes successfully networked, skipping deploy subtask") + self.logger.warning("Unable to configure platform on any nodes, skipping deploy subtask") final_result = None if worked and failed: diff --git a/examples/designparts_v1.0.yaml b/examples/designparts_v1.0.yaml index 0ca7011c..31a33e2d 100644 --- a/examples/designparts_v1.0.yaml +++ b/examples/designparts_v1.0.yaml @@ -224,6 +224,16 @@ spec: networks: - 'mgmt' - 'admin' + # Settings for the platform (operating system) + platform: + # Which image to deploy on the node, must be available in the provisioner. Defaults to 'ubuntu/xenial' + image: + # Which kernel to enable. Defaults to generic, can also be hwe (hardware enablement) + kernel: generic + # K/V list of kernel parameters to configure on boot. No default. Use value of true for params that are just flags + kernel_params: + console: tty1 + quiet: true # Metadata about the node metadata: # Explicit tags to propagate to Kubernetes. Simple strings of any value diff --git a/examples/drydock.conf b/examples/drydock.conf index 48db0e8d..48d6cf3d 100644 --- a/examples/drydock.conf +++ b/examples/drydock.conf @@ -39,8 +39,10 @@ create_network_template = 2 identify_node = 10 configure_hardware = 30 apply_node_networking = 5 +apply_node_platform = 5 deploy_node = 45 [maasdriver] maas_api_url = 'http://localhost:8000/MAAS/api/2.0/' -maas_api_key = 'your:secret:key' \ No newline at end of file +maas_api_key = 'your:secret:key' + From 064395fea8ea2712bba732e6d8953301c9488ded Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Wed, 28 Jun 2017 13:08:42 -0500 Subject: [PATCH 15/16] Convert to cfg.CONF pattern --- drydock_provisioner/config.py | 7 +---- drydock_provisioner/control/middleware.py | 6 ++--- .../drivers/node/maasdriver/api_client.py | 5 ++-- .../drivers/node/maasdriver/driver.py | 26 +++++++++---------- .../drivers/oob/manual_driver/driver.py | 7 +++-- .../drivers/oob/pyghmi_driver/__init__.py | 9 +++---- drydock_provisioner/drydock.py | 22 +++++++--------- drydock_provisioner/orchestrator/__init__.py | 3 +-- 8 files changed, 36 insertions(+), 49 deletions(-) diff --git a/drydock_provisioner/config.py b/drydock_provisioner/config.py index 4cc7d0ff..d92844a1 100644 --- a/drydock_provisioner/config.py +++ b/drydock_provisioner/config.py @@ -67,8 +67,6 @@ class DrydockConfig(object): cfg.StrOpt('node_driver', default='drydock_provisioner.drivers.node.maasdriver.driver.MaasNodeDriver', help='Module path string of the Node driver to enable'), - cfg.StrOpt('network_driver', - default=None, help='Module path string of the Network driver to enable'), ] # Timeouts for various tasks specified in minutes @@ -82,7 +80,7 @@ class DrydockConfig(object): ] def __init__(self): - self.conf = cfg.ConfigOpts() + self.conf = cfg.CONF def register_options(self): self.conf.register_opts(DrydockConfig.logging_options, group='logging') @@ -90,9 +88,6 @@ class DrydockConfig(object): self.conf.register_opts(DrydockConfig.plugin_options, group='plugins') self.conf.register_opts(DrydockConfig.timeout_options, group='timeouts') -config_mgr = DrydockConfig() -conf = config_mgr.conf - IGNORED_MODULES = ('drydock', 'config') def list_opts(): diff --git a/drydock_provisioner/control/middleware.py b/drydock_provisioner/control/middleware.py index 875e9a6c..acbdb27e 100644 --- a/drydock_provisioner/control/middleware.py +++ b/drydock_provisioner/control/middleware.py @@ -16,7 +16,7 @@ import falcon import logging import uuid -import drydock_provisioner.config as config +from oslo_config import cfg class AuthMiddleware(object): @@ -66,7 +66,7 @@ class ContextMiddleware(object): requested_logging = req.get_header('X-Log-Level') - if (config.conf.logging.log_level == 'DEBUG' or + if (cfg.CONF.logging.log_level == 'DEBUG' or (requested_logging == 'DEBUG' and 'admin' in ctx.roles)): ctx.set_log_level('DEBUG') elif requested_logging == 'INFO': @@ -78,7 +78,7 @@ class ContextMiddleware(object): class LoggingMiddleware(object): def __init__(self): - self.logger = logging.getLogger(config.conf.logging.control_logger_name) + self.logger = logging.getLogger(cfg.CONF.logging.control_logger_name) def process_response(self, req, resp, resource, req_succeeded): ctx = req.context diff --git a/drydock_provisioner/drivers/node/maasdriver/api_client.py b/drydock_provisioner/drivers/node/maasdriver/api_client.py index 789349b6..7aa4b0fb 100644 --- a/drydock_provisioner/drivers/node/maasdriver/api_client.py +++ b/drydock_provisioner/drivers/node/maasdriver/api_client.py @@ -44,7 +44,6 @@ class MaasRequestFactory(object): self.base_url = base_url self.apikey = apikey - print("Creating MaaS API client for URL %s with key %s" % (base_url, apikey)) self.signer = MaasOauth(apikey) self.http_session = requests.Session() @@ -143,7 +142,7 @@ class MaasRequestFactory(object): resp = self.http_session.send(prepared_req, timeout=timeout) if resp.status_code >= 400: - print("FAILED API CALL:\nURL: %s %s\nBODY:\n%s\nRESPONSE: %s\nBODY:\n%s" % + self.logger.debug("FAILED API CALL:\nURL: %s %s\nBODY:\n%s\nRESPONSE: %s\nBODY:\n%s" % (prepared_req.method, prepared_req.url, str(prepared_req.body).replace('\\r\\n','\n'), resp.status_code, resp.text)) - return resp \ No newline at end of file + return resp diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index aec144a5..e5b86121 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -18,7 +18,6 @@ import sys from oslo_config import cfg -import drydock_provisioner.config as config import drydock_provisioner.error as errors import drydock_provisioner.drivers as drivers import drydock_provisioner.objects.fields as hd_fields @@ -46,10 +45,9 @@ class MaasNodeDriver(NodeDriver): def __init__(self, **kwargs): super(MaasNodeDriver, self).__init__(**kwargs) - config.conf.register_opts(MaasNodeDriver.maasdriver_options, group=MaasNodeDriver.driver_key) + cfg.CONF.register_opts(MaasNodeDriver.maasdriver_options, group=MaasNodeDriver.driver_key) - self.logger = logging.getLogger("%s.%s" % - (config.conf.logging.nodedriver_logger_name, self.driver_key)) + self.logger = logging.getLogger(cfg.CONF.logging.nodedriver_logger_name) def execute_task(self, task_id): task = self.state_manager.get_task(task_id) @@ -64,7 +62,7 @@ class MaasNodeDriver(NodeDriver): if task.action == hd_fields.OrchestratorAction.ValidateNodeServices: self.orchestrator.task_field_update(task.get_id(), status=hd_fields.TaskStatus.Running) - maas_client = MaasRequestFactory(config.conf.maasdriver.maas_api_url, config.conf.maasdriver.maas_api_key) + maas_client = MaasRequestFactory(cfg.CONF.maasdriver.maas_api_url, cfg.CONF.maasdriver.maas_api_key) try: if maas_client.test_connectivity(): @@ -136,7 +134,7 @@ class MaasNodeDriver(NodeDriver): runner.start() - runner.join(timeout=config.conf.timeouts.create_network_template * 60) + runner.join(timeout=cfg.CONF.timeouts.create_network_template * 60) if runner.is_alive(): result = { @@ -190,7 +188,7 @@ class MaasNodeDriver(NodeDriver): attempts = 0 worked = failed = False - while running_subtasks > 0 and attempts < config.conf.timeouts.identify_node: + while running_subtasks > 0 and attempts < cfg.CONF.timeouts.identify_node: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -260,7 +258,7 @@ class MaasNodeDriver(NodeDriver): worked = failed = False #TODO Add timeout to config - while running_subtasks > 0 and attempts < config.conf.timeouts.configure_hardware: + while running_subtasks > 0 and attempts < cfg.CONF.timeouts.configure_hardware: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -329,7 +327,7 @@ class MaasNodeDriver(NodeDriver): attempts = 0 worked = failed = False - while running_subtasks > 0 and attempts < config.conf.timeouts.apply_node_networking: + while running_subtasks > 0 and attempts < cfg.CONF.timeouts.apply_node_networking: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -467,7 +465,7 @@ class MaasNodeDriver(NodeDriver): attempts = 0 worked = failed = False - while running_subtasks > 0 and attempts < config.conf.timeouts.deploy_node: + while running_subtasks > 0 and attempts < cfg.CONF.timeouts.deploy_node: for t in subtasks: subtask = self.state_manager.get_task(t) @@ -519,8 +517,8 @@ class MaasTaskRunner(drivers.DriverTaskRunner): status=hd_fields.TaskStatus.Running, result=hd_fields.ActionResult.Incomplete) - self.maas_client = MaasRequestFactory(config.conf.maasdriver.maas_api_url, - config.conf.maasdriver.maas_api_key) + self.maas_client = MaasRequestFactory(cfg.CONF.maasdriver.maas_api_url, + cfg.CONF.maasdriver.maas_api_key) site_design = self.orchestrator.get_effective_site(self.task.design_id) @@ -809,7 +807,7 @@ class MaasTaskRunner(drivers.DriverTaskRunner): # Poll machine status attempts = 0 - while attempts < config.conf.timeouts.configure_hardware and machine.status_name != 'Ready': + while attempts < cfg.CONF.timeouts.configure_hardware and machine.status_name != 'Ready': attempts = attempts + 1 time.sleep(1 * 60) try: @@ -1149,7 +1147,7 @@ class MaasTaskRunner(drivers.DriverTaskRunner): continue attempts = 0 - while attempts < config.conf.timeouts.deploy_node and not machine.status_name.startswith('Deployed'): + while attempts < cfg.CONF.timeouts.deploy_node and not machine.status_name.startswith('Deployed'): attempts = attempts + 1 time.sleep(1 * 60) try: diff --git a/drydock_provisioner/drivers/oob/manual_driver/driver.py b/drydock_provisioner/drivers/oob/manual_driver/driver.py index ab64d3a8..9d513144 100644 --- a/drydock_provisioner/drivers/oob/manual_driver/driver.py +++ b/drydock_provisioner/drivers/oob/manual_driver/driver.py @@ -14,9 +14,9 @@ import time import logging -import drydock_provisioner.config as config -import drydock_provisioner.error as errors +from oslo_config import cfg +import drydock_provisioner.error as errors import drydock_provisioner.objects.fields as hd_fields import drydock_provisioner.objects.task as task_model @@ -36,8 +36,7 @@ class ManualDriver(oob.OobDriver): self.driver_key = "manual_driver" self.driver_desc = "Manual (Noop) OOB Driver" - self.logger = logging.getLogger("%s.%s" % - (config.conf.logging.oobdriver_logger_name, self.driver_key)) + self.logger = logging.getLogger(cfg.CONF.logging.oobdriver_logger_name) def execute_task(self, task_id): task = self.state_manager.get_task(task_id) diff --git a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py index 4505af4a..6174f3e0 100644 --- a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py +++ b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py @@ -14,9 +14,9 @@ import time import logging +from oslo_config import cfg from pyghmi.ipmi.command import Command -import drydock_provisioner.config as config import drydock_provisioner.error as errors import drydock_provisioner.objects.fields as hd_fields @@ -37,8 +37,7 @@ class PyghmiDriver(oob.OobDriver): self.driver_key = "pyghmi_driver" self.driver_desc = "Pyghmi OOB Driver" - self.logger = logging.getLogger("%s.%s" % - (config.conf.logging.oobdriver_logger_name, self.driver_key)) + self.logger = logging.getLogger(cfg.CONF.logging.oobdriver_logger_name) def execute_task(self, task_id): task = self.state_manager.get_task(task_id) @@ -101,7 +100,7 @@ class PyghmiDriver(oob.OobDriver): attempts = 0 while (len(incomplete_subtasks) > 0 and - attempts <= getattr(config.conf.timeouts, task.action, config.conf.timeouts.drydock_timeout)): + attempts <= getattr(cfg.CONF.timeouts, task.action, cfg.CONF.timeouts.drydock_timeout)): for n in incomplete_subtasks: t = self.state_manager.get_task(n) if t.get_status() in [hd_fields.TaskStatus.Terminated, @@ -298,4 +297,4 @@ class PyghmiTaskRunner(drivers.DriverTaskRunner): result=hd_fields.ActionResult.Success, status=hd_fields.TaskStatus.Complete, result_detail=mci_id) - return \ No newline at end of file + return diff --git a/drydock_provisioner/drydock.py b/drydock_provisioner/drydock.py index 70c196ba..f734c32b 100644 --- a/drydock_provisioner/drydock.py +++ b/drydock_provisioner/drydock.py @@ -30,24 +30,24 @@ def start_drydock(): cfg.BoolOpt('debug', short='d', default=False, help='Enable debug logging'), ] - config.conf.register_cli_opts(cli_options) + cfg.CONF.register_cli_opts(cli_options) config.config_mgr.register_options() - config.conf(sys.argv[1:]) + cfg.CONF(sys.argv[1:]) - if config.conf.debug: - config.conf.logging.log_level = 'DEBUG' + if cfg.CONF.debug: + cfg.CONF.logging.log_level = 'DEBUG' # Setup root logger - logger = logging.getLogger(config.conf.logging.global_logger_name) + logger = logging.getLogger(cfg.CONF.logging.global_logger_name) - logger.setLevel(config.conf.logging.log_level) + logger.setLevel(cfg.CONF.logging.log_level) ch = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s:%(funcName)s - %(message)s') ch.setFormatter(formatter) logger.addHandler(ch) # Specalized format for API logging - logger = logging.getLogger(config.conf.logging.control_logger_name) + logger = logging.getLogger(cfg.CONF.logging.control_logger_name) logger.propagate = False formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(user)s - %(req_id)s - %(external_ctx)s - %(message)s') @@ -55,16 +55,14 @@ def start_drydock(): ch.setFormatter(formatter) logger.addHandler(ch) - state = statemgmt.DesignState() - orchestrator = orch.Orchestrator(config.conf.plugins, - state_manager=state) + orchestrator = orch.Orchestrator(cfg.CONF.plugins, state_manager=state) input_ingester = ingester.Ingester() - input_ingester.enable_plugins(config.conf.plugins.ingester) + input_ingester.enable_plugins(cfg.CONF.plugins.ingester) # Now that loggers are configured, log the effective config - config.conf.log_opt_values(logging.getLogger(config.conf.logging.global_logger_name), logging.DEBUG) + cfg.CONF.log_opt_values(logging.getLogger(cfg.CONF.logging.global_logger_name), logging.DEBUG) # Now that loggers are configured, log the effective config drydock_provisioner.conf.log_opt_values(logging.getLogger(drydock_provisioner.conf.logging.global_logger_name), logging.DEBUG) diff --git a/drydock_provisioner/orchestrator/__init__.py b/drydock_provisioner/orchestrator/__init__.py index 037907c0..25eedbbb 100644 --- a/drydock_provisioner/orchestrator/__init__.py +++ b/drydock_provisioner/orchestrator/__init__.py @@ -41,10 +41,9 @@ class Orchestrator(object): # This is because oslo_config changes the option value # for multiopt depending on if multiple values are actually defined - print("%s" % (oob_drivers)) for d in oob_drivers: - print("Enabling OOB driver %s" % d) + self.logger.info("Enabling OOB driver %s" % d) if d is not None: m, c = d.rsplit('.', 1) oob_driver_class = \ From b097054e5e47c10ec31a96e8e1095dcb5a4f73da Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Wed, 28 Jun 2017 14:55:22 -0500 Subject: [PATCH 16/16] Fixes from PR review Added PartialSuccess logic for ApplyNodePlatform task A few stylistic updates --- drydock_provisioner/drivers/node/maasdriver/driver.py | 6 ++++-- drydock_provisioner/drivers/node/maasdriver/models/tag.py | 6 ++++-- drydock_provisioner/ingester/plugins/yaml.py | 2 +- drydock_provisioner/orchestrator/__init__.py | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index e5b86121..5e16ef24 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -1038,7 +1038,7 @@ class MaasTaskRunner(drivers.DriverTaskRunner): # Render the string of all kernel params for the node kp_string = "" - for k,v in getattr(node, 'kernel_params', {}).items(): + for k, v in getattr(node, 'kernel_params', {}).items(): if v == 'True': kp_string = kp_string + " %s" % (k) else: @@ -1093,7 +1093,9 @@ class MaasTaskRunner(drivers.DriverTaskRunner): self.logger.error("Error configuring static tags for node %s: %s" % (node.name, str(ex3))) continue - if failed: + if worked and failed: + final_result = hd_fields.ActionResult.PartialSuccess + elif failed: final_result = hd_fields.ActionResult.Failure else: final_result = hd_fields.ActionResult.Success diff --git a/drydock_provisioner/drivers/node/maasdriver/models/tag.py b/drydock_provisioner/drivers/node/maasdriver/models/tag.py index f55b4f6a..d9d4f263 100644 --- a/drydock_provisioner/drivers/node/maasdriver/models/tag.py +++ b/drydock_provisioner/drivers/node/maasdriver/models/tag.py @@ -42,7 +42,9 @@ class Tag(model_base.ResourceBase): system_id_list = [] for n in resp_json: - system_id_list.append(n.get('system_id')) + system_id = n.get('system_id', None) + if system_id is not None: + system_id_list.append(system_id) return system_id_list else: @@ -100,7 +102,7 @@ class Tag(model_base.ResourceBase): refined_dict = {k: obj_dict.get(k, None) for k in cls.fields} - if 'name' in obj_dict.keys(): + if 'name' in obj_dict: refined_dict['resource_id'] = obj_dict.get('name') i = cls(api_client, **refined_dict) diff --git a/drydock_provisioner/ingester/plugins/yaml.py b/drydock_provisioner/ingester/plugins/yaml.py index b566cf91..cc2c4403 100644 --- a/drydock_provisioner/ingester/plugins/yaml.py +++ b/drydock_provisioner/ingester/plugins/yaml.py @@ -310,7 +310,7 @@ class YamlIngester(IngesterPlugin): model.kernel = platform.get('kernel', None) model.kernel_params = {} - for k,v in platform.get('kernel_params', {}).items(): + for k, v in platform.get('kernel_params', {}).items(): model.kernel_params[k] = v model.primary_network = spec.get('primary_network', None) diff --git a/drydock_provisioner/orchestrator/__init__.py b/drydock_provisioner/orchestrator/__init__.py index 25eedbbb..087d36b0 100644 --- a/drydock_provisioner/orchestrator/__init__.py +++ b/drydock_provisioner/orchestrator/__init__.py @@ -483,7 +483,7 @@ class Orchestrator(object): if node_platform_task.get_result() in [hd_fields.ActionResult.Success, hd_fields.ActionResult.PartialSuccess]: worked = True - elif node_platform_task.get_result() in [hd_fields.ActionResult.Failure, + if node_platform_task.get_result() in [hd_fields.ActionResult.Failure, hd_fields.ActionResult.PartialSuccess]: failed = True else: