Merge pull request #56 from sh8121att/feature/dryd9/drydock_cli

DRDY 9 - CLI and REST client
This commit is contained in:
Mark Burnett 2017-07-24 07:54:58 -05:00 committed by GitHub
commit 0b5b62f215
31 changed files with 1046 additions and 159 deletions

View File

@ -15,6 +15,7 @@ FROM ubuntu:16.04
ENV DEBIAN_FRONTEND noninteractive
ENV container docker
ENV PORT 9000
RUN apt -qq update && \
apt -y install git \
@ -39,7 +40,7 @@ COPY . /tmp/drydock
WORKDIR /tmp/drydock
RUN python3 setup.py install
EXPOSE 8000
EXPOSE $PORT
ENTRYPOINT ["./entrypoint.sh"]

82
docs/drydock_client.rst Normal file
View File

@ -0,0 +1,82 @@
===========================================================
drydock_client - client for drydock_provisioner RESTful API
===========================================================
The drydock_client module can be used to access a remote (or local)
Drydock REST API server. It supports tokenized authentication and
marking API calls with an external context marker for log aggregation.
It is composed of two parts - a DrydockSession which denotes the call
context for the API and a DrydockClient which gives access to actual
API calls.
Simple Usage
============
The usage pattern for drydock_client is to build a DrydockSession
with your credentials and the target host. Then use this session
to build a DrydockClient to make one or more API calls. The
DrydockSession will care for TCP connection pooling and header
management::
import drydock_provisioner.drydock_client.client as client
import drydock_provisioner.drydock_client.session as session
dd_session = session.DrydockSession('host.com', port=9000, token='abc123')
dd_client = client.DrydockClient(dd_session)
drydock_task = dd_client.get_task('ba44e582-6b26-11e7-81cc-080027ef795a')
Drydock Client Method API
=========================
drydock_client.client.DrydockClient supports the following methods for
accessing the Drydock RESTful API
get_design_ids
--------------
Return a list of UUID-formatted design IDs
get_design
----------
Provide a UUID-formatted design ID, receive back a dictionary representing
a objects.site.SiteDesign instance. You can provide the kwarg 'source' with
the value of 'compiled' to see the site design after inheritance is applied.
create_design
-------------
Create a new design. Optionally provide a new base design (by UUID-formatted
design_id) that the new design uses as the starting state. Receive back a
UUID-formatted string of design_id
get_part
--------
Get the attributes of a particular design part. Provide the design_id the part
is loaded in, the kind (one of 'Region', 'NetworkLink', 'Network', 'HardwareProfile',
'HostProfile' or 'BaremetalNode' and the part key (i.e. name). You can provide the kwarg
'source' with the value of 'compiled' to see the site design after inheritance is
applied.
load_parts
----------
Parse a provided YAML string and load the parts into the provided design context
get_tasks
---------
Get a list of all task ids
get_task
--------
Get the attributes of the task identified by the provided task_id
create_task
-----------
Create a task to execute the provided action on the provided design context

View File

@ -0,0 +1,29 @@
# 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.
""" Base classes for cli actions intended to invoke the api
"""
import logging
class CliAction: # pylint: disable=too-few-public-methods
""" Action base for CliActions
"""
def __init__(self, api_client):
self.logger = logging.getLogger('drydock_cli')
self.api_client = api_client
self.logger.debug("Action initialized with client %s", self.api_client.session.host)
def invoke(self):
""" The action to be taken. By default, this is not implemented
"""
raise NotImplementedError("Invoke method has not been implemented")

View File

@ -11,39 +11,69 @@
# 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.
""" The entry point for the cli commands
"""
import os
import logging
from urllib.parse import urlparse
import click
from drydock_provisioner.drydock_client.session import DrydockSession
from drydock_provisioner.drydock_client.client import DrydockClient
from .design import commands as design
from .part import commands as part
from .task import commands as task
@click.group()
@click.option('--debug/--no-debug', default=False)
@click.option('--token')
@click.option('--url')
@click.option('--debug/--no-debug',
help='Enable or disable debugging',
default=False)
@click.option('--token',
'-t',
help='The auth token to be used',
default=lambda: os.environ.get('DD_TOKEN', ''))
@click.option('--url',
'-u',
help='The url of the running drydock instance',
default=lambda: os.environ.get('DD_URL', ''))
@click.pass_context
def drydock(ctx, debug, token, url):
""" Drydock CLI to invoke the running instance of the drydock API
"""
if not ctx.obj:
ctx.obj = {}
ctx.obj['DEBUG'] = debug
if not token:
ctx.obj['TOKEN'] = os.environ['DD_TOKEN']
else:
ctx.obj['TOKEN'] = token
ctx.fail('Error: Token must be specified either by '
'--token or DD_TOKEN from the environment')
if not url:
ctx.obj['URL'] = os.environ['DD_URL']
else:
ctx.obj['URL'] = url
ctx.fail('Error: URL must be specified either by '
'--url or DD_URL from the environment')
@drydock.group()
def create():
pass
# setup logging for the CLI
# Setup root logger
logger = logging.getLogger('drydock_cli')
@drydock.group()
def list()
pass
logger.setLevel(logging.DEBUG if debug else logging.INFO)
logging_handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - '
'%(filename)s:%(funcName)s - %(message)s')
logging_handler.setFormatter(formatter)
logger.addHandler(logging_handler)
logger.debug('logging for cli initialized')
@drydock.group()
def show():
pass
# setup the drydock client using the passed parameters.
url_parse_result = urlparse(url)
logger.debug(url_parse_result)
if not url_parse_result.scheme:
ctx.fail('URL must specify a scheme and hostname, optionally a port')
ctx.obj['CLIENT'] = DrydockClient(DrydockSession(scheme=url_parse_result.scheme,
host=url_parse_result.netloc,
token=token))
@create.command()
def design
drydock.add_command(design.design)
drydock.add_command(part.part)
drydock.add_command(task.task)

