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.
|
||||
"""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):
|
||||
|
@ -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):
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)),
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
123
tests/integration/postgres/test_api_bootaction_status.py
Normal file
123
tests/integration/postgres/test_api_bootaction_status.py
Normal file
@ -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
|
||||
import drydock_provisioner.objects as objects
|
||||
|
||||
|
||||
class TestClass(object):
|
||||
def test_bootaction_render(self, input_files, setup):
|
||||
objects.register_all()
|
||||
|
@ -14,6 +14,7 @@
|
||||
|
||||
import drydock_provisioner.objects as objects
|
||||
|
||||
|
||||
class TestClass(object):
|
||||
def test_bootaction_scoping_blankfilter(self, input_files,
|
||||
test_orchestrator):
|
||||
|
@ -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")
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user