From adf07eead8f751a031e9c11e813715a6d0fea5c6 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Thu, 2 Nov 2017 15:37:35 -0500 Subject: [PATCH] Boot action signal API - Implement boot action signal API - Implement boot action signal unit tests - Implement orchestrator step to wait for boot action reports before marking a deploy_nodes task a complete Change-Id: I0098e66dd2cd65f349274914dd25cbdf44194f78 --- drydock_provisioner/control/bootaction.py | 105 +++++++++++++-- drydock_provisioner/objects/fields.py | 13 +- drydock_provisioner/objects/task.py | 31 +++-- .../orchestrator/actions/orchestrator.py | 97 +++++++++++++- .../orchestrator/orchestrator.py | 16 ++- drydock_provisioner/statemgmt/db/tables.py | 5 +- drydock_provisioner/statemgmt/state.py | 101 ++++++++------ .../postgres/test_api_bootaction.py | 5 +- .../postgres/test_api_bootaction_status.py | 123 ++++++++++++++++++ .../test_postgres_bootaction_status.py | 70 ++++++++++ tests/unit/test_bootaction_asset_render.py | 1 + tests/unit/test_bootaction_scoping.py | 1 + tests/unit/test_design_inheritance.py | 1 + tests/unit/test_ingester.py | 1 + tests/unit/test_ingester_bootaction.py | 1 + tests/unit/test_ingester_yaml.py | 1 + tests/unit/test_orch_node_filter.py | 1 + 17 files changed, 492 insertions(+), 81 deletions(-) create mode 100644 tests/integration/postgres/test_api_bootaction_status.py create mode 100644 tests/integration/postgres/test_postgres_bootaction_status.py diff --git a/drydock_provisioner/control/bootaction.py b/drydock_provisioner/control/bootaction.py index 1aee068b..a1da9cb4 100644 --- a/drydock_provisioner/control/bootaction.py +++ b/drydock_provisioner/control/bootaction.py @@ -13,17 +13,51 @@ # limitations under the License. """Handle resources for boot action API endpoints. """ -import falcon -import ulid2 import tarfile import io import logging +import jsonschema +import json +import ulid2 +import falcon + +from drydock_provisioner.objects.fields import ActionResult +import drydock_provisioner.objects as objects + from .base import StatefulResource logger = logging.getLogger('drydock') class BootactionResource(StatefulResource): + bootaction_schema = { + '$schema': 'http://json-schema.org/schema#', + 'type': 'object', + 'additionalProperties': False, + 'properties': { + 'status': { + 'type': 'string', + 'enum': ['Failure', 'Success', 'failure', 'success'], + }, + 'details': { + 'type': 'array', + 'items': { + 'type': 'object', + 'additionalProperties': True, + 'properties': { + 'message': { + 'type': 'string', + }, + 'error': { + 'type': 'boolean', + } + }, + 'required': ['message', 'error'], + }, + }, + }, + } + def __init__(self, orchestrator=None, **kwargs): super().__init__(**kwargs) self.orchestrator = orchestrator @@ -38,6 +72,60 @@ class BootactionResource(StatefulResource): :param resp: falcone response :param action_id: ULID ID of the boot action """ + try: + ba_entry = self.state_manager.get_boot_action(action_id) + except Exception as ex: + self.logger.error( + "Error querying for boot action %s" % action_id, exc_info=ex) + raise falcon.HTTPInternalServerError(str(ex)) + + if ba_entry is None: + raise falcon.HTTPNotFound() + + BootactionUtils.check_auth(ba_entry, req) + + try: + json_body = self.req_json(req) + jsonschema.validate(json_body, + BootactionResource.bootaction_schema) + except Exception as ex: + self.logger.error("Error processing boot action body", exc_info=ex) + raise falcon.HTTPBadRequest(description="Error processing body.") + + if ba_entry.get('action_status').lower() != ActionResult.Incomplete: + self.logger.info( + "Attempt to update boot action %s after status finalized." % + action_id) + raise falcon.HTTPConflict( + description= + "Action %s status finalized, not available for update." % + action_id) + + for m in json_body.get('details', []): + rm = objects.TaskStatusMessage( + m.get('message'), m.get('error'), 'bootaction', action_id) + for f, v in m.items(): + if f not in ['message', 'error']: + rm['extra'] = dict() + rm['extra'][f] = v + + self.state_manager.post_result_message(ba_entry['task_id'], rm) + + new_status = json_body.get('status', None) + + if new_status is not None: + self.state_manager.put_bootaction_status( + action_id, action_status=new_status.lower()) + + ba_entry = self.state_manager.get_boot_action(action_id) + ba_entry.pop('identity_key') + resp.status = falcon.HTTP_200 + resp.content_type = 'application/json' + ba_entry['task_id'] = str(ba_entry['task_id']) + ba_entry['action_id'] = ulid2.encode_ulid_base32( + ba_entry['action_id']) + resp.body = json.dumps(ba_entry) + return class BootactionAssetsResource(StatefulResource): @@ -79,18 +167,19 @@ class BootactionAssetsResource(StatefulResource): task.design_ref) assets = list() + ba_status_list = self.state_manager.get_boot_actions_for_node( + hostname) + for ba in site_design.bootactions: if hostname in ba.target_nodes: - action_id = ulid2.generate_binary_ulid() + ba_status = ba_status_list.get(ba.name, None) + action_id = ba_status.get('action_id') assets.extend( ba.render_assets( hostname, site_design, action_id, type_filter=asset_type_filter)) - self.state_manager.post_boot_action( - hostname, ba_ctx['task_id'], ba_ctx['identity_key'], - action_id) tarball = BootactionUtils.tarbuilder(asset_list=assets) resp.set_header('Content-Type', 'application/gzip') @@ -112,7 +201,7 @@ class BootactionUnitsResource(BootactionAssetsResource): def on_get(self, req, resp, hostname): self.logger.debug( "Accessing boot action units resource for host %s." % hostname) - super().do_get(req, resp, hostname, 'unit') + self.do_get(req, resp, hostname, 'unit') class BootactionFilesResource(BootactionAssetsResource): @@ -120,7 +209,7 @@ class BootactionFilesResource(BootactionAssetsResource): super().__init__(**kwargs) def on_get(self, req, resp, hostname): - super().do_get(req, resp, hostname, 'file') + self.do_get(req, resp, hostname, 'file') class BootactionUtils(object): diff --git a/drydock_provisioner/objects/fields.py b/drydock_provisioner/objects/fields.py index bbeb53ee..53c6fd32 100644 --- a/drydock_provisioner/objects/fields.py +++ b/drydock_provisioner/objects/fields.py @@ -30,6 +30,7 @@ class OrchestratorAction(BaseDrydockEnum): PrepareNodes = 'prepare_nodes' DeployNodes = 'deploy_nodes' DestroyNodes = 'destroy_nodes' + BootactionReports = 'bootaction_reports' # OOB driver actions ValidateOobServices = 'validate_oob_services' @@ -63,12 +64,12 @@ class OrchestratorAction(BaseDrydockEnum): ConfigurePortProduction = 'config_port_production' ALL = (Noop, ValidateDesign, VerifySite, PrepareSite, VerifyNodes, - PrepareNodes, DeployNodes, DestroyNodes, ConfigNodePxe, SetNodeBoot, - PowerOffNode, PowerOnNode, PowerCycleNode, InterrogateOob, - CreateNetworkTemplate, CreateStorageTemplate, CreateBootMedia, - PrepareHardwareConfig, ConfigureHardware, InterrogateNode, - ApplyNodeNetworking, ApplyNodeStorage, ApplyNodePlatform, - DeployNode, DestroyNode) + PrepareNodes, DeployNodes, BootactionReports, DestroyNodes, + ConfigNodePxe, SetNodeBoot, PowerOffNode, PowerOnNode, + PowerCycleNode, InterrogateOob, CreateNetworkTemplate, + CreateStorageTemplate, CreateBootMedia, PrepareHardwareConfig, + ConfigureHardware, InterrogateNode, ApplyNodeNetworking, + ApplyNodeStorage, ApplyNodePlatform, DeployNode, DestroyNode) class OrchestratorActionField(fields.BaseEnumField): diff --git a/drydock_provisioner/objects/task.py b/drydock_provisioner/objects/task.py index 88f4eb10..75b6128e 100644 --- a/drydock_provisioner/objects/task.py +++ b/drydock_provisioner/objects/task.py @@ -277,19 +277,26 @@ class Task(object): else: self.logger.debug("Skipping subtask due to action filter.") - def align_result(self): - """Align the result of this task with the combined results of all the subtasks.""" + def align_result(self, action_filter=None): + """Align the result of this task with the combined results of all the subtasks. + + :param action_filter: string action name to filter subtasks on + """ for st in self.statemgr.get_complete_subtasks(self.task_id): - if st.get_result() in [ - hd_fields.ActionResult.Success, - hd_fields.ActionResult.PartialSuccess - ]: - self.success() - if st.get_result() in [ - hd_fields.ActionResult.Failure, - hd_fields.ActionResult.PartialSuccess - ]: - self.failure() + if action_filter is None or (action_filter is not None + and st.action == action_filter): + if st.get_result() in [ + hd_fields.ActionResult.Success, + hd_fields.ActionResult.PartialSuccess + ]: + self.success() + if st.get_result() in [ + hd_fields.ActionResult.Failure, + hd_fields.ActionResult.PartialSuccess + ]: + self.failure() + else: + self.logger.debug("Skipping subtask due to action filter.") def add_status_msg(self, **kwargs): """Add a status message to this task's result status.""" diff --git a/drydock_provisioner/orchestrator/actions/orchestrator.py b/drydock_provisioner/orchestrator/actions/orchestrator.py index b599660d..0ec52496 100644 --- a/drydock_provisioner/orchestrator/actions/orchestrator.py +++ b/drydock_provisioner/orchestrator/actions/orchestrator.py @@ -14,6 +14,7 @@ """Actions for the Orchestrator level of the Drydock workflow.""" import time +import datetime import logging import concurrent.futures import uuid @@ -718,7 +719,101 @@ class DeployNodes(BaseAction): "Unable to configure platform on any nodes, skipping deploy subtask" ) + node_deploy_task.bubble_results( + action_filter=hd_fields.OrchestratorAction.DeployNode) + + if len(node_deploy_task.result.successes) > 0: + node_bootaction_task = self.orchestrator.create_task( + design_ref=self.task.design_ref, + action=hd_fields.OrchestratorAction.BootactionReport, + node_filter=node_deploy_task.node_filter_from_successes()) + action = BootactionReports(node_bootaction_task, self.orchestrator, + self.state_manager) + action.start() + + self.task.align_result( + action_filter=hd_fields.OrchestratorAction.BootactionReport) self.task.set_status(hd_fields.TaskStatus.Complete) - self.task.align_result() + self.task.save() + return + + +class BootactionReports(BaseAction): + """Wait for nodes to report status of boot action.""" + + def start(self): + self.task.set_status(hd_fields.TaskStatus.Running) + self.task.save() + + poll_start = datetime.utcnow() + + still_running = True + timeout = datetime.timedelta( + minutes=config.config_mgr.conf.timeouts.bootaction_final_status) + running_time = datetime.utcnow() - poll_start + nodelist = [ + n.get_id() for n in self.orchestrator.get_target_nodes(self.task) + ] + + while running_time < timeout: + still_running = False + for n in nodelist: + bas = self.state_manager.get_boot_actions_for_node(n) + running_bas = { + k: v + for (k, v) in bas.items() + if v.get('action_status') == + hd_fields.ActionResult.Incomplete + } + if len(running_bas) > 0: + still_running = True + break + if still_running: + time.sleep(config.config_mgr.conf.poll_interval) + running_time = datetime.utcnow() + + for n in nodelist: + bas = self.state_manager.get_boot_actions_for_node(n) + success_bas = { + k: v + for (k, v) in bas.items() + if v.get('action_status') == hd_fields.ActionResult.Success + } + running_bas = { + k: v + for (k, v) in bas.items() + if v.get('action_status') == hd_fields.ActionResult.Incomplete + } + failure_bas = { + k: v + for (k, v) in bas.items() + if v.get('action_status') == hd_fields.ActionResult.Failure + } + for ba in success_bas.values(): + self.task.add_status_msg( + msg="Boot action %s completed with status %s" % + (ba['action_name'], ba['action_status']), + error=False, + ctx=n, + ctx_type='node') + for ba in failure_bas.values(): + self.task.add_status_msg( + msg="Boot action %s completed with status %s" % + (ba['action_name'], ba['action_status']), + error=True, + ctx=n, + ctx_type='node') + for ba in running_bas.values(): + self.task.add_status_msg( + msg="Boot action %s timed out." % (ba['action_name']), + error=True, + ctx=n, + ctx_type='node') + + if len(failure_bas) == 0 and len(running_bas) == 0: + self.task.success(focus=n) + else: + self.task.failure(focus=n) + self.task.save() return diff --git a/drydock_provisioner/orchestrator/orchestrator.py b/drydock_provisioner/orchestrator/orchestrator.py index b24488c7..3b813a8f 100644 --- a/drydock_provisioner/orchestrator/orchestrator.py +++ b/drydock_provisioner/orchestrator/orchestrator.py @@ -17,6 +17,7 @@ import time import importlib import logging import uuid +import ulid2 import concurrent.futures import os @@ -556,11 +557,16 @@ class Orchestrator(object): if site_design.bootactions is None: return None + identity_key = None + for ba in site_design.bootactions: if nodename in ba.target_nodes: - identity_key = os.urandom(32) - self.state_manager.post_boot_action_context( - nodename, task.get_id(), identity_key) - return identity_key + if identity_key is None: + identity_key = os.urandom(32) + self.state_manager.post_boot_action_context( + nodename, task.get_id(), identity_key) + action_id = ulid2.generate_binary_ulid() + self.state_manager.post_boot_action( + nodename, task.get_id(), identity_key, action_id, ba.name) - return None + return identity_key diff --git a/drydock_provisioner/statemgmt/db/tables.py b/drydock_provisioner/statemgmt/db/tables.py index e04207be..19903156 100644 --- a/drydock_provisioner/statemgmt/db/tables.py +++ b/drydock_provisioner/statemgmt/db/tables.py @@ -49,7 +49,7 @@ class ResultMessage(ExtendTable): __schema__ = [ Column('sequence', Integer, primary_key=True), Column('task_id', pg.BYTEA(16)), - Column('message', String(128)), + Column('message', String(1024)), Column('error', Boolean), Column('context', String(64)), Column('context_type', String(16)), @@ -89,7 +89,8 @@ class BootActionStatus(ExtendTable): __schema__ = [ Column('node_name', String(32)), - Column('bootaction_id', pg.BYTEA(16), primary_key=True), + Column('action_id', pg.BYTEA(16), primary_key=True), + Column('action_name', String(64)), Column('task_id', pg.BYTEA(16)), Column('identity_key', pg.BYTEA(32)), Column('action_status', String(32)), diff --git a/drydock_provisioner/statemgmt/state.py b/drydock_provisioner/statemgmt/state.py index 934c01ce..e2685431 100644 --- a/drydock_provisioner/statemgmt/state.py +++ b/drydock_provisioner/statemgmt/state.py @@ -30,8 +30,6 @@ from .design import resolver from drydock_provisioner import config -from drydock_provisioner.error import StateError - class DrydockState(object): def __init__(self): @@ -491,20 +489,22 @@ class DrydockState(object): task_id, identity_key, action_id, + action_name, action_status=hd_fields.ActionResult.Incomplete): """Post a individual boot action. :param nodename: The name of the node the boot action is running on :param task_id: The uuid.UUID task_id of the task that instigated the node deployment :param identity_key: A 256-bit key the node must provide when accessing the boot action API - :param action_id: The string ULID id of the boot action + :param action_id: The 32-byte ULID id of the boot action :param action_status: The status of the action. """ try: with self.db_engine.connect() as conn: query = self.ba_status_tbl.insert().values( node_name=nodename, - bootaction_id=action_id, + action_id=action_id, + action_name=action_name, task_id=task_id.bytes, identity_key=identity_key, action_status=action_status) @@ -513,6 +513,55 @@ class DrydockState(object): except Exception as ex: self.logger.error( "Error saving boot action %s." % action_id, exc_info=ex) + return False + + def put_bootaction_status(self, + action_id, + action_status=hd_fields.ActionResult.Incomplete): + """Update the status of a bootaction. + + :param action_id: string ULID ID of the boot action + :param action_status: The string statu to set for the boot action + """ + try: + with self.db_engine.connect() as conn: + query = self.ba_status_tbl.update().where( + self.ba_status_tbl.c.action_id == ulid2.decode_ulid_base32( + action_id)).values(action_status=action_status) + conn.execute(query) + return True + except Exception as ex: + self.logger.error( + "Error updating boot action %s status." % action_id, + exc_info=ex) + return False + + def get_boot_actions_for_node(self, nodename): + """Query for getting all boot action statuses for a node. + + Return a dictionary of boot action dictionaries keyed by the + boot action name. + + :param nodename: string nodename of the target node + """ + try: + with self.db_engine.connect() as conn: + query = self.ba_status_tbl.select().where( + self.ba_status_tbl.c.node_name == nodename) + rs = conn.execute(query) + actions = dict() + for r in rs: + ba_dict = dict(r) + ba_dict['action_id'] = bytes(ba_dict['action_id']) + ba_dict['identity_key'] = bytes(ba_dict['identity_key']) + ba_dict['task_id'] = uuid.UUID(bytes=ba_dict['task_id']) + actions[ba_dict.get('action_name', 'undefined')] = ba_dict + return actions + except Exception as ex: + self.logger.error( + "Error selecting boot actions for node %s" % nodename, + exc_info=ex) + return None def get_boot_action(self, action_id): """Query for a single boot action by ID. @@ -522,52 +571,18 @@ class DrydockState(object): try: with self.db_engine.connect() as conn: query = self.ba_status_tbl.select().where( - bootaction_id=ulid2.decode_ulid_base32(action_id)) + self.ba_status_tbl.c.action_id == ulid2.decode_ulid_base32( + action_id)) rs = conn.execute(query) r = rs.fetchone() if r is not None: ba_dict = dict(r) - ba_dict['bootaction_id'] = bytes(ba_dict['bootaction_id']) - ba_dict['identity_key'] = bytes( - ba_dict['identity_key']).hex() + ba_dict['action_id'] = bytes(ba_dict['action_id']) + ba_dict['identity_key'] = bytes(ba_dict['identity_key']) + ba_dict['task_id'] = uuid.UUID(bytes=ba_dict['task_id']) return ba_dict else: return None except Exception as ex: self.logger.error( "Error querying boot action %s" % action_id, exc_info=ex) - - def post_promenade_part(self, part): - my_lock = self.promenade_lock.acquire(blocking=True, timeout=10) - if my_lock: - if self.promenade.get(part.target, None) is not None: - self.promenade[part.target].append(part.obj_to_primitive()) - else: - self.promenade[part.target] = [part.obj_to_primitive()] - self.promenade_lock.release() - return None - else: - raise StateError("Could not acquire lock") - - def get_promenade_parts(self, target): - parts = self.promenade.get(target, None) - - if parts is not None: - return [ - objects.PromenadeConfig.obj_from_primitive(p) for p in parts - ] - else: - # Return an empty list just to play nice with extend - return [] - - def set_bootdata_key(self, hostname, design_id, data_key): - my_lock = self.bootdata_lock.acquire(blocking=True, timeout=10) - if my_lock: - self.bootdata[hostname] = {'design_id': design_id, 'key': data_key} - self.bootdata_lock.release() - return None - else: - raise StateError("Could not acquire lock") - - def get_bootdata_key(self, hostname): - return self.bootdata.get(hostname, None) diff --git a/tests/integration/postgres/test_api_bootaction.py b/tests/integration/postgres/test_api_bootaction.py index 1beb1a55..29cd2a8b 100644 --- a/tests/integration/postgres/test_api_bootaction.py +++ b/tests/integration/postgres/test_api_bootaction.py @@ -14,7 +14,6 @@ """Generic testing for the orchestrator.""" from falcon import testing import pytest -import os import tarfile import io import falcon @@ -74,9 +73,7 @@ class TestClass(object): test_task = test_orchestrator.create_task( action=hd_fields.OrchestratorAction.Noop, design_ref=design_ref) - id_key = os.urandom(32) - blank_state.post_boot_action_context('compute01', - test_task.get_id(), id_key) + id_key = test_orchestrator.create_bootaction_context('compute01', test_task) ba_ctx = dict( nodename='compute01', diff --git a/tests/integration/postgres/test_api_bootaction_status.py b/tests/integration/postgres/test_api_bootaction_status.py new file mode 100644 index 00000000..1ace530c --- /dev/null +++ b/tests/integration/postgres/test_api_bootaction_status.py @@ -0,0 +1,123 @@ +# 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. +"""Generic testing for the orchestrator.""" +from falcon import testing +import pytest +import os +import json + +import ulid2 +import falcon + +import drydock_provisioner.objects.fields as hd_fields +from drydock_provisioner.control.api import start_api + + +class TestClass(object): + def test_bootaction_detail(self, falcontest, seed_bootaction_status): + """Test that the API allows boot action detail messages.""" + url = "/api/v1.0/bootactions/%s" % seed_bootaction_status['action_id'] + hdr = { + 'X-Bootaction-Key': "%s" % seed_bootaction_status['identity_key'], + 'Content-Type': 'application/json', + } + + body = { + 'details': [ + { + 'message': 'Test message.', + 'error': True, + }, + ] + } + + result = falcontest.simulate_post( + url, headers=hdr, body=json.dumps(body)) + + assert result.status == falcon.HTTP_200 + + def test_bootaction_status(self, falcontest, seed_bootaction_status): + """Test that the API allows boot action status updates.""" + url = "/api/v1.0/bootactions/%s" % seed_bootaction_status['action_id'] + hdr = { + 'X-Bootaction-Key': "%s" % seed_bootaction_status['identity_key'], + 'Content-Type': 'application/json', + } + + body = { + 'status': 'Success', + 'details': [ + { + 'message': 'Test message.', + 'error': True, + }, + ] + } + + result = falcontest.simulate_post( + url, headers=hdr, body=json.dumps(body)) + + assert result.status == falcon.HTTP_200 + + result = falcontest.simulate_post( + url, headers=hdr, body=json.dumps(body)) + + assert result.status == falcon.HTTP_409 + + def test_bootaction_schema(self, falcontest, seed_bootaction_status): + """Test that the API allows boot action status updates.""" + url = "/api/v1.0/bootactions/%s" % seed_bootaction_status['action_id'] + hdr = { + 'X-Bootaction-Key': "%s" % seed_bootaction_status['identity_key'], + 'Content-Type': 'application/json', + } + + body = { + 'foo': 'Success', + } + + result = falcontest.simulate_post( + url, headers=hdr, body=json.dumps(body)) + + assert result.status == falcon.HTTP_400 + + @pytest.fixture() + def seed_bootaction_status(self, blank_state, test_orchestrator, + input_files): + """Add a task and boot action to the database for testing.""" + input_file = input_files.join("fullsite.yaml") + design_ref = "file://%s" % input_file + test_task = test_orchestrator.create_task( + action=hd_fields.OrchestratorAction.Noop, design_ref=design_ref) + + id_key = os.urandom(32) + action_id = ulid2.generate_binary_ulid() + blank_state.post_boot_action('compute01', + test_task.get_id(), id_key, action_id, 'helloworld') + + ba = dict( + nodename='compute01', + task_id=test_task.get_id(), + identity_key=id_key.hex(), + action_id=ulid2.encode_ulid_base32(action_id)) + return ba + + @pytest.fixture() + def falcontest(self, drydock_state, test_ingester, test_orchestrator): + """Create a test harness for the the Falcon API framework.""" + return testing.TestClient( + start_api( + state_manager=drydock_state, + ingester=test_ingester, + orchestrator=test_orchestrator)) diff --git a/tests/integration/postgres/test_postgres_bootaction_status.py b/tests/integration/postgres/test_postgres_bootaction_status.py new file mode 100644 index 00000000..dc31eea0 --- /dev/null +++ b/tests/integration/postgres/test_postgres_bootaction_status.py @@ -0,0 +1,70 @@ +# 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. +"""Test postgres integration for task management.""" + +import pytest +import os +import ulid2 + +from drydock_provisioner import objects + + +class TestPostgres(object): + def test_bootaction_post(self, populateddb, drydock_state): + """Test that a boot action status can be added.""" + id_key = os.urandom(32) + action_id = ulid2.generate_binary_ulid() + nodename = 'testnode' + result = drydock_state.post_boot_action(nodename, + populateddb.get_id(), id_key, + action_id, + 'helloworld') + + assert result + + def test_bootaction_put(self, populateddb, drydock_state): + """Test that a boot action status can be updated.""" + id_key = os.urandom(32) + action_id = ulid2.generate_binary_ulid() + nodename = 'testnode' + drydock_state.post_boot_action(nodename, + populateddb.get_id(), id_key, action_id, 'helloworld') + + result = drydock_state.put_bootaction_status( + ulid2.encode_ulid_base32(action_id), + action_status=objects.fields.ActionResult.Success) + + assert result + + def test_bootaction_get(self, populateddb, drydock_state): + """Test that a boot action status can be retrieved.""" + id_key = os.urandom(32) + action_id = ulid2.generate_binary_ulid() + nodename = 'testnode' + drydock_state.post_boot_action(nodename, + populateddb.get_id(), id_key, action_id, 'helloworld') + + ba = drydock_state.get_boot_action(ulid2.encode_ulid_base32(action_id)) + + assert ba.get('identity_key') == id_key + + @pytest.fixture(scope='function') + def populateddb(self, blank_state): + """Add dummy task to test against.""" + task = objects.Task( + action='prepare_site', design_ref='http://test.com/design') + + blank_state.post_task(task) + + return task diff --git a/tests/unit/test_bootaction_asset_render.py b/tests/unit/test_bootaction_asset_render.py index b5ba1a66..7fb13652 100644 --- a/tests/unit/test_bootaction_asset_render.py +++ b/tests/unit/test_bootaction_asset_render.py @@ -19,6 +19,7 @@ from drydock_provisioner.ingester.ingester import Ingester from drydock_provisioner.statemgmt.state import DrydockState import drydock_provisioner.objects as objects + class TestClass(object): def test_bootaction_render(self, input_files, setup): objects.register_all() diff --git a/tests/unit/test_bootaction_scoping.py b/tests/unit/test_bootaction_scoping.py index 88e7283c..56f94626 100644 --- a/tests/unit/test_bootaction_scoping.py +++ b/tests/unit/test_bootaction_scoping.py @@ -14,6 +14,7 @@ import drydock_provisioner.objects as objects + class TestClass(object): def test_bootaction_scoping_blankfilter(self, input_files, test_orchestrator): diff --git a/tests/unit/test_design_inheritance.py b/tests/unit/test_design_inheritance.py index 2dc4013d..1367d368 100644 --- a/tests/unit/test_design_inheritance.py +++ b/tests/unit/test_design_inheritance.py @@ -16,6 +16,7 @@ from drydock_provisioner.ingester.ingester import Ingester from drydock_provisioner.statemgmt.state import DrydockState from drydock_provisioner.orchestrator.orchestrator import Orchestrator + class TestClass(object): def test_design_inheritance(self, input_files, setup): input_file = input_files.join("fullsite.yaml") diff --git a/tests/unit/test_ingester.py b/tests/unit/test_ingester.py index 2ab71343..864b2206 100644 --- a/tests/unit/test_ingester.py +++ b/tests/unit/test_ingester.py @@ -17,6 +17,7 @@ from drydock_provisioner.ingester.ingester import Ingester from drydock_provisioner.statemgmt.state import DrydockState import drydock_provisioner.objects as objects + class TestClass(object): def test_ingest_full_site(self, input_files, setup): objects.register_all() diff --git a/tests/unit/test_ingester_bootaction.py b/tests/unit/test_ingester_bootaction.py index b622d2e1..93204bd6 100644 --- a/tests/unit/test_ingester_bootaction.py +++ b/tests/unit/test_ingester_bootaction.py @@ -17,6 +17,7 @@ from drydock_provisioner.ingester.ingester import Ingester from drydock_provisioner.statemgmt.state import DrydockState import drydock_provisioner.objects as objects + class TestClass(object): def test_bootaction_parse(self, input_files, setup): objects.register_all() diff --git a/tests/unit/test_ingester_yaml.py b/tests/unit/test_ingester_yaml.py index 75d23027..2bf9cd43 100644 --- a/tests/unit/test_ingester_yaml.py +++ b/tests/unit/test_ingester_yaml.py @@ -15,6 +15,7 @@ from drydock_provisioner.ingester.plugins.yaml import YamlIngester + class TestClass(object): def test_ingest_singledoc(self, input_files): input_file = input_files.join("singledoc.yaml") diff --git a/tests/unit/test_orch_node_filter.py b/tests/unit/test_orch_node_filter.py index c9113e8f..9792fa69 100644 --- a/tests/unit/test_orch_node_filter.py +++ b/tests/unit/test_orch_node_filter.py @@ -17,6 +17,7 @@ from drydock_provisioner.ingester.ingester import Ingester from drydock_provisioner.statemgmt.state import DrydockState import drydock_provisioner.objects as objects + class TestClass(object): def test_node_filter_obj(self, input_files, setup, test_orchestrator): input_file = input_files.join("fullsite.yaml")