View File

@ -0,0 +1,57 @@
# 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.
""" Actions related to design
"""
from drydock_provisioner.cli.action import CliAction
class DesignList(CliAction): # pylint: disable=too-few-public-methods
""" Action to list designs
"""
def __init__(self, api_client):
super().__init__(api_client)
self.logger.debug("DesignList action initialized")
def invoke(self):
return self.api_client.get_design_ids()
class DesignCreate(CliAction): # pylint: disable=too-few-public-methods
""" Action to create designs
"""
def __init__(self, api_client, base_design=None):
"""
:param string base_design: A UUID of the base design to model after
"""
super().__init__(api_client)
self.logger.debug("DesignCreate action initialized with base_design=%s", base_design)
self.base_design = base_design
def invoke(self):
return self.api_client.create_design(base_design=self.base_design)
class DesignShow(CliAction): # pylint: disable=too-few-public-methods
""" Action to show a design.
:param string design_id: A UUID design_id
:param string source: (Optional) The model source to return. 'designed' is as input,
'compiled' is after merging
"""
def __init__(self, api_client, design_id, source='designed'):
super().__init__(api_client)
self.design_id = design_id
self.source = source
self.logger.debug("DesignShow action initialized for design_id = %s", design_id)
def invoke(self):
return self.api_client.get_design(design_id=self.design_id, source=self.source)

View File

@ -0,0 +1,57 @@
# 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.
""" cli.design.commands
Contains commands related to designs
"""
import click
from drydock_provisioner.cli.design.actions import DesignList
from drydock_provisioner.cli.design.actions import DesignShow
from drydock_provisioner.cli.design.actions import DesignCreate
@click.group()
def design():
""" Drydock design commands
"""
pass
@design.command(name='create')
@click.option('--base-design',
'-b',
help='The base design to model this new design after')
@click.pass_context
def design_create(ctx, base_design=None):
""" Create a design
"""
click.echo(DesignCreate(ctx.obj['CLIENT'], base_design).invoke())
@design.command(name='list')
@click.pass_context
def design_list(ctx):
""" List designs
"""
click.echo(DesignList(ctx.obj['CLIENT']).invoke())
@design.command(name='show')
@click.option('--design-id',
'-i',
help='The design id to show')
@click.pass_context
def design_show(ctx, design_id):
""" show designs
"""
if not design_id:
ctx.fail('The design id must be specified by --design-id')
click.echo(DesignShow(ctx.obj['CLIENT'], design_id).invoke())

View File

@ -0,0 +1,86 @@
# 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.
""" Actions related to part command
"""
from drydock_provisioner.cli.action import CliAction
class PartBase(CliAction): # pylint: disable=too-few-public-methods
""" base class to set up part actions requiring a design_id
"""
def __init__(self, api_client, design_id):
super().__init__(api_client)
self.design_id = design_id
self.logger.debug('Initializing a Part action with design_id=%s', design_id)
class PartList(PartBase): # pylint: disable=too-few-public-methods
""" Action to list parts of a design
"""
def __init__(self, api_client, design_id):
"""
:param DrydockClient api_client: The api client used for invocation.
:param string design_id: The UUID of the design for which to list parts
"""
super().__init__(api_client, design_id)
self.logger.debug('PartList action initialized')
def invoke(self):
#TODO: change the api call
return 'This function does not yet have an implementation to support the request'
class PartCreate(PartBase): # pylint: disable=too-few-public-methods
""" Action to create parts of a design
"""
def __init__(self, api_client, design_id, in_file):
"""
:param DrydockClient api_client: The api client used for invocation.
:param string design_id: The UUID of the design for which to create a part
:param in_file: The file containing the specification of the part
"""
super().__init__(api_client, design_id)
self.in_file = in_file
self.logger.debug('PartCreate action init. Input file (trunc to 100 chars)=%s', in_file[:100])
def invoke(self):
return self.api_client.load_parts(self.design_id, self.in_file)
class PartShow(PartBase): # pylint: disable=too-few-public-methods
""" Action to show a part of a design.
"""
def __init__(self, api_client, design_id, kind, key, source='designed'):
"""
:param DrydockClient api_client: The api client used for invocation.
:param string design_id: the UUID of the design containing this part
:param string kind: the string represesnting the 'kind' of the document to return
:param string key: the string representing the key of the document to return.
:param string source: 'designed' (default) if this is the designed version,
'compiled' if the compiled version (after merging)
"""
super().__init__(api_client, design_id)
self.kind = kind
self.key = key
self.source = source
self.logger.debug('DesignShow action initialized for design_id=%s,'
' kind=%s, key=%s, source=%s',
design_id,
kind,
key,
source)
def invoke(self):
return self.api_client.get_part(design_id=self.design_id,
kind=self.kind,
key=self.key,
source=self.source)

View File

@ -0,0 +1,85 @@
# 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.
""" cli.part.commands
Contains commands related to parts of designs
"""
import click
from drydock_provisioner.cli.part.actions import PartList
from drydock_provisioner.cli.part.actions import PartShow
from drydock_provisioner.cli.part.actions import PartCreate
@click.group()
@click.option('--design-id',
'-d',
help='The id of the design containing the target parts')
@click.pass_context
def part(ctx, design_id=None):
""" Drydock part commands
"""
if not design_id:
ctx.fail('Error: Design id must be specified using --design-id')
ctx.obj['DESIGN_ID'] = design_id
@part.command(name='create')
@click.option('--file',
'-f',
help='The file name containing the part to create')
@click.pass_context
def part_create(ctx, file=None):
""" Create a part
"""
if not file:
ctx.fail('A file to create a part is required using --file')
with open(file, 'r') as file_input:
file_contents = file_input.read()
# here is where some potential validation could be done on the input file
click.echo(PartCreate(ctx.obj['CLIENT'],
design_id=ctx.obj['DESIGN_ID'],
in_file=file_contents).invoke())
@part.command(name='list')
@click.pass_context
def part_list(ctx):
""" List parts of a design
"""
click.echo(PartList(ctx.obj['CLIENT'], design_id=ctx.obj['DESIGN_ID']).invoke())
@part.command(name='show')
@click.option('--source',
'-s',
help='designed | compiled')
@click.option('--kind',
'-k',
help='The kind value of the document to show')
@click.option('--key',
'-i',
help='The key value of the document to show')
@click.pass_context
def part_show(ctx, source, kind, key):
""" show a part of a design
"""
if not kind:
ctx.fail('The kind must be specified by --kind')
if not key:
ctx.fail('The key must be specified by --key')
click.echo(PartShow(ctx.obj['CLIENT'],
design_id=ctx.obj['DESIGN_ID'],
kind=kind,
key=key,
source=source).invoke())

