Merge pull request #56 from sh8121att/feature/dryd9/drydock_cli
DRDY 9 - CLI and REST client
This commit is contained in:
commit
0b5b62f215
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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")
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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())
|
|
@ -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)
|
|
@ -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())
|
|
@ -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)
|
|
@ -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())
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Binary file not shown.
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -346,4 +346,4 @@ spec:
|
|||
address: 'dhcp'
|
||||
- network: 'mgmt'
|
||||
address: '172.16.1.83'
|
||||
---
|
||||
...
|
|
@ -8,3 +8,4 @@ oauthlib
|
|||
uwsgi>1.4
|
||||
bson===0.4.7
|
||||
oslo.config
|
||||
click===6.7
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
pytest-mock
|
||||
pytest
|
||||
responses
|
||||
mock
|
||||
tox
|
||||
oslo.versionedobjects[fixtures]>=1.23.0
|
8
setup.py
8
setup.py
|
@ -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'
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
pytest-mock
|
||||
pytest
|
||||
mock
|
||||
tox
|
||||
oslo.versionedobjects[fixtures]>=1.23.0
|
|
@ -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']
|
Loading…
Reference in New Issue