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
This commit is contained in:
parent
f4dba218ac
commit
adf07eead8
|
@ -13,17 +13,51 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""Handle resources for boot action API endpoints. """
|
"""Handle resources for boot action API endpoints. """
|
||||||
|
|
||||||
import falcon
|
|
||||||
import ulid2
|
|
||||||
import tarfile
|
import tarfile
|
||||||
import io
|
import io
|
||||||
import logging
|
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
|
from .base import StatefulResource
|
||||||
|
|
||||||
logger = logging.getLogger('drydock')
|
logger = logging.getLogger('drydock')
|
||||||
|
|
||||||
class BootactionResource(StatefulResource):
|
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):
|
def __init__(self, orchestrator=None, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.orchestrator = orchestrator
|
self.orchestrator = orchestrator
|
||||||
|
@ -38,6 +72,60 @@ class BootactionResource(StatefulResource):
|
||||||
:param resp: falcone response
|
:param resp: falcone response
|
||||||
:param action_id: ULID ID of the boot action
|
: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):
|
class BootactionAssetsResource(StatefulResource):
|
||||||
|
@ -79,18 +167,19 @@ class BootactionAssetsResource(StatefulResource):
|
||||||
task.design_ref)
|
task.design_ref)
|
||||||
|
|
||||||
assets = list()
|
assets = list()
|
||||||
|
ba_status_list = self.state_manager.get_boot_actions_for_node(
|
||||||
|
hostname)
|
||||||
|
|
||||||
for ba in site_design.bootactions:
|
for ba in site_design.bootactions:
|
||||||
if hostname in ba.target_nodes:
|
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(
|
assets.extend(
|
||||||
ba.render_assets(
|
ba.render_assets(
|
||||||
hostname,
|
hostname,
|
||||||
site_design,
|
site_design,
|
||||||
action_id,
|
action_id,
|
||||||
type_filter=asset_type_filter))
|
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)
|
tarball = BootactionUtils.tarbuilder(asset_list=assets)
|
||||||
resp.set_header('Content-Type', 'application/gzip')
|
resp.set_header('Content-Type', 'application/gzip')
|
||||||
|
@ -112,7 +201,7 @@ class BootactionUnitsResource(BootactionAssetsResource):
|
||||||
def on_get(self, req, resp, hostname):
|
def on_get(self, req, resp, hostname):
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
"Accessing boot action units resource for host %s." % hostname)
|
"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):
|
class BootactionFilesResource(BootactionAssetsResource):
|
||||||
|
@ -120,7 +209,7 @@ class BootactionFilesResource(BootactionAssetsResource):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def on_get(self, req, resp, hostname):
|
def on_get(self, req, resp, hostname):
|
||||||
super().do_get(req, resp, hostname, 'file')
|
self.do_get(req, resp, hostname, 'file')
|
||||||
|
|
||||||
|
|
||||||
class BootactionUtils(object):
|
class BootactionUtils(object):
|
||||||
|
|
|
@ -30,6 +30,7 @@ class OrchestratorAction(BaseDrydockEnum):
|
||||||
PrepareNodes = 'prepare_nodes'
|
PrepareNodes = 'prepare_nodes'
|
||||||
DeployNodes = 'deploy_nodes'
|
DeployNodes = 'deploy_nodes'
|
||||||
DestroyNodes = 'destroy_nodes'
|
DestroyNodes = 'destroy_nodes'
|
||||||
|
BootactionReports = 'bootaction_reports'
|
||||||
|
|
||||||
# OOB driver actions
|
# OOB driver actions
|
||||||
ValidateOobServices = 'validate_oob_services'
|
ValidateOobServices = 'validate_oob_services'
|
||||||
|
@ -63,12 +64,12 @@ class OrchestratorAction(BaseDrydockEnum):
|
||||||
ConfigurePortProduction = 'config_port_production'
|
ConfigurePortProduction = 'config_port_production'
|
||||||
|
|
||||||
ALL = (Noop, ValidateDesign, VerifySite, PrepareSite, VerifyNodes,
|
ALL = (Noop, ValidateDesign, VerifySite, PrepareSite, VerifyNodes,
|
||||||
PrepareNodes, DeployNodes, DestroyNodes, ConfigNodePxe, SetNodeBoot,
|
PrepareNodes, DeployNodes, BootactionReports, DestroyNodes,
|
||||||
PowerOffNode, PowerOnNode, PowerCycleNode, InterrogateOob,
|
ConfigNodePxe, SetNodeBoot, PowerOffNode, PowerOnNode,
|
||||||
CreateNetworkTemplate, CreateStorageTemplate, CreateBootMedia,
|
PowerCycleNode, InterrogateOob, CreateNetworkTemplate,
|
||||||
PrepareHardwareConfig, ConfigureHardware, InterrogateNode,
|
CreateStorageTemplate, CreateBootMedia, PrepareHardwareConfig,
|
||||||
ApplyNodeNetworking, ApplyNodeStorage, ApplyNodePlatform,
|
ConfigureHardware, InterrogateNode, ApplyNodeNetworking,
|
||||||
DeployNode, DestroyNode)
|
ApplyNodeStorage, ApplyNodePlatform, DeployNode, DestroyNode)
|
||||||
|
|
||||||
|
|
||||||
class OrchestratorActionField(fields.BaseEnumField):
|
class OrchestratorActionField(fields.BaseEnumField):
|
||||||
|
|
|
@ -277,19 +277,26 @@ class Task(object):
|
||||||
else:
|
else:
|
||||||
self.logger.debug("Skipping subtask due to action filter.")
|
self.logger.debug("Skipping subtask due to action filter.")
|
||||||
|
|
||||||
def align_result(self):
|
def align_result(self, action_filter=None):
|
||||||
"""Align the result of this task with the combined results of all the subtasks."""
|
"""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):
|
for st in self.statemgr.get_complete_subtasks(self.task_id):
|
||||||
if st.get_result() in [
|
if action_filter is None or (action_filter is not None
|
||||||
hd_fields.ActionResult.Success,
|
and st.action == action_filter):
|
||||||
hd_fields.ActionResult.PartialSuccess
|
if st.get_result() in [
|
||||||
]:
|
hd_fields.ActionResult.Success,
|
||||||
self.success()
|
hd_fields.ActionResult.PartialSuccess
|
||||||
if st.get_result() in [
|
]:
|
||||||
hd_fields.ActionResult.Failure,
|
self.success()
|
||||||
hd_fields.ActionResult.PartialSuccess
|
if st.get_result() in [
|
||||||
]:
|
hd_fields.ActionResult.Failure,
|
||||||
self.failure()
|
hd_fields.ActionResult.PartialSuccess
|
||||||
|
]:
|
||||||
|
self.failure()
|
||||||
|
else:
|
||||||
|
self.logger.debug("Skipping subtask due to action filter.")
|
||||||
|
|
||||||
def add_status_msg(self, **kwargs):
|
def add_status_msg(self, **kwargs):
|
||||||
"""Add a status message to this task's result status."""
|
"""Add a status message to this task's result status."""
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"""Actions for the Orchestrator level of the Drydock workflow."""
|
"""Actions for the Orchestrator level of the Drydock workflow."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import uuid
|
import uuid
|
||||||
|
@ -718,7 +719,101 @@ class DeployNodes(BaseAction):
|
||||||
"Unable to configure platform on any nodes, skipping deploy subtask"
|
"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.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()
|
self.task.save()
|
||||||
return
|
return
|
||||||
|
|
|
@ -17,6 +17,7 @@ import time
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
import ulid2
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -556,11 +557,16 @@ class Orchestrator(object):
|
||||||
if site_design.bootactions is None:
|
if site_design.bootactions is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
identity_key = None
|
||||||
|
|
||||||
for ba in site_design.bootactions:
|
for ba in site_design.bootactions:
|
||||||
if nodename in ba.target_nodes:
|
if nodename in ba.target_nodes:
|
||||||
identity_key = os.urandom(32)
|
if identity_key is None:
|
||||||
self.state_manager.post_boot_action_context(
|
identity_key = os.urandom(32)
|
||||||
nodename, task.get_id(), identity_key)
|
self.state_manager.post_boot_action_context(
|
||||||
return identity_key
|
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
|
||||||
|
|
|
@ -49,7 +49,7 @@ class ResultMessage(ExtendTable):
|
||||||
__schema__ = [
|
__schema__ = [
|
||||||
Column('sequence', Integer, primary_key=True),
|
Column('sequence', Integer, primary_key=True),
|
||||||
Column('task_id', pg.BYTEA(16)),
|
Column('task_id', pg.BYTEA(16)),
|
||||||
Column('message', String(128)),
|
Column('message', String(1024)),
|
||||||
Column('error', Boolean),
|
Column('error', Boolean),
|
||||||
Column('context', String(64)),
|
Column('context', String(64)),
|
||||||
Column('context_type', String(16)),
|
Column('context_type', String(16)),
|
||||||
|
@ -89,7 +89,8 @@ class BootActionStatus(ExtendTable):
|
||||||
|
|
||||||
__schema__ = [
|
__schema__ = [
|
||||||
Column('node_name', String(32)),
|
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('task_id', pg.BYTEA(16)),
|
||||||
Column('identity_key', pg.BYTEA(32)),
|
Column('identity_key', pg.BYTEA(32)),
|
||||||
Column('action_status', String(32)),
|
Column('action_status', String(32)),
|
||||||
|
|
|
@ -30,8 +30,6 @@ from .design import resolver
|
||||||
|
|
||||||
from drydock_provisioner import config
|
from drydock_provisioner import config
|
||||||
|
|
||||||
from drydock_provisioner.error import StateError
|
|
||||||
|
|
||||||
|
|
||||||
class DrydockState(object):
|
class DrydockState(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -491,20 +489,22 @@ class DrydockState(object):
|
||||||
task_id,
|
task_id,
|
||||||
identity_key,
|
identity_key,
|
||||||
action_id,
|
action_id,
|
||||||
|
action_name,
|
||||||
action_status=hd_fields.ActionResult.Incomplete):
|
action_status=hd_fields.ActionResult.Incomplete):
|
||||||
"""Post a individual boot action.
|
"""Post a individual boot action.
|
||||||
|
|
||||||
:param nodename: The name of the node the boot action is running on
|
: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 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 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.
|
:param action_status: The status of the action.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with self.db_engine.connect() as conn:
|
with self.db_engine.connect() as conn:
|
||||||
query = self.ba_status_tbl.insert().values(
|
query = self.ba_status_tbl.insert().values(
|
||||||
node_name=nodename,
|
node_name=nodename,
|
||||||
bootaction_id=action_id,
|
action_id=action_id,
|
||||||
|
action_name=action_name,
|
||||||
task_id=task_id.bytes,
|
task_id=task_id.bytes,
|
||||||
identity_key=identity_key,
|
identity_key=identity_key,
|
||||||
action_status=action_status)
|
action_status=action_status)
|
||||||
|
@ -513,6 +513,55 @@ class DrydockState(object):
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
"Error saving boot action %s." % action_id, exc_info=ex)
|
"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):
|
def get_boot_action(self, action_id):
|
||||||
"""Query for a single boot action by ID.
|
"""Query for a single boot action by ID.
|
||||||
|
@ -522,52 +571,18 @@ class DrydockState(object):
|
||||||
try:
|
try:
|
||||||
with self.db_engine.connect() as conn:
|
with self.db_engine.connect() as conn:
|
||||||
query = self.ba_status_tbl.select().where(
|
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)
|
rs = conn.execute(query)
|
||||||
r = rs.fetchone()
|
r = rs.fetchone()
|
||||||
if r is not None:
|
if r is not None:
|
||||||
ba_dict = dict(r)
|
ba_dict = dict(r)
|
||||||
ba_dict['bootaction_id'] = bytes(ba_dict['bootaction_id'])
|
ba_dict['action_id'] = bytes(ba_dict['action_id'])
|
||||||
ba_dict['identity_key'] = bytes(
|
ba_dict['identity_key'] = bytes(ba_dict['identity_key'])
|
||||||
ba_dict['identity_key']).hex()
|
ba_dict['task_id'] = uuid.UUID(bytes=ba_dict['task_id'])
|
||||||
return ba_dict
|
return ba_dict
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
"Error querying boot action %s" % action_id, exc_info=ex)
|
"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)
|
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
"""Generic testing for the orchestrator."""
|
"""Generic testing for the orchestrator."""
|
||||||
from falcon import testing
|
from falcon import testing
|
||||||
import pytest
|
import pytest
|
||||||
import os
|
|
||||||
import tarfile
|
import tarfile
|
||||||
import io
|
import io
|
||||||
import falcon
|
import falcon
|
||||||
|
@ -74,9 +73,7 @@ class TestClass(object):
|
||||||
test_task = test_orchestrator.create_task(
|
test_task = test_orchestrator.create_task(
|
||||||
action=hd_fields.OrchestratorAction.Noop, design_ref=design_ref)
|
action=hd_fields.OrchestratorAction.Noop, design_ref=design_ref)
|
||||||
|
|
||||||
id_key = os.urandom(32)
|
id_key = test_orchestrator.create_bootaction_context('compute01', test_task)
|
||||||
blank_state.post_boot_action_context('compute01',
|
|
||||||
test_task.get_id(), id_key)
|
|
||||||
|
|
||||||
ba_ctx = dict(
|
ba_ctx = dict(
|
||||||
nodename='compute01',
|
nodename='compute01',
|
||||||
|
|
|
@ -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))
|
|
@ -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
|
|
@ -19,6 +19,7 @@ from drydock_provisioner.ingester.ingester import Ingester
|
||||||
from drydock_provisioner.statemgmt.state import DrydockState
|
from drydock_provisioner.statemgmt.state import DrydockState
|
||||||
import drydock_provisioner.objects as objects
|
import drydock_provisioner.objects as objects
|
||||||
|
|
||||||
|
|
||||||
class TestClass(object):
|
class TestClass(object):
|
||||||
def test_bootaction_render(self, input_files, setup):
|
def test_bootaction_render(self, input_files, setup):
|
||||||
objects.register_all()
|
objects.register_all()
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
|
|
||||||
import drydock_provisioner.objects as objects
|
import drydock_provisioner.objects as objects
|
||||||
|
|
||||||
|
|
||||||
class TestClass(object):
|
class TestClass(object):
|
||||||
def test_bootaction_scoping_blankfilter(self, input_files,
|
def test_bootaction_scoping_blankfilter(self, input_files,
|
||||||
test_orchestrator):
|
test_orchestrator):
|
||||||
|
|
|
@ -16,6 +16,7 @@ from drydock_provisioner.ingester.ingester import Ingester
|
||||||
from drydock_provisioner.statemgmt.state import DrydockState
|
from drydock_provisioner.statemgmt.state import DrydockState
|
||||||
from drydock_provisioner.orchestrator.orchestrator import Orchestrator
|
from drydock_provisioner.orchestrator.orchestrator import Orchestrator
|
||||||
|
|
||||||
|
|
||||||
class TestClass(object):
|
class TestClass(object):
|
||||||
def test_design_inheritance(self, input_files, setup):
|
def test_design_inheritance(self, input_files, setup):
|
||||||
input_file = input_files.join("fullsite.yaml")
|
input_file = input_files.join("fullsite.yaml")
|
||||||
|
|
|
@ -17,6 +17,7 @@ from drydock_provisioner.ingester.ingester import Ingester
|
||||||
from drydock_provisioner.statemgmt.state import DrydockState
|
from drydock_provisioner.statemgmt.state import DrydockState
|
||||||
import drydock_provisioner.objects as objects
|
import drydock_provisioner.objects as objects
|
||||||
|
|
||||||
|
|
||||||
class TestClass(object):
|
class TestClass(object):
|
||||||
def test_ingest_full_site(self, input_files, setup):
|
def test_ingest_full_site(self, input_files, setup):
|
||||||
objects.register_all()
|
objects.register_all()
|
||||||
|
|
|
@ -17,6 +17,7 @@ from drydock_provisioner.ingester.ingester import Ingester
|
||||||
from drydock_provisioner.statemgmt.state import DrydockState
|
from drydock_provisioner.statemgmt.state import DrydockState
|
||||||
import drydock_provisioner.objects as objects
|
import drydock_provisioner.objects as objects
|
||||||
|
|
||||||
|
|
||||||
class TestClass(object):
|
class TestClass(object):
|
||||||
def test_bootaction_parse(self, input_files, setup):
|
def test_bootaction_parse(self, input_files, setup):
|
||||||
objects.register_all()
|
objects.register_all()
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
from drydock_provisioner.ingester.plugins.yaml import YamlIngester
|
from drydock_provisioner.ingester.plugins.yaml import YamlIngester
|
||||||
|
|
||||||
|
|
||||||
class TestClass(object):
|
class TestClass(object):
|
||||||
def test_ingest_singledoc(self, input_files):
|
def test_ingest_singledoc(self, input_files):
|
||||||
input_file = input_files.join("singledoc.yaml")
|
input_file = input_files.join("singledoc.yaml")
|
||||||
|
|
|
@ -17,6 +17,7 @@ from drydock_provisioner.ingester.ingester import Ingester
|
||||||
from drydock_provisioner.statemgmt.state import DrydockState
|
from drydock_provisioner.statemgmt.state import DrydockState
|
||||||
import drydock_provisioner.objects as objects
|
import drydock_provisioner.objects as objects
|
||||||
|
|
||||||
|
|
||||||
class TestClass(object):
|
class TestClass(object):
|
||||||
def test_node_filter_obj(self, input_files, setup, test_orchestrator):
|
def test_node_filter_obj(self, input_files, setup, test_orchestrator):
|
||||||
input_file = input_files.join("fullsite.yaml")
|
input_file = input_files.join("fullsite.yaml")
|
||||||
|
|
Loading…
Reference in New Issue