View File

@ -0,0 +1,83 @@
# 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.
""" Actions related to task commands
"""
from drydock_provisioner.cli.action import CliAction
class TaskList(CliAction): # pylint: disable=too-few-public-methods
""" Action to list tasks
"""
def __init__(self, api_client):
"""
:param DrydockClient api_client: The api client used for invocation.
"""
super().__init__(api_client)
self.logger.debug('TaskList action initialized')
def invoke(self):
return self.api_client.get_tasks()
class TaskCreate(CliAction): # pylint: disable=too-few-public-methods
""" Action to create tasks against a design
"""
def __init__(self, api_client, design_id, action_name=None, node_names=None, rack_names=None, node_tags=None):
"""
:param DrydockClient api_client: The api client used for invocation.
:param string design_id: The UUID of the design for which to create a task
:param string action_name: The name of the action being performed for this task
:param List node_names: The list of node names to restrict action application
:param List rack_names: The list of rack names to restrict action application
:param List node_tags: The list of node tags to restrict action application
"""
super().__init__(api_client)
self.design_id = design_id
self.action_name = action_name
self.logger.debug('TaskCreate action initialized for design=%s', design_id)
self.logger.debug('Action is %s', action_name)
if node_names is None:
node_names = []
if rack_names is None:
rack_names = []
if node_tags is None:
node_tags = []
self.logger.debug("Node names = %s", node_names)
self.logger.debug("Rack names = %s", rack_names)
self.logger.debug("Node tags = %s", node_tags)
self.node_filter = {'node_names' : node_names,
'rack_names' : rack_names,
'node_tags' : node_tags
}
def invoke(self):
return self.api_client.create_task(design_id=self.design_id,
task_action=self.action_name,
node_filter=self.node_filter)
class TaskShow(CliAction): # pylint: disable=too-few-public-methods
""" Action to show a task's detial.
"""
def __init__(self, api_client, task_id):
"""
:param DrydockClient api_client: The api client used for invocation.
:param string task_id: the UUID of the task to retrieve
"""
super().__init__(api_client)
self.task_id = task_id
self.logger.debug('TaskShow action initialized for task_id=%s,', task_id)
def invoke(self):
return self.api_client.get_task(task_id=self.task_id)

View File

@ -0,0 +1,81 @@
# 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.
""" cli.task.commands
Contains commands related to tasks against designs
"""
import click
from drydock_provisioner.cli.task.actions import TaskList
from drydock_provisioner.cli.task.actions import TaskShow
from drydock_provisioner.cli.task.actions import TaskCreate
@click.group()
def task():
""" Drydock task commands
"""
@task.command(name='create')
@click.option('--design-id',
'-d',
help='The design id for this action')
@click.option('--action',
'-a',
help='The action to perform')
@click.option('--node-names',
'-n',
help='The nodes targeted by this action, comma separated')
@click.option('--rack-names',
'-r',
help='The racks targeted by this action, comma separated')
@click.option('--node-tags',
'-t',
help='The nodes by tag name targeted by this action, comma separated')
@click.pass_context
def task_create(ctx, design_id=None, action=None, node_names=None, rack_names=None, node_tags=None):
""" Create a task
"""
if not design_id:
ctx.fail('Error: Design id must be specified using --design-id')
if not action:
ctx.fail('Error: Action must be specified using --action')
click.echo(TaskCreate(ctx.obj['CLIENT'],
design_id=design_id,
action_name=action,
node_names=[x.strip() for x in node_names.split(',')] if node_names else [],
rack_names=[x.strip() for x in rack_names.split(',')] if rack_names else [],
node_tags=[x.strip() for x in node_tags.split(',')] if node_tags else []
).invoke())
@task.command(name='list')
@click.pass_context
def task_list(ctx):
""" List tasks.
"""
click.echo(TaskList(ctx.obj['CLIENT']).invoke())
@task.command(name='show')
@click.option('--task-id',
'-t',
help='The required task id')
@click.pass_context
def task_show(ctx, task_id=None):
""" show a task's details
"""
if not task_id:
ctx.fail('The task id must be specified by --task-id')
click.echo(TaskShow(ctx.obj['CLIENT'],
task_id=task_id).invoke())

View File

@ -114,6 +114,32 @@ class DesignsPartsResource(StatefulResource):
except LookupError:
self.return_error(resp, falcon.HTTP_400, message="Ingester %s not registered" % ingester_name, retry=False)
def on_get(self, req, resp, design_id):
try:
design = self.state_manager.get_design(design_id)
except DesignError:
self.return_error(resp, falcon.HTTP_404, message="Design %s nout found" % design_id, retry=False)
part_catalog = []
site = design.get_site()
part_catalog.append({'kind': 'Region', 'key': site.get_id()})
part_catalog.extend([{'kind': 'Network', 'key': n.get_id()} for n in design.networks])
part_catalog.extend([{'kind': 'NetworkLink', 'key': l.get_id()} for l in design.network_links])
part_catalog.extend([{'kind': 'HostProfile', 'key': p.get_id()} for p in design.host_profiles])
part_catalog.extend([{'kind': 'HardwareProfile', 'key': p.get_id()} for p in design.hardware_profiles])
part_catalog.extend([{'kind': 'BaremetalNode', 'key': n.get_id()} for n in design.baremetal_nodes])
resp.body = json.dumps(part_catalog)
resp.status = falcon.HTTP_200
return
class DesignsPartsKindsResource(StatefulResource):
def __init__(self, **kwargs):
@ -161,4 +187,4 @@ class DesignsPartResource(StatefulResource):
resp.body = json.dumps(part.obj_to_simple())
except errors.DesignError as dex:
self.error(req.context, str(dex))
self.return_error(resp, falcon.HTTP_404, message=str(dex), retry=False)
self.return_error(resp, falcon.HTTP_404, message=str(dex), retry=False)

