Support links for task status
- Some status changes in a task may have additional information that is referenced by a URI link. Support describing these links and returning them via API. - Refactor alembic stuff to better handle table schema updates - Add unit tests Change-Id: Iae63a9716f2522578be0244925fc274a4338eac4
This commit is contained in:
parent
e27eaf94f5
commit
cff7420cff
29
alembic/versions/4713e7ebca9_add_task_status_links.py
Normal file
29
alembic/versions/4713e7ebca9_add_task_status_links.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""add task status links
|
||||||
|
|
||||||
|
Revision ID: 4713e7ebca9
|
||||||
|
Revises: 4a5bef3702b
|
||||||
|
Create Date: 2018-07-05 14:54:18.381988
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4713e7ebca9'
|
||||||
|
down_revision = '4a5bef3702b'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from drydock_provisioner.statemgmt.db import tables
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
for c in tables.Tasks.__add_result_links__:
|
||||||
|
op.add_column(tables.Tasks.__tablename__, c)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
for c in tables.Tasks.__add_result_links__:
|
||||||
|
op.drop_column(tables.Tasks.__tablename__, c.name)
|
||||||
|
|
@ -19,7 +19,7 @@ from drydock_provisioner.statemgmt.db import tables
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
op.create_table(tables.BuildData.__tablename__,
|
op.create_table(tables.BuildData.__tablename__,
|
||||||
*tables.BuildData.__schema__)
|
*tables.BuildData.__baseschema__)
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
|
@ -18,15 +18,15 @@ from drydock_provisioner.statemgmt.db import tables
|
|||||||
|
|
||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
op.create_table(tables.Tasks.__tablename__, *tables.Tasks.__schema__)
|
op.create_table(tables.Tasks.__tablename__, *tables.Tasks.__baseschema__)
|
||||||
op.create_table(tables.ResultMessage.__tablename__,
|
op.create_table(tables.ResultMessage.__tablename__,
|
||||||
*tables.ResultMessage.__schema__)
|
*tables.ResultMessage.__baseschema__)
|
||||||
op.create_table(tables.ActiveInstance.__tablename__,
|
op.create_table(tables.ActiveInstance.__tablename__,
|
||||||
*tables.ActiveInstance.__schema__)
|
*tables.ActiveInstance.__baseschema__)
|
||||||
op.create_table(tables.BootAction.__tablename__,
|
op.create_table(tables.BootAction.__tablename__,
|
||||||
*tables.BootAction.__schema__)
|
*tables.BootAction.__baseschema__)
|
||||||
op.create_table(tables.BootActionStatus.__tablename__,
|
op.create_table(tables.BootActionStatus.__tablename__,
|
||||||
*tables.BootActionStatus.__schema__)
|
*tables.BootActionStatus.__baseschema__)
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
|
30
drydock_provisioner/control/util.py
Normal file
30
drydock_provisioner/control/util.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""Reusable utility functions for API access."""
|
||||||
|
from drydock_provisioner.error import ApiError
|
||||||
|
from drydock_provisioner.drydock_client.session import KeystoneClient
|
||||||
|
from drydock_provisioner.util import KeystoneUtils
|
||||||
|
|
||||||
|
def get_internal_api_href(ver):
|
||||||
|
"""Get the internal API href for Drydock API version ``ver``."""
|
||||||
|
|
||||||
|
# TODO(sh8121att) Support versioned service registration
|
||||||
|
supported_versions = ['v1.0']
|
||||||
|
if ver in supported_versions:
|
||||||
|
ks_sess = KeystoneUtils.get_session()
|
||||||
|
url = KeystoneClient.get_endpoint(
|
||||||
|
"physicalprovisioner", ks_sess=ks_sess, interface='internal')
|
||||||
|
return url
|
||||||
|
else:
|
||||||
|
raise ApiError("API version %s unknown." % ver)
|
@ -180,12 +180,13 @@ class DrydockSession(object):
|
|||||||
|
|
||||||
class KeystoneClient(object):
|
class KeystoneClient(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_endpoint(endpoint, ks_sess=None, auth_info=None):
|
def get_endpoint(endpoint, ks_sess=None, auth_info=None, interface='internal'):
|
||||||
"""
|
"""
|
||||||
Wraps calls to keystone for lookup of an endpoint by service type
|
Wraps calls to keystone for lookup of an endpoint by service type
|
||||||
:param endpoint: The endpoint to look up
|
:param endpoint: The endpoint to look up
|
||||||
:param ks_sess: A keystone session to use for accessing endpoint catalogue
|
:param ks_sess: A keystone session to use for accessing endpoint catalogue
|
||||||
:param auth_info: Authentication info to use for building a token if a ``ks_sess`` is not specified
|
:param auth_info: Authentication info to use for building a token if a ``ks_sess`` is not specified
|
||||||
|
:param str interface: Which registered endpoint to return
|
||||||
:returns: The url string of the endpoint
|
:returns: The url string of the endpoint
|
||||||
:rtype: str
|
:rtype: str
|
||||||
"""
|
"""
|
||||||
@ -193,7 +194,7 @@ class KeystoneClient(object):
|
|||||||
ks_sess = KeystoneClient.get_ks_session(**auth_info)
|
ks_sess = KeystoneClient.get_ks_session(**auth_info)
|
||||||
|
|
||||||
return ks_sess.get_endpoint(
|
return ks_sess.get_endpoint(
|
||||||
interface='internal', service_type=endpoint)
|
interface=interface, service_type=endpoint)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_token(ks_sess=None, auth_info=None):
|
def get_token(ks_sess=None, auth_info=None):
|
||||||
|
@ -371,6 +371,8 @@ class Task(object):
|
|||||||
self.result.successes,
|
self.result.successes,
|
||||||
'result_failures':
|
'result_failures':
|
||||||
self.result.failures,
|
self.result.failures,
|
||||||
|
'result_links':
|
||||||
|
self.result.links,
|
||||||
'status':
|
'status':
|
||||||
self.status,
|
self.status,
|
||||||
'created':
|
'created':
|
||||||
@ -486,6 +488,7 @@ class Task(object):
|
|||||||
i.result.status = d.get('result_status')
|
i.result.status = d.get('result_status')
|
||||||
i.result.successes = d.get('result_successes', [])
|
i.result.successes = d.get('result_successes', [])
|
||||||
i.result.failures = d.get('result_failures', [])
|
i.result.failures = d.get('result_failures', [])
|
||||||
|
i.result.links = d.get('result_links', [])
|
||||||
|
|
||||||
# Deserialize the request context for this task
|
# Deserialize the request context for this task
|
||||||
if i.request_context is not None:
|
if i.request_context is not None:
|
||||||
@ -506,6 +509,8 @@ class TaskStatus(object):
|
|||||||
self.reason = None
|
self.reason = None
|
||||||
self.status = hd_fields.ActionResult.Incomplete
|
self.status = hd_fields.ActionResult.Incomplete
|
||||||
|
|
||||||
|
self.links = dict()
|
||||||
|
|
||||||
# For tasks operating on multiple contexts (nodes, networks, etc...)
|
# For tasks operating on multiple contexts (nodes, networks, etc...)
|
||||||
# track which contexts ended successfully and which failed
|
# track which contexts ended successfully and which failed
|
||||||
self.successes = []
|
self.successes = []
|
||||||
@ -515,6 +520,31 @@ class TaskStatus(object):
|
|||||||
def obj_name(cls):
|
def obj_name(cls):
|
||||||
return cls.__name__
|
return cls.__name__
|
||||||
|
|
||||||
|
def add_link(self, relation, uri):
|
||||||
|
"""Add a external reference link to this status.
|
||||||
|
|
||||||
|
:param str relation: The relation of the link
|
||||||
|
:param str uri: A valid URI that references the external content
|
||||||
|
"""
|
||||||
|
self.links.setdefault(relation, [])
|
||||||
|
self.links[relation].append(uri)
|
||||||
|
|
||||||
|
def get_links(self, relation=None):
|
||||||
|
"""Get one or more links of this status.
|
||||||
|
|
||||||
|
If ``relation`` is None, then return all links.
|
||||||
|
|
||||||
|
:param str relation: Return only links that exhibit this relation
|
||||||
|
:returns: a list of str URIs or empty list
|
||||||
|
"""
|
||||||
|
if relation:
|
||||||
|
return self.links.get(relation, [])
|
||||||
|
else:
|
||||||
|
all_links = list()
|
||||||
|
for v in self.links.values():
|
||||||
|
all_links.extend(v)
|
||||||
|
return all_links
|
||||||
|
|
||||||
def set_message(self, msg):
|
def set_message(self, msg):
|
||||||
self.message = msg
|
self.message = msg
|
||||||
|
|
||||||
@ -560,6 +590,11 @@ class TaskStatus(object):
|
|||||||
return new_msg
|
return new_msg
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
links = list()
|
||||||
|
if self.links:
|
||||||
|
for k, v in self.links.items():
|
||||||
|
for r in v:
|
||||||
|
links.append(dict(rel=k, href=r))
|
||||||
return {
|
return {
|
||||||
'kind': 'Status',
|
'kind': 'Status',
|
||||||
'apiVersion': 'v1.0',
|
'apiVersion': 'v1.0',
|
||||||
@ -569,6 +604,7 @@ class TaskStatus(object):
|
|||||||
'status': self.status,
|
'status': self.status,
|
||||||
'successes': self.successes,
|
'successes': self.successes,
|
||||||
'failures': self.failures,
|
'failures': self.failures,
|
||||||
|
'links': links,
|
||||||
'details': {
|
'details': {
|
||||||
'errorCount': self.error_count,
|
'errorCount': self.error_count,
|
||||||
'messageList': [x.to_dict() for x in self.message_list],
|
'messageList': [x.to_dict() for x in self.message_list],
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
"""Definitions for Drydock database tables."""
|
"""Definitions for Drydock database tables."""
|
||||||
|
import copy
|
||||||
|
|
||||||
from sqlalchemy.schema import Table, Column
|
from sqlalchemy.schema import Table, Column
|
||||||
from sqlalchemy.types import Boolean, DateTime, String, Integer, Text
|
from sqlalchemy.types import Boolean, DateTime, String, Integer, Text
|
||||||
@ -30,7 +31,7 @@ class Tasks(ExtendTable):
|
|||||||
|
|
||||||
__tablename__ = 'tasks'
|
__tablename__ = 'tasks'
|
||||||
|
|
||||||
__schema__ = [
|
__baseschema__ = [
|
||||||
Column('task_id', pg.BYTEA(16), primary_key=True),
|
Column('task_id', pg.BYTEA(16), primary_key=True),
|
||||||
Column('parent_task_id', pg.BYTEA(16)),
|
Column('parent_task_id', pg.BYTEA(16)),
|
||||||
Column('subtask_id_list', pg.ARRAY(pg.BYTEA(16))),
|
Column('subtask_id_list', pg.ARRAY(pg.BYTEA(16))),
|
||||||
@ -54,13 +55,19 @@ class Tasks(ExtendTable):
|
|||||||
Column('terminate', Boolean, default=False)
|
Column('terminate', Boolean, default=False)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
__add_result_links__ = [
|
||||||
|
Column('result_links', pg.JSON),
|
||||||
|
]
|
||||||
|
|
||||||
|
__schema__ = copy.copy(__baseschema__)
|
||||||
|
__schema__.extend(__add_result_links__)
|
||||||
|
|
||||||
class ResultMessage(ExtendTable):
|
class ResultMessage(ExtendTable):
|
||||||
"""Table for tracking result/status messages."""
|
"""Table for tracking result/status messages."""
|
||||||
|
|
||||||
__tablename__ = 'result_message'
|
__tablename__ = 'result_message'
|
||||||
|
|
||||||
__schema__ = [
|
__baseschema__ = [
|
||||||
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(1024)),
|
Column('message', String(1024)),
|
||||||
@ -71,37 +78,43 @@ class ResultMessage(ExtendTable):
|
|||||||
Column('extra', pg.JSON)
|
Column('extra', pg.JSON)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
__schema__ = copy.copy(__baseschema__)
|
||||||
|
|
||||||
|
|
||||||
class ActiveInstance(ExtendTable):
|
class ActiveInstance(ExtendTable):
|
||||||
"""Table to organize multiple orchestrator instances."""
|
"""Table to organize multiple orchestrator instances."""
|
||||||
|
|
||||||
__tablename__ = 'active_instance'
|
__tablename__ = 'active_instance'
|
||||||
|
|
||||||
__schema__ = [
|
__baseschema__ = [
|
||||||
Column('dummy_key', Integer, primary_key=True),
|
Column('dummy_key', Integer, primary_key=True),
|
||||||
Column('identity', pg.BYTEA(16)),
|
Column('identity', pg.BYTEA(16)),
|
||||||
Column('last_ping', DateTime),
|
Column('last_ping', DateTime),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
__schema__ = copy.copy(__baseschema__)
|
||||||
|
|
||||||
|
|
||||||
class BootAction(ExtendTable):
|
class BootAction(ExtendTable):
|
||||||
"""Table persisting node build data."""
|
"""Table persisting node build data."""
|
||||||
|
|
||||||
__tablename__ = 'boot_action'
|
__tablename__ = 'boot_action'
|
||||||
|
|
||||||
__schema__ = [
|
__baseschema__ = [
|
||||||
Column('node_name', String(280), primary_key=True),
|
Column('node_name', String(280), primary_key=True),
|
||||||
Column('task_id', pg.BYTEA(16)),
|
Column('task_id', pg.BYTEA(16)),
|
||||||
Column('identity_key', pg.BYTEA(32)),
|
Column('identity_key', pg.BYTEA(32)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
__schema__ = copy.copy(__baseschema__)
|
||||||
|
|
||||||
|
|
||||||
class BootActionStatus(ExtendTable):
|
class BootActionStatus(ExtendTable):
|
||||||
"""Table tracking status of node boot actions."""
|
"""Table tracking status of node boot actions."""
|
||||||
|
|
||||||
__tablename__ = 'boot_action_status'
|
__tablename__ = 'boot_action_status'
|
||||||
|
|
||||||
__schema__ = [
|
__baseschema__ = [
|
||||||
Column('node_name', String(280), index=True),
|
Column('node_name', String(280), index=True),
|
||||||
Column('action_id', pg.BYTEA(16), primary_key=True),
|
Column('action_id', pg.BYTEA(16), primary_key=True),
|
||||||
Column('action_name', String(64)),
|
Column('action_name', String(64)),
|
||||||
@ -110,13 +123,15 @@ class BootActionStatus(ExtendTable):
|
|||||||
Column('action_status', String(32)),
|
Column('action_status', String(32)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
__schema__ = copy.copy(__baseschema__)
|
||||||
|
|
||||||
|
|
||||||
class BuildData(ExtendTable):
|
class BuildData(ExtendTable):
|
||||||
"""Table for persisting node build data."""
|
"""Table for persisting node build data."""
|
||||||
|
|
||||||
__tablename__ = 'build_data'
|
__tablename__ = 'build_data'
|
||||||
|
|
||||||
__schema__ = [
|
__baseschema__ = [
|
||||||
Column('node_name', String(32), index=True),
|
Column('node_name', String(32), index=True),
|
||||||
Column('task_id', pg.BYTEA(16), index=True),
|
Column('task_id', pg.BYTEA(16), index=True),
|
||||||
Column('collected_date', DateTime),
|
Column('collected_date', DateTime),
|
||||||
@ -124,3 +139,5 @@ class BuildData(ExtendTable):
|
|||||||
Column('data_format', String(32)),
|
Column('data_format', String(32)),
|
||||||
Column('data_element', Text),
|
Column('data_element', Text),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
__schema__ = copy.copy(__baseschema__)
|
||||||
|
@ -26,6 +26,9 @@
|
|||||||
# The URI database connect string. (string value)
|
# The URI database connect string. (string value)
|
||||||
#database_connect_string = <None>
|
#database_connect_string = <None>
|
||||||
|
|
||||||
|
# The SQLalchemy database connection pool size. (integer value)
|
||||||
|
#pool_size = 15
|
||||||
|
|
||||||
|
|
||||||
[keystone_authtoken]
|
[keystone_authtoken]
|
||||||
|
|
||||||
|
114
tests/unit/test_task_link.py
Normal file
114
tests/unit/test_task_link.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
'''Tests the functions for adding and retrieving task status links.'''
|
||||||
|
from drydock_provisioner.objects import TaskStatus
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskStatusLinks():
|
||||||
|
def test_links_add(self):
|
||||||
|
'''Add a link to a task status.'''
|
||||||
|
ts = TaskStatus()
|
||||||
|
|
||||||
|
relation = 'test'
|
||||||
|
uri = 'http://foo.com/test'
|
||||||
|
|
||||||
|
ts.add_link(relation, uri)
|
||||||
|
|
||||||
|
assert relation in ts.links
|
||||||
|
assert uri in ts.links.get(relation, [])
|
||||||
|
|
||||||
|
def test_links_get_empty(self):
|
||||||
|
'''Get links with an empty list.'''
|
||||||
|
ts = TaskStatus()
|
||||||
|
|
||||||
|
links = ts.get_links()
|
||||||
|
|
||||||
|
assert len(links) == 0
|
||||||
|
|
||||||
|
relation = 'test'
|
||||||
|
uri = 'http://foo.com/test'
|
||||||
|
|
||||||
|
ts.add_link(relation, uri)
|
||||||
|
links = ts.get_links(relation='none')
|
||||||
|
|
||||||
|
assert len(links) == 0
|
||||||
|
|
||||||
|
def test_links_get_all(self):
|
||||||
|
'''Get all links in a task status.'''
|
||||||
|
ts = TaskStatus()
|
||||||
|
|
||||||
|
relation = 'test'
|
||||||
|
uri = 'http://foo.com/test'
|
||||||
|
|
||||||
|
ts.add_link(relation, uri)
|
||||||
|
links = ts.get_links()
|
||||||
|
|
||||||
|
assert len(links) == 1
|
||||||
|
assert uri in links
|
||||||
|
|
||||||
|
def test_links_get_all_duplicate_relation(self):
|
||||||
|
'''Get all links where a relation has multiple uris.'''
|
||||||
|
ts = TaskStatus()
|
||||||
|
|
||||||
|
relation = 'test'
|
||||||
|
uri = 'http://foo.com/test'
|
||||||
|
uri2 = 'http://baz.com/test'
|
||||||
|
|
||||||
|
ts.add_link(relation, uri)
|
||||||
|
ts.add_link(relation, uri2)
|
||||||
|
|
||||||
|
links = ts.get_links()
|
||||||
|
|
||||||
|
assert len(links) == 2
|
||||||
|
assert uri in links
|
||||||
|
assert uri2 in links
|
||||||
|
|
||||||
|
def test_links_get_filter(self):
|
||||||
|
'''Get links with a filter.'''
|
||||||
|
ts = TaskStatus()
|
||||||
|
|
||||||
|
relation = 'test'
|
||||||
|
uri = 'http://foo.com/test'
|
||||||
|
|
||||||
|
relation2 = 'test2'
|
||||||
|
uri2 = 'http://baz.com/test'
|
||||||
|
|
||||||
|
ts.add_link(relation, uri)
|
||||||
|
ts.add_link(relation2, uri2)
|
||||||
|
|
||||||
|
links = ts.get_links(relation=relation)
|
||||||
|
|
||||||
|
assert len(links) == 1
|
||||||
|
assert uri in links
|
||||||
|
|
||||||
|
links = ts.get_links(relation=relation2)
|
||||||
|
|
||||||
|
assert len(links) == 1
|
||||||
|
assert uri2 in links
|
||||||
|
|
||||||
|
def test_links_serialization(self):
|
||||||
|
'''Check that task status serilization contains links correctly.'''
|
||||||
|
ts = TaskStatus()
|
||||||
|
|
||||||
|
relation = 'test'
|
||||||
|
uri = 'http://bar.com'
|
||||||
|
|
||||||
|
ts.set_message('foo')
|
||||||
|
ts.set_reason('bar')
|
||||||
|
ts.add_link(relation, uri)
|
||||||
|
|
||||||
|
ts_dict = ts.to_dict()
|
||||||
|
|
||||||
|
assert isinstance(ts_dict.get('links'), list)
|
||||||
|
assert {'rel': relation, 'href': uri} in ts_dict.get('links', [])
|
Loading…
Reference in New Issue
Block a user