View File

@ -84,8 +84,8 @@ class LoggingMiddleware(object):
ctx = req.context
extra = {
'user': ctx.user,
'req_id': ctx.req_id,
'req_id': ctx.request_id,
'external_ctx': ctx.external_marker,
}
resp.append_header('X-Drydock-Req', ctx.req_id)
resp.append_header('X-Drydock-Req', ctx.request_id)
self.logger.info("%s - %s" % (req.uri, resp.status), extra=extra)

View File

@ -34,18 +34,17 @@ class TasksResource(StatefulResource):
try:
json_data = self.req_json(req)
sitename = json_data.get('sitename', None)
design_id = json_data.get('design_id', None)
action = json_data.get('action', None)
node_filter = json_data.get('node_filter', None)
if sitename is None or design_id is None or action is None:
self.info(req.context, "Task creation requires fields sitename, design_id, action")
self.return_error(resp, falcon.HTTP_400, message="Task creation requires fields sitename, design_id, action", retry=False)
if design_id is None or action is None:
self.info(req.context, "Task creation requires fields design_id, action")
self.return_error(resp, falcon.HTTP_400, message="Task creation requires fields design_id, action", retry=False)
return
task = self.orchestrator.create_task(obj_task.OrchestratorTask, site=sitename,
design_id=design_id, action=action, node_filter=node_filter)
task = self.orchestrator.create_task(obj_task.OrchestratorTask, design_id=design_id,
action=action, node_filter=node_filter)
task_thread = threading.Thread(target=self.orchestrator.execute_task, args=[task.get_id()])
task_thread.start()

View File

@ -74,7 +74,7 @@ class MaasNodeDriver(NodeDriver):
status=hd_fields.TaskStatus.Complete,
result=hd_fields.ActionResult.Success)
return
except errors.TransientDriverError(ex):
except errors.TransientDriverError as ex:
result = {
'retry': True,
'detail': str(ex),
@ -84,7 +84,7 @@ class MaasNodeDriver(NodeDriver):
result=hd_fields.ActionResult.Failure,
result_details=result)
return
except errors.PersistentDriverError(ex):
except errors.PersistentDriverError as ex:
result = {
'retry': False,
'detail': str(ex),
@ -94,7 +94,7 @@ class MaasNodeDriver(NodeDriver):
result=hd_fields.ActionResult.Failure,
result_details=result)
return
except Exception(ex):
except Exception as ex:
result = {
'retry': False,
'detail': str(ex),
@ -111,11 +111,6 @@ class MaasNodeDriver(NodeDriver):
raise errors.DriverError("No design ID specified in task %s" %
(task_id))
if task.site_name is None:
raise errors.DriverError("No site specified for task %s." %
(task_id))
self.orchestrator.task_field_update(task.get_id(),
status=hd_fields.TaskStatus.Running)
@ -127,8 +122,7 @@ class MaasNodeDriver(NodeDriver):
subtask = self.orchestrator.create_task(task_model.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
action=task.action, site_name=task.site_name,
task_scope={'site': task.site_name})
action=task.action)
runner = MaasTaskRunner(state_manager=self.state_manager,
orchestrator=self.orchestrator,
task_id=subtask.get_id())
@ -165,8 +159,7 @@ class MaasNodeDriver(NodeDriver):
subtask = self.orchestrator.create_task(task_model.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
action=task.action, site_name=task.site_name,
task_scope={'site': task.site_name})
action=task.action)
runner = MaasTaskRunner(state_manager=self.state_manager,
orchestrator=self.orchestrator,
task_id=subtask.get_id())
@ -211,8 +204,7 @@ class MaasNodeDriver(NodeDriver):
subtask = self.orchestrator.create_task(task_model.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
action=hd_fields.OrchestratorAction.IdentifyNode,
site_name=task.site_name,
task_scope={'site': task.site_name, 'node_names': [n]})
task_scope={'node_names': [n]})
runner = MaasTaskRunner(state_manager=self.state_manager,
orchestrator=self.orchestrator,
task_id=subtask.get_id())
@ -286,8 +278,7 @@ class MaasNodeDriver(NodeDriver):
subtask = self.orchestrator.create_task(task_model.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
action=hd_fields.OrchestratorAction.ConfigureHardware,
site_name=task.site_name,
task_scope={'site': task.site_name, 'node_names': [n]})
task_scope={'node_names': [n]})
runner = MaasTaskRunner(state_manager=self.state_manager,
orchestrator=self.orchestrator,
task_id=subtask.get_id())
@ -361,8 +352,7 @@ class MaasNodeDriver(NodeDriver):
subtask = self.orchestrator.create_task(task_model.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
action=hd_fields.OrchestratorAction.ApplyNodeNetworking,
site_name=task.site_name,
task_scope={'site': task.site_name, 'node_names': [n]})
task_scope={'node_names': [n]})
runner = MaasTaskRunner(state_manager=self.state_manager,
orchestrator=self.orchestrator,
task_id=subtask.get_id())
@ -436,8 +426,7 @@ class MaasNodeDriver(NodeDriver):
subtask = self.orchestrator.create_task(task_model.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
action=hd_fields.OrchestratorAction.ApplyNodePlatform,
site_name=task.site_name,
task_scope={'site': task.site_name, 'node_names': [n]})
task_scope={'node_names': [n]})
runner = MaasTaskRunner(state_manager=self.state_manager,
orchestrator=self.orchestrator,
task_id=subtask.get_id())
@ -512,8 +501,7 @@ class MaasNodeDriver(NodeDriver):
subtask = self.orchestrator.create_task(task_model.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
action=hd_fields.OrchestratorAction.DeployNode,
site_name=task.site_name,
task_scope={'site': task.site_name, 'node_names': [n]})
task_scope={'node_names': [n]})
runner = MaasTaskRunner(state_manager=self.state_manager,
orchestrator=self.orchestrator,
task_id=subtask.get_id())

View File

@ -57,11 +57,6 @@ class ManualDriver(oob.OobDriver):
raise errors.DriverError("No design ID specified in task %s" %
(task_id))
if task.site_name is None:
raise errors.DriverError("Not site specified for task %s." %
(task_id))
self.orchestrator.task_field_update(task.get_id(),
status=hd_fields.TaskStatus.Running)

View File

@ -67,11 +67,6 @@ class PyghmiDriver(oob.OobDriver):
raise errors.DriverError("No design ID specified in task %s" %
(task_id))
if task.site_name is None:
raise errors.DriverError("Not site specified for task %s." %
(task_id))
self.orchestrator.task_field_update(task.get_id(),
status=hd_fields.TaskStatus.Running)
@ -98,8 +93,7 @@ class PyghmiDriver(oob.OobDriver):
subtask = self.orchestrator.create_task(task_model.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
action=task.action,
task_scope={'site': task.site_name,
'node_names': [n.get_name()]})
task_scope={'node_names': [n.get_name()]})
incomplete_subtasks.append(subtask.get_id())
runner = PyghmiTaskRunner(state_manager=self.state_manager,

View File

@ -14,75 +14,180 @@
import json
import requests
from .session import DrydockSession
from drydock_provisioner import error as errors
class DrydockClient(object):
""""
A client for the Drydock API
:param string host: Hostname or IP address of Drydock API
:param string port: Port number of Drydock API
:param string version: API version to access
:param string token: Authentication token to use
:param string marker: (optional) External marker to include with requests
:param DrydockSession session: A instance of DrydockSession to be used by this client
"""
def __init__(self, host=None, port=9000, version='1.0', token=None, marker=None):
self.version = version
self.session = DrydockSession(token=token, ext_marker=marker)
self.base_url = "http://%s:%d/api/%s/" % (host, port, version)
def __init__(self, session):
self.session = session
def send_get(self, api_url, query=None):
"""
Send a GET request to Drydock.
:param string api_url: The URL string following the hostname and API prefix
:param dict query: A dict of k, v pairs to add to the query string
:return: A requests.Response object
"""
resp = requests.get(self.base_url + api_url, params=query)
return resp
def send_post(self, api_url, query=None, body=None, data=None):
"""
Send a POST request to Drydock. If both body and data are specified,
body will will be used.
:param string api_url: The URL string following the hostname and API prefix
:param dict query: A dict of k, v parameters to add to the query string
:param string body: A string to use as the request body. Will be treated as raw
:param data: Something json.dumps(s) can serialize. Result will be used as the request body
:return: A requests.Response object
"""
if body is not None:
resp = requests.post(self.base_url + api_url, params=query, data=body)
else:
resp = requests.post(self.base_url + api_url, params=query, json=data)
return resp
def get_designs(self):
def get_design_ids(self):
"""
Get list of Drydock design_ids
:return: A list of string design_ids
"""
endpoint = 'v1.0/designs'
def get_design(self, design_id):
resp = self.session.get(endpoint)
def create_design(self):
if resp.status_code != 200:
raise errors.ClientError("Received a %d from GET URL: %s" % (resp.status_code, endpoint),
code=resp.status_code)
else:
return resp.json()
def get_parts(self, design_id):
def get_design(self, design_id, source='designed'):
"""
Get a full design based on the passed design_id
def get_part(self, design_id, kind, key):
:param string design_id: A UUID design_id
:param string source: The model source to return. 'designed' is as input, 'compiled' is after merging
:return: A dict of the design and all currently loaded design parts
"""
endpoint = "v1.0/designs/%s" % design_id
resp = self.session.get(endpoint, query={'source': source})
if resp.status_code == 404:
raise errors.ClientError("Design ID %s not found." % (design_id), code=404)
elif resp.status_code != 200:
raise errors.ClientError("Received a %d from GET URL: %s" % (resp.status_code, endpoint),
code=resp.status_code)
else:
return resp.json()
def create_design(self, base_design=None):
"""
Create a new design context for holding design parts
:param string base_design: String UUID of the base design to build this design upon
:return string: String UUID of the design ID
"""
endpoint = 'v1.0/designs'
if base_design is not None:
resp = self.session.post(endpoint, data={'base_design_id': base_design})
else:
resp = self.session.post(endpoint)
if resp.status_code != 201:
raise errors.ClientError("Received a %d from POST URL: %s" % (resp.status_code, endpoint),
code=resp.status_code)
else:
design = resp.json()
return design.get('id', None)
def get_part(self, design_id, kind, key, source='designed'):
"""
Query the model definition of a design part
:param string design_id: The string UUID of the design context to query
:param string kind: The design part kind as defined in the Drydock design YAML schema
:param string key: The design part key, generally a name.
:param string source: The model source to return. 'designed' is as input, 'compiled' is after merging
:return: A dict of the design part
"""
endpoint = "v1.0/designs/%s/parts/%s/%s" % (design_id, kind, key)
resp = self.session.get(endpoint, query={'source': source})
if resp.status_code == 404:
raise errors.ClientError("%s %s in design %s not found" % (key, kind, design_id), code=404)
elif resp.status_code != 200:
raise errors.ClientError("Received a %d from GET URL: %s" % (resp.status_code, endpoint),
code=resp.status_code)
else:
return resp.json()
def load_parts(self, design_id, yaml_string=None):
"""
Load new design parts into a design context via YAML conforming to the Drydock design YAML schema
:param string design_id: String uuid design_id of the design context
:param string yaml_string: A single or multidoc YAML string to be ingested
:return: Dict of the parsed design parts
"""
endpoint = "v1.0/designs/%s/parts" % (design_id)
resp = self.session.post(endpoint, query={'ingester': 'yaml'}, body=yaml_string)
if resp.status_code == 400:
raise errors.ClientError("Invalid inputs: %s" % resp.text, code=resp.status_code)
elif resp.status_code == 500:
raise errors.ClientError("Server error: %s" % resp.text, code=resp.status_code)
elif resp.status_code == 201:
return resp.json()
else:
raise errors.ClientError("Uknown error. Received %d" % resp.status_code,
code=resp.status_code)
def get_tasks(self):
"""
Get a list of all the tasks, completed or running.
:return: List of string uuid task IDs
"""
endpoint = "v1.0/tasks"
resp = self.session.get(endpoint)
if resp.status_code != 200:
raise errors.ClientError("Server error: %s" % resp.text, code=resp.status_code)
else:
return resp.json()
def get_task(self, task_id):
"""
Get the current description of a Drydock task
:param string task_id: The string uuid task id to query
:return: A dict representing the current state of the task
"""
endpoint = "v1.0/tasks/%s" % (task_id)
resp = self.session.get(endpoint)
if resp.status_code == 200:
return resp.json()
elif resp.status_code == 404:
raise errors.ClientError("Task %s not found" % task_id, code=resp.status_code)
else:
raise errors.ClientError("Server error: %s" % resp.text, code=resp.status_code)
def create_task(self, design_id, task_action, node_filter=None):
"""
Create a new task in Drydock
:param string design_id: A string uuid identifying the design context the task should operate on
:param string task_action: The action that should be executed
:param dict node_filter: A filter for narrowing the scope of the task. Valid fields are 'node_names',
'rack_names', 'node_tags'.
:return: The string uuid of the create task's id
"""
endpoint = 'v1.0/tasks'
task_dict = {
'action': task_action,
'design_id': design_id,
'node_filter': node_filter
}
resp = self.session.post(endpoint, data=task_dict)
if resp.status_code == 201:
return resp.json().get('task_id')
elif resp.status_code == 400:
raise errors.ClientError("Invalid inputs, received a %d: %s" % (resp.status_code, resp.text),
code=resp.status_code)

View File

@ -11,16 +11,63 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import requests
class DrydockSession(object)
class DrydockSession(object):
"""
A session to the Drydock API maintaining credentials and API options
:param string host: The Drydock server hostname or IP
:param int port: (optional) The service port appended if specified
:param string token: Auth token
:param string marker: (optional) external context marker
"""
def __init__(self, token=None, marker=None):
def __init__(self, host, *, port=None, scheme='http', token=None, marker=None):
self.__session = requests.Session()
self.__session.headers.update({'X-Auth-Token': token, 'X-Context-Marker': marker})
self.host = host
self.scheme = scheme
if port:
self.port = port
self.base_url = "%s://%s:%s/api/" % (self.scheme, self.host, self.port)
else:
#assume default port for scheme
self.base_url = "%s://%s/api/" % (self.scheme, self.host)
self.token = token
self.marker = marker
# TODO Add keystone authentication to produce a token for this session
def get(self, endpoint, query=None):
"""
Send a GET request to Drydock.
:param string endpoint: The URL string following the hostname and API prefix
:param dict query: A dict of k, v pairs to add to the query string
:return: A requests.Response object
"""
resp = self.__session.get(self.base_url + endpoint, params=query, timeout=10)
return resp
def post(self, endpoint, query=None, body=None, data=None):
"""
Send a POST request to Drydock. If both body and data are specified,
body will will be used.
:param string endpoint: The URL string following the hostname and API prefix
:param dict query: A dict of k, v parameters to add to the query string
:param string body: A string to use as the request body. Will be treated as raw
:param data: Something json.dumps(s) can serialize. Result will be used as the request body
:return: A requests.Response object
"""
if body is not None:
resp = self.__session.post(self.base_url + endpoint, params=query, data=body, timeout=10)
else:
resp = self.__session.post(self.base_url + endpoint, params=query, json=data, timeout=10)
return resp

View File

@ -11,6 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
class DesignError(Exception):
pass
@ -37,7 +38,22 @@ class PersistentDriverError(DriverError):
pass
class ApiError(Exception):
pass
def __init__(self, msg, code=500):
super().__init__(msg)
self.message = msg
self.status_code = code
def to_json(self):
err_dict = {'error': msg, 'type': self.__class__.__name__}
return json.dumps(err_dict)
class InvalidFormat(ApiError):
pass
def __init__(self, msg, code=400):
super(InvalidFormat, self).__init__(msg, code=code)
class ClientError(Exception):
def __init__(self, msg, code=500):
super().__init__(msg)
self.message = msg
self.status_code = code

View File

@ -333,6 +333,7 @@ class YamlIngester(IngesterPlugin):
node_metadata = spec.get('metadata', {})
metadata_tags = node_metadata.get('tags', [])
model.tags = metadata_tags
owner_data = node_metadata.get('owner_data', {})
model.owner_data = {}

View File

@ -79,15 +79,9 @@ class Task(object):
class OrchestratorTask(Task):
def __init__(self, site=None, design_id=None, **kwargs):
def __init__(self, design_id=None, **kwargs):
super(OrchestratorTask, self).__init__(**kwargs)
# Validate parameters based on action
self.site = site
if self.site is None:
raise ValueError("Orchestration Task requires 'site' parameter")
self.design_id = design_id
if self.action in [hd_fields.OrchestratorAction.VerifyNode,
@ -99,7 +93,6 @@ class OrchestratorTask(Task):
def to_dict(self):
_dict = super(OrchestratorTask, self).to_dict()
_dict['site'] = self.site
_dict['design_id'] = self.design_id
_dict['node_filter'] = getattr(self, 'node_filter', None)
@ -109,17 +102,14 @@ class DriverTask(Task):
def __init__(self, task_scope={}, **kwargs):
super(DriverTask, self).__init__(**kwargs)
self.design_id = kwargs.get('design_id', 0)
self.site_name = task_scope.get('site', None)
self.design_id = kwargs.get('design_id', None)
self.node_list = task_scope.get('node_names', [])
def to_dict(self):
_dict = super(DriverTask, self).to_dict()
_dict['site_name'] = self.site_name
_dict['design_id'] = self.design_id
_dict['node_list'] = self.node_list
return _dict
return _dict

View File

@ -94,7 +94,6 @@ class Orchestrator(object):
% (task_id))
design_id = task.design_id
task_site = task.site
# Just for testing now, need to implement with enabled_drivers
# logic
@ -157,16 +156,11 @@ class Orchestrator(object):
result=hd_fields.ActionResult.Failure)
return
task_scope = {
'site': task.site
}
worked = failed = False
site_network_task = self.create_task(tasks.DriverTask,
parent_task_id=task.get_id(),
design_id=design_id,
task_scope=task_scope,
action=hd_fields.OrchestratorAction.CreateNetworkTemplate)
self.logger.info("Starting node driver task %s to create network templates" % (site_network_task.get_id()))
@ -187,7 +181,6 @@ class Orchestrator(object):
user_creds_task = self.create_task(tasks.DriverTask,
parent_task_id=task.get_id(),
design_id=design_id,
task_scope=task_scope,
action=hd_fields.OrchestratorAction.ConfigureUserCredentials)
self.logger.info("Starting node driver task %s to configure user credentials" % (user_creds_task.get_id()))
@ -217,8 +210,7 @@ class Orchestrator(object):
result=final_result)
return
elif task.action == hd_fields.OrchestratorAction.VerifyNode:
self.task_field_update(task_id,
status=hd_fields.TaskStatus.Running)
self.task_field_update(task_id, status=hd_fields.TaskStatus.Running)
site_design = self.get_effective_site(design_id)
@ -253,8 +245,7 @@ class Orchestrator(object):
target_names = [x.get_name() for x in oob_nodes]
task_scope = {'site' : task_site,
'node_names' : target_names}
task_scope = {'node_names' : target_names}
oob_driver_task = self.create_task(tasks.DriverTask,
parent_task_id=task.get_id(),
@ -341,8 +332,7 @@ class Orchestrator(object):
target_names = [x.get_name() for x in oob_nodes]
task_scope = {'site' : task_site,
'node_names' : target_names}
task_scope = {'node_names' : target_names}
setboot_task = self.create_task(tasks.DriverTask,
parent_task_id=task.get_id(),
@ -429,8 +419,7 @@ class Orchestrator(object):
node_commission_task = self.create_task(tasks.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
action=hd_fields.OrchestratorAction.ConfigureHardware,
task_scope={'site': task_site,
'node_names': node_identify_task.result_detail['successful_nodes']})
task_scope={'node_names': node_identify_task.result_detail['successful_nodes']})
self.logger.info("Starting node driver task %s to commission nodes." % (node_commission_task.get_id()))
node_driver.execute_task(node_commission_task.get_id())
@ -482,8 +471,7 @@ class Orchestrator(object):
target_names = [x.get_name() for x in target_nodes]
task_scope = {'site' : task_site,
'node_names' : target_names}
task_scope = {'node_names' : target_names}
node_networking_task = self.create_task(tasks.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
@ -510,8 +498,7 @@ class Orchestrator(object):
node_platform_task = self.create_task(tasks.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
action=hd_fields.OrchestratorAction.ApplyNodePlatform,
task_scope={'site': task_site,
'node_names': node_networking_task.result_detail['successful_nodes']})
task_scope={'node_names': node_networking_task.result_detail['successful_nodes']})
self.logger.info("Starting node driver task %s to configure node platform." % (node_platform_task.get_id()))
node_driver.execute_task(node_platform_task.get_id())
@ -532,8 +519,7 @@ class Orchestrator(object):
node_deploy_task = self.create_task(tasks.DriverTask,
parent_task_id=task.get_id(), design_id=design_id,
action=hd_fields.OrchestratorAction.DeployNode,
task_scope={'site': task_site,
'node_names': node_platform_task.result_detail['successful_nodes']})
task_scope={'node_names': node_platform_task.result_detail['successful_nodes']})
self.logger.info("Starting node driver task %s to deploy nodes." % (node_deploy_task.get_id()))
node_driver.execute_task(node_deploy_task.get_id())

View File

@ -2,7 +2,7 @@
set -ex
CMD="drydock"
PORT="8000"
PORT=${PORT:-9000}
if [ "$1" = 'server' ]; then
exec uwsgi --http :${PORT} -w drydock_provisioner.drydock --callable drydock --enable-threads -L --pyargv "--config-file /etc/drydock/drydock.conf"

View File

@ -346,4 +346,4 @@ spec:
address: 'dhcp'
- network: 'mgmt'
address: '172.16.1.83'
---
...

View File

@ -8,3 +8,4 @@ oauthlib
uwsgi>1.4
bson===0.4.7
oslo.config
click===6.7

6
requirements-test.txt Normal file
View File

@ -0,0 +1,6 @@
pytest-mock
pytest
responses
mock
tox
oslo.versionedobjects[fixtures]>=1.23.0

View File

@ -40,7 +40,12 @@ setup(name='drydock_provisioner',
'drydock_provisioner.drivers.node',
'drydock_provisioner.drivers.node.maasdriver',
'drydock_provisioner.drivers.node.maasdriver.models',
'drydock_provisioner.control'],
'drydock_provisioner.control',
'drydock_provisioner.cli',
'drydock_provisioner.cli.design',
'drydock_provisioner.cli.part',
'drydock_provisioner.cli.task',
'drydock_provisioner.drydock_client'],
install_requires=[
'PyYAML',
'pyghmi>=1.0.18',
@ -55,6 +60,7 @@ setup(name='drydock_provisioner',
],
entry_points={
'oslo.config.opts': 'drydock_provisioner = drydock_provisioner.config:list_opts',
'console_scripts': 'drydock = drydock_provisioner.cli.commands:drydock'
}
)

View File

@ -1,5 +0,0 @@
pytest-mock
pytest
mock
tox
oslo.versionedobjects[fixtures]>=1.23.0

View File

@ -0,0 +1,141 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pytest
import responses
import drydock_provisioner.drydock_client.session as dc_session
import drydock_provisioner.drydock_client.client as dc_client
def test_blank_session_error():
with pytest.raises(Exception):
dd_ses = dc_session.DrydockSession()
def test_session_init_minimal():
port = 9000
host = 'foo.bar.baz'
dd_ses = dc_session.DrydockSession(host, port=port)
assert dd_ses.base_url == "http://%s:%d/api/" % (host, port)
def test_session_init_minimal_no_port():
host = 'foo.bar.baz'
dd_ses = dc_session.DrydockSession(host)
assert dd_ses.base_url == "http://%s/api/" % (host)
def test_session_init_uuid_token():
host = 'foo.bar.baz'
token = '5f1e08b6-38ec-4a99-9d0f-00d29c4e325b'
dd_ses = dc_session.DrydockSession(host, token=token)
assert dd_ses.base_url == "http://%s/api/" % (host)
assert dd_ses.token == token
def test_session_init_fernet_token():
host = 'foo.bar.baz'
token = 'gAAAAABU7roWGiCuOvgFcckec-0ytpGnMZDBLG9hA7Hr9qfvdZDHjsak39YN98HXxoYLIqVm19Egku5YR3wyI7heVrOmPNEtmr-fIM1rtahudEdEAPM4HCiMrBmiA1Lw6SU8jc2rPLC7FK7nBCia_BGhG17NVHuQu0S7waA306jyKNhHwUnpsBQ'
dd_ses = dc_session.DrydockSession(host, token=token)
assert dd_ses.base_url == "http://%s/api/" % (host)
assert dd_ses.token == token
def test_session_init_marker():
host = 'foo.bar.baz'
marker = '5f1e08b6-38ec-4a99-9d0f-00d29c4e325b'
dd_ses = dc_session.DrydockSession(host, marker=marker)
assert dd_ses.base_url == "http://%s/api/" % (host)
assert dd_ses.marker == marker
@responses.activate
def test_session_get():
responses.add(responses.GET, 'http://foo.bar.baz/api/v1.0/test', body='okay',
status=200)
host = 'foo.bar.baz'
token = '5f1e08b6-38ec-4a99-9d0f-00d29c4e325b'
marker = '40c3eaf6-6a8a-11e7-a4bd-080027ef795a'
dd_ses = dc_session.DrydockSession(host, token=token, marker=marker)
resp = dd_ses.get('v1.0/test')
req = resp.request
assert req.headers.get('X-Auth-Token', None) == token
assert req.headers.get('X-Context-Marker', None) == marker
@responses.activate
def test_client_designs_get():
design_id = '828e88dc-6a8b-11e7-97ae-080027ef795a'
responses.add(responses.GET, 'http://foo.bar.baz/api/v1.0/designs',
json=[design_id], status=200)
host = 'foo.bar.baz'
token = '5f1e08b6-38ec-4a99-9d0f-00d29c4e325b'
dd_ses = dc_session.DrydockSession(host, token=token)
dd_client = dc_client.DrydockClient(dd_ses)
design_list = dd_client.get_design_ids()
assert design_id in design_list
@responses.activate
def test_client_design_get():
design = { 'id': '828e88dc-6a8b-11e7-97ae-080027ef795a',
'model_type': 'SiteDesign'
}
responses.add(responses.GET, 'http://foo.bar.baz/api/v1.0/designs/828e88dc-6a8b-11e7-97ae-080027ef795a',
json=design, status=200)
host = 'foo.bar.baz'
dd_ses = dc_session.DrydockSession(host)
dd_client = dc_client.DrydockClient(dd_ses)
design_resp = dd_client.get_design('828e88dc-6a8b-11e7-97ae-080027ef795a')
assert design_resp['id'] == design['id']
assert design_resp['model_type'] == design['model_type']
@responses.activate
def test_client_task_get():
task = {'action': 'deploy_node',
'result': 'success',
'parent_task': '444a1a40-7b5b-4b80-8265-cadbb783fa82',
'subtasks': [],
'status': 'complete',
'result_detail': {
'detail': ['Node cab23-r720-17 deployed']
},
'site_name': 'mec_demo',
'task_id': '1476902c-758b-49c0-b618-79ff3fd15166',
'node_list': ['cab23-r720-17'],
'design_id': 'fcf37ba1-4cde-48e5-a713-57439fc6e526'}
host = 'foo.bar.baz'
responses.add(responses.GET, "http://%s/api/v1.0/tasks/1476902c-758b-49c0-b618-79ff3fd15166" % (host),
json=task, status=200)
dd_ses = dc_session.DrydockSession(host)
dd_client = dc_client.DrydockClient(dd_ses)
task_resp = dd_client.get_task('1476902c-758b-49c0-b618-79ff3fd15166')
assert task_resp['status'] == task['status']

View File

@ -3,8 +3,8 @@ envlist = py35
[testenv]
deps=
-rrequirements.txt
-rtestrequirements.txt
-rrequirements-direct.txt
-rrequirements-test.txt
setenv=
PYTHONWARNING=all
commands=
@ -12,4 +12,4 @@ commands=
{posargs}
[flake8]
ignore=E302,H306
ignore=E302,H306