From 5252c2c941bda6e8017c807725d4a75bf3ec2ee6 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Fri, 7 Jul 2017 12:43:35 -0500 Subject: [PATCH 01/26] WIP Continue developing API client --- drydock_provisioner/drydock_client/client.py | 8 ++++++++ drydock_provisioner/error.py | 13 +++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/drydock_provisioner/drydock_client/client.py b/drydock_provisioner/drydock_client/client.py index d0ddce3b..a5cda6d0 100644 --- a/drydock_provisioner/drydock_client/client.py +++ b/drydock_provisioner/drydock_client/client.py @@ -69,6 +69,14 @@ class DrydockClient(object): :return: A list of string design_ids """ + url = '/designs' + + resp = send_get(url) + + if resp.status_code != 200: + + else: + return resp.json() def get_design(self, design_id): diff --git a/drydock_provisioner/error.py b/drydock_provisioner/error.py index 04a38196..f146787a 100644 --- a/drydock_provisioner/error.py +++ b/drydock_provisioner/error.py @@ -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,15 @@ 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 \ No newline at end of file + def __init__(self, msg, code=400): + super(InvalidFormat, self).__init__(msg, code=code) From 9c0873a58728a3dff1286084bf49ea1b8d590160 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Mon, 10 Jul 2017 20:35:13 -0500 Subject: [PATCH 02/26] Remove sitename from task context Initially tasks were implemented with the possibility a design may have multiple sites in it and the task would operate on only one of them. Now a site design can contain only a single site, so no need to include the sitename in the operating context --- drydock_provisioner/control/tasks.py | 11 ++++---- .../drivers/node/maasdriver/driver.py | 26 +++++------------ .../drivers/oob/manual_driver/driver.py | 5 ---- .../drivers/oob/pyghmi_driver/__init__.py | 8 +----- drydock_provisioner/objects/task.py | 16 ++--------- drydock_provisioner/orchestrator/__init__.py | 28 +++++-------------- 6 files changed, 23 insertions(+), 71 deletions(-) diff --git a/drydock_provisioner/control/tasks.py b/drydock_provisioner/control/tasks.py index fcca1eb9..595d2c71 100644 --- a/drydock_provisioner/control/tasks.py +++ b/drydock_provisioner/control/tasks.py @@ -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() diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index 67159f99..17082f96 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -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()) diff --git a/drydock_provisioner/drivers/oob/manual_driver/driver.py b/drydock_provisioner/drivers/oob/manual_driver/driver.py index 9d513144..e89dc428 100644 --- a/drydock_provisioner/drivers/oob/manual_driver/driver.py +++ b/drydock_provisioner/drivers/oob/manual_driver/driver.py @@ -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) diff --git a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py index 7456d115..402ec7a3 100644 --- a/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py +++ b/drydock_provisioner/drivers/oob/pyghmi_driver/__init__.py @@ -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, diff --git a/drydock_provisioner/objects/task.py b/drydock_provisioner/objects/task.py index 6da718c8..293c8b56 100644 --- a/drydock_provisioner/objects/task.py +++ b/drydock_provisioner/objects/task.py @@ -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 \ No newline at end of file + return _dict diff --git a/drydock_provisioner/orchestrator/__init__.py b/drydock_provisioner/orchestrator/__init__.py index 645108e1..f16f2aef 100644 --- a/drydock_provisioner/orchestrator/__init__.py +++ b/drydock_provisioner/orchestrator/__init__.py @@ -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()) From 282349680f30029c97c48163fa49183a9df90701 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Mon, 10 Jul 2017 20:37:01 -0500 Subject: [PATCH 03/26] Implement low level API client Implement client object for low level API calls --- drydock_provisioner/drydock_client/client.py | 178 ++++++++++++++++--- 1 file changed, 154 insertions(+), 24 deletions(-) diff --git a/drydock_provisioner/drydock_client/client.py b/drydock_provisioner/drydock_client/client.py index a5cda6d0..ca38154c 100644 --- a/drydock_provisioner/drydock_client/client.py +++ b/drydock_provisioner/drydock_client/client.py @@ -14,42 +14,39 @@ 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 + :param string version: Drydock API version to use, default is '1.0' """ - def __init__(self, host=None, port=9000, version='1.0', token=None, marker=None): + def __init__(self, session, version='1.0'): + self.session = session self.version = version - self.session = DrydockSession(token=token, ext_marker=marker) - self.base_url = "http://%s:%d/api/%s/" % (host, port, version) + self.api_url = "%s/%s/" % (self.session.base_url, version) - def send_get(self, api_url, query=None): + def __send_get(self, endpoint, query=None): """ Send a GET request to Drydock. - :param string api_url: The URL string following the hostname and API prefix + :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 = requests.get(self.base_url + api_url, params=query) + resp = self.session.get(self.api_url + endpoint, params=query) return resp - def send_post(self, api_url, query=None, body=None, data=None): + def __send_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 api_url: The URL string following the hostname and API prefix + :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 @@ -57,40 +54,173 @@ class DrydockClient(object): """ if body is not None: - resp = requests.post(self.base_url + api_url, params=query, data=body) + resp = self.session.post(self.api_url + endpoint, params=query, data=body) else: - resp = requests.post(self.base_url + api_url, params=query, json=data) + resp = self.session.post(self.api_url + endpoint, 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 """ - url = '/designs' + endpoint = '/designs' - resp = send_get(url) + resp = self.__send_get(endpiont) 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_design(self, design_id): + def get_design(self, design_id, source='designed'): + """ + Get a full design based on the passed design_id - def create_design(self): + :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 = "/designs/%s" % design_id - def get_parts(self, design_id): - def get_part(self, design_id, kind, key): + resp = self.__send_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 = '/designs' + + if base_design is not None: + resp = self.__send_post(endpoint, data={'base_design_id': base_design}) + else: + resp = self.__send_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 = "/designs/%s/parts/%s/%s" % (design_id, kind, key) + + resp = self.__send_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) 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 = "/designs/%s/parts" % (design_id) + + resp = self.__send_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 = "/tasks" + + resp = self.__send_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 = "/tasks/%s" % (task_id) + + resp = self.__send_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 = '/tasks' + + task_dict = { + 'action': task_action, + 'design_id': design_id, + 'node_filter': node_filter + } + + resp = self.__send_post(endpoint, data=task_dict) + + if resp.status_code == 201: + return resp.json().get('id') + elif resp.status_code == 400: + raise errors.ClientError("Invalid inputs, received a %d: %s" % (resp.status_code, resp.text), + code=resp.status_code) + + From 397e9e97694c9b2dda301e16056eecce6b2c6042 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Mon, 10 Jul 2017 20:37:38 -0500 Subject: [PATCH 04/26] Update client session to use requests.session DrydockSession will handle the base Drydock URL and authentication. Also use requests.session to more efficiently use TCP connection pooling --- drydock_provisioner/drydock_client/session.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/drydock_provisioner/drydock_client/session.py b/drydock_provisioner/drydock_client/session.py index a93c2351..69a46d41 100644 --- a/drydock_provisioner/drydock_client/session.py +++ b/drydock_provisioner/drydock_client/session.py @@ -11,16 +11,25 @@ # 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: The service port, defaults to 9000 :param string token: Auth token :param string marker: (optional) external context marker """ - def __init__(self, token=None, marker=None): + def __init__(self, host=None, port=9000, token=None, marker=None): + self.__session = requests.Session() + self.__session.headers.update({'X-Auth-Token': token, 'X-Context-Marker': marker}) + self.host = host + self.port = port + self.base_url = "http://%s:%d/api/" % (host, port) self.token = token self.marker = marker + # TODO Add keystone authentication to produce a token for this session From 3f2f069e1c22ee88afb67ef68164046222a009e3 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Mon, 10 Jul 2017 20:39:07 -0500 Subject: [PATCH 05/26] Create a error class for the API client Use a custom exception class for the API client including the HTTP status code that API calls return --- drydock_provisioner/error.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/drydock_provisioner/error.py b/drydock_provisioner/error.py index f146787a..8f0076fd 100644 --- a/drydock_provisioner/error.py +++ b/drydock_provisioner/error.py @@ -44,9 +44,16 @@ class ApiError(Exception): self.status_code = code def to_json(self): - err_dict = {'error': msg, 'type': self.__class__.__name__}} + err_dict = {'error': msg, 'type': self.__class__.__name__} return json.dumps(err_dict) class InvalidFormat(ApiError): 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 + From fb0ba7fb23b093a4272894cace008f7ee0abcd98 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Mon, 10 Jul 2017 20:39:49 -0500 Subject: [PATCH 06/26] Cleanup swap file --- .../drydock_client/.client.py.swp | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 drydock_provisioner/drydock_client/.client.py.swp diff --git a/drydock_provisioner/drydock_client/.client.py.swp b/drydock_provisioner/drydock_client/.client.py.swp deleted file mode 100644 index 9889eefc9c4cba2d436b77617dc5dd2d7f97d22a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeHN&yO2P6|Sr{Ss)3Z<+Pk$9EHp%j_nC60!9Wo$&eVr%xLT+8w6P8?y_Aw?e3;DlId5fXwE5)yo`y4vGRyg|wZ1-hiq z)!p^#y;t9R^{PkHgU0R7m+5ltd4cv(A^vjkmAj|DdqVu?IU&sW(;JsIc;L}3m%Dgd zXLc&1E6Kt8h`*c*)1uwHV>0vuju<}mh5YzlUQYoXF;S@Z0mVfOXu&S zzkFOWP%`i+22P7lUfO7|#M#Oj`q;C#9wl6kl?;>&lnj&%lnj&%lnj&%lnj&%{EsnU z(_`Wltm!Ggt`~fN>`3o#%dTXgWT0fAWT0fAWT0fAWT0fAWT0fAWT0fAWZ(&8K!!s6 z6o33To`&=I|9|oK|1Ulw#LoZ&G=LMp)4*FF7UH+SJ>WX<&(lKu9QYpaZD0-@06m}! zd1)&3ET(13A_q?4frbXdEj%v$ARa7KZD2jfS&+A27Um1AD99Azy{#Begbo) zbou|70YVGaQ23q#e7$2;-%7K$W>hq+V$|qS+}B~1_-S7T7N7A{DNaJwAL_`ethI&j zde)oRW@ykxBW0IWxOqBUC1ISE<&h|nVLz3b&4(UvU@j+7A72>hc(l?cPkY$yJsrdO zJvCo8!!EA0vMl7;$SHWpQ&zDwwB%`_gZZdbDuR1%%}m*87B{I)kv5i+A?2$%WZC2_ zvh;Oj7h>1)@2QsMScF?)pddbX=|N&yF1L-&q=z>00h3Y-1K*e`V@)khOwM9$AdTuz zvxqK`Of_Fs38cx1X+budSLsEQ#H~I+T z^0#(x77h=SC`x8b5-S=bYhpRU0^*R`I~__hHPrZM5bJi$#mrH)%vVkLVd^SOl(e(m z?Jd}YlRoon)Zwt(aUOd%!l5FWDKe=F^iZp?x=>bR0dEQe#r;K6R}WL!t=$!ps?+K( zUCJ$&H1=@i?m3#+s0gk~{Kt)Rn9PNvY~yj#^-&?$|HWwgXkOo38BD>;rEt&e0lYChAs_;ddW90LRxM|4xljhM%HF z`E2Xv57un5r{b3Tt&%#h{fW%>aNyE*E98~+`#RRP-}h$|JLGeXy=r}? z(P%7OYn|VyHO}L(b*4EDtK_XN=VU>gFI#@A=7LRHOHw|iWwcJ42iWzv@=AE$>o|y} zp+eolb}9;^bM`@P&IOc`DfV}4bs$-1?ii~21otGNvJ9xmq374>~I%h$QCw zp{wre%&N2y~TNezb7cSGa&88IPuNq-d0Y+n9@Qsl)G;SpZMe0SY$Lml2%C;zM>Q#A`~X2U9+D z9`cw^;AH_M2hPVzyGxyJg|4)_oo*F6Zp9HRW=?blJ%c$)Ff0Ue5Gkx^#x~_BQy6pE zNwZ8_EUii=8QK|2yf_Oz-25h=&1N;3%3!Q&Nj9oO7c*$;H#=81x4N4bFVz}u%H3p& zyr1K&w^GA93X&9e!v{!~7Ru*dlHVx!7{=*u2(Pya!~`m}*ff~T#K<`CTjxmdd>ogF zYC9i`2v_NLUVaGv(Hc*$_(_%Z&pL;%!oN(9#+EKMHZD`U_lX{LVymJE?<)8VpLVhY Z0k`uSQ5$a^I1mv#H-j0*yU9KW?%x)WCb<9r From 8737b8ef4c5de6afd0e54ab3972b2c3f8bf4f527 Mon Sep 17 00:00:00 2001 From: Bryan Strassner Date: Wed, 12 Jul 2017 17:41:50 -0500 Subject: [PATCH 07/26] Establish pattern for click cli interface commands Set up the pattern of having command groups in packages under the cli package. Added lambdas to retrieve environment values as default values for --url and --token. Triggered messages other than stack dump to user of cli if the values are not resolved. --- drydock_provisioner/cli/commands.py | 41 +++++++++++----------- drydock_provisioner/cli/design/commands.py | 31 ++++++++++++++++ setup.py | 5 ++- 3 files changed, 55 insertions(+), 22 deletions(-) create mode 100644 drydock_provisioner/cli/design/commands.py diff --git a/drydock_provisioner/cli/commands.py b/drydock_provisioner/cli/commands.py index 482071f1..711a060c 100644 --- a/drydock_provisioner/cli/commands.py +++ b/drydock_provisioner/cli/commands.py @@ -15,35 +15,34 @@ import os import click +from .design import commands as design + @click.group() @click.option('--debug/--no-debug', default=False) -@click.option('--token') -@click.option('--url') +@click.option('--token', default=lambda: os.environ.get('DD_TOKEN', None)) +@click.option('--url' , default=lambda: os.environ.get('DD_URL', None)) @click.pass_context def drydock(ctx, debug, token, url): + """ Base cli command. Peforms validations and sets default values. + """ + if not ctx.obj: + ctx.obj = {} + ctx.obj['DEBUG'] = debug + option_validation_error = False + if not token: - ctx.obj['TOKEN'] = os.environ['DD_TOKEN'] - else: - ctx.obj['TOKEN'] = token + click.echo("Error: Token must be specified either by " + "--token or DD_TOKEN from the environment") + option_validation_error = True if not url: - ctx.obj['URL'] = os.environ['DD_URL'] - else: - ctx.obj['URL'] = url + click.echo("Error: URL must be specified either by " + "--url or DD_URL from the environment") + option_validation_error = True -@drydock.group() -def create(): - pass + if option_validation_error: + ctx.exit() -@drydock.group() -def list() - pass - -@drydock.group() -def show(): - pass - -@create.command() -def design \ No newline at end of file +drydock.add_command(design.design) diff --git a/drydock_provisioner/cli/design/commands.py b/drydock_provisioner/cli/design/commands.py new file mode 100644 index 00000000..9a79256e --- /dev/null +++ b/drydock_provisioner/cli/design/commands.py @@ -0,0 +1,31 @@ +# 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 click + +@click.group() +def design(): + click.echo('design invoked') + +@design.command() +def create(): + click.echo('create invoked') + +@design.command() +def list(): + click.echo('list invoked') + +@design.command() +def show(): + click.echo('design invoked.') diff --git a/setup.py b/setup.py index 88bef9fc..f4f67adc 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,9 @@ 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'], install_requires=[ 'PyYAML', 'pyghmi>=1.0.18', @@ -55,6 +57,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' } ) From a73d5d917de7bc3d78625a4658b8af0671b3e4aa Mon Sep 17 00:00:00 2001 From: Bryan Strassner Date: Fri, 14 Jul 2017 12:34:06 -0500 Subject: [PATCH 08/26] Add logging configuration for cli. Begin action approach Actions are not yet working as desired. This commit also contains some small amount of api refactor for setup of port. --- drydock_provisioner/cli/action.py | 31 +++++++++++ drydock_provisioner/cli/commands.py | 51 +++++++++++++------ drydock_provisioner/cli/design/actions.py | 0 drydock_provisioner/cli/design/commands.py | 43 ++++++++++++---- drydock_provisioner/drydock_client/client.py | 2 +- drydock_provisioner/drydock_client/session.py | 11 ++-- setup.py | 3 +- 7 files changed, 109 insertions(+), 32 deletions(-) create mode 100644 drydock_provisioner/cli/action.py create mode 100644 drydock_provisioner/cli/design/actions.py diff --git a/drydock_provisioner/cli/action.py b/drydock_provisioner/cli/action.py new file mode 100644 index 00000000..c82b5df7 --- /dev/null +++ b/drydock_provisioner/cli/action.py @@ -0,0 +1,31 @@ +# 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 + +from oslo_config import cfg + +class CliAction(object): + """ Action base for CliActions + """ + def __init__(self, api_client, debug): + self.logger = logging.getLogger(cfg.CONF.logging.control_logger_name) + self.api_client = api_client + self.debug = debug + if self.debug: + self.logger.info("Action initialized with client %s" % (self.api_client), extra=extra) + + def invoke(self): + raise NotImplementedError("Invoke method has not been implemented") diff --git a/drydock_provisioner/cli/commands.py b/drydock_provisioner/cli/commands.py index 711a060c..7448436b 100644 --- a/drydock_provisioner/cli/commands.py +++ b/drydock_provisioner/cli/commands.py @@ -11,38 +11,59 @@ # 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 entrypoint for the cli commands +""" import os - +import logging import click +from drydock_provisioner.drydock_client.session import DrydockSession +from drydock_provisioner.drydock_client.client import DrydockClient from .design import commands as design @click.group() -@click.option('--debug/--no-debug', default=False) -@click.option('--token', default=lambda: os.environ.get('DD_TOKEN', None)) -@click.option('--url' , default=lambda: os.environ.get('DD_URL', None)) +@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): - """ Base cli command. Peforms validations and sets default values. + """ Drydock CLI to invoke the running instance of the drydock API """ if not ctx.obj: ctx.obj = {} ctx.obj['DEBUG'] = debug - option_validation_error = False - if not token: - click.echo("Error: Token must be specified either by " - "--token or DD_TOKEN from the environment") - option_validation_error = True + ctx.fail("Error: Token must be specified either by " + "--token or DD_TOKEN from the environment") if not url: - click.echo("Error: URL must be specified either by " - "--url or DD_URL from the environment") - option_validation_error = True + ctx.fail("Error: URL must be specified either by " + "--url or DD_URL from the environment") - if option_validation_error: - ctx.exit() + # setup logging for the CLI + # Setup root logger + logger = logging.getLogger('drydock_cli') + + 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') + + # setup the drydock client using the passed parameters. + ctx.obj['CLIENT'] = DrydockClient(DrydockSession(host=url, + token=token)) drydock.add_command(design.design) diff --git a/drydock_provisioner/cli/design/actions.py b/drydock_provisioner/cli/design/actions.py new file mode 100644 index 00000000..e69de29b diff --git a/drydock_provisioner/cli/design/commands.py b/drydock_provisioner/cli/design/commands.py index 9a79256e..09dbbb85 100644 --- a/drydock_provisioner/cli/design/commands.py +++ b/drydock_provisioner/cli/design/commands.py @@ -11,21 +11,42 @@ # 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 logging import click @click.group() -def design(): - click.echo('design invoked') +@click.pass_context +def design(ctx): + """ Drydock design commands + """ + pass -@design.command() -def create(): +@design.command(name='create') +@click.pass_context +def design_create(ctx): + """ Create a design + """ click.echo('create invoked') -@design.command() -def list(): - click.echo('list invoked') +@design.command(name='list') +@click.pass_context +def design_list(ctx): + """ List designs + """ + click.echo(ctx.obj['CLIENT'].get_design_ids()) -@design.command() -def show(): - click.echo('design invoked.') +@click.option('--design-id', + '-id', + help='The deisgn id to show') +@design.command(name='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('show invoked for {}'.format(design_id)) diff --git a/drydock_provisioner/drydock_client/client.py b/drydock_provisioner/drydock_client/client.py index ca38154c..51941f22 100644 --- a/drydock_provisioner/drydock_client/client.py +++ b/drydock_provisioner/drydock_client/client.py @@ -68,7 +68,7 @@ class DrydockClient(object): """ endpoint = '/designs' - resp = self.__send_get(endpiont) + resp = self.__send_get(endpoint) if resp.status_code != 200: raise errors.ClientError("Received a %d from GET URL: %s" % (resp.status_code, endpoint), diff --git a/drydock_provisioner/drydock_client/session.py b/drydock_provisioner/drydock_client/session.py index 69a46d41..7bc5cea4 100644 --- a/drydock_provisioner/drydock_client/session.py +++ b/drydock_provisioner/drydock_client/session.py @@ -18,17 +18,20 @@ 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: The service port, defaults to 9000 + :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, host=None, port=9000, token=None, marker=None): + def __init__(self, host, *, port=None, token=None, marker=None): self.__session = requests.Session() - self.__session.headers.update({'X-Auth-Token': token, 'X-Context-Marker': marker}) + self.__session.headers.update({'X-Auth-Token': token, 'X-Context-Marker': marker}) self.host = host self.port = port - self.base_url = "http://%s:%d/api/" % (host, port) + if port: + self.base_url = "http://%s:%d/api/" % (host, port) + else: + self.base_url = "http://%s/api/" % (host) self.token = token self.marker = marker diff --git a/setup.py b/setup.py index f4f67adc..9b49a6a3 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,8 @@ setup(name='drydock_provisioner', 'drydock_provisioner.drivers.node.maasdriver.models', 'drydock_provisioner.control', 'drydock_provisioner.cli', - 'drydock_provisioner.cli.design'], + 'drydock_provisioner.cli.design', + 'drydock_provisioner.drydock_client'], install_requires=[ 'PyYAML', 'pyghmi>=1.0.18', From 07f2afc1124adaf96a74a264e341cb23e2dc1070 Mon Sep 17 00:00:00 2001 From: Bryan Strassner Date: Fri, 14 Jul 2017 16:13:53 -0500 Subject: [PATCH 09/26] Completion of wiring of design cli commands added the create and show commands and wired to specific api client calls --- drydock_provisioner/cli/action.py | 14 +++--- drydock_provisioner/cli/design/actions.py | 55 ++++++++++++++++++++++ drydock_provisioner/cli/design/commands.py | 25 ++++++---- 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/drydock_provisioner/cli/action.py b/drydock_provisioner/cli/action.py index c82b5df7..85d947ef 100644 --- a/drydock_provisioner/cli/action.py +++ b/drydock_provisioner/cli/action.py @@ -15,17 +15,15 @@ """ import logging -from oslo_config import cfg - -class CliAction(object): +class CliAction: # pylint: disable=too-few-public-methods """ Action base for CliActions """ - def __init__(self, api_client, debug): - self.logger = logging.getLogger(cfg.CONF.logging.control_logger_name) + def __init__(self, api_client): + self.logger = logging.getLogger('drydock_cli') self.api_client = api_client - self.debug = debug - if self.debug: - self.logger.info("Action initialized with client %s" % (self.api_client), extra=extra) + 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") diff --git a/drydock_provisioner/cli/design/actions.py b/drydock_provisioner/cli/design/actions.py index e69de29b..224b8d69 100644 --- a/drydock_provisioner/cli/design/actions.py +++ b/drydock_provisioner/cli/design/actions.py @@ -0,0 +1,55 @@ +# 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 + :param string base_design: A UUID of the base design to model after + """ + def __init__(self, api_client, base_design=None): + 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) diff --git a/drydock_provisioner/cli/design/commands.py b/drydock_provisioner/cli/design/commands.py index 09dbbb85..4e51a360 100644 --- a/drydock_provisioner/cli/design/commands.py +++ b/drydock_provisioner/cli/design/commands.py @@ -14,34 +14,39 @@ """ cli.design.commands Contains commands related to designs """ -import logging 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() -@click.pass_context -def design(ctx): +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): +def design_create(ctx, base_design=None): """ Create a design """ - click.echo('create invoked') + click.echo(DesignCreate(ctx.obj['CLIENT'], base_design).invoke()) @design.command(name='list') @click.pass_context def design_list(ctx): """ List designs """ - click.echo(ctx.obj['CLIENT'].get_design_ids()) + click.echo(DesignList(ctx.obj['CLIENT']).invoke()) -@click.option('--design-id', - '-id', - help='The deisgn id to show') @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 @@ -49,4 +54,4 @@ def design_show(ctx, design_id): if not design_id: ctx.fail('The design id must be specified by --design-id') - click.echo('show invoked for {}'.format(design_id)) + click.echo(DesignShow(ctx.obj['CLIENT'], design_id).invoke()) From 459ddf102321722dc2de961554cfbe741215965b Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Fri, 14 Jul 2017 17:20:14 -0500 Subject: [PATCH 10/26] Update tox with new requirements Change requirements file naming to be similar to the requirements-direct.txt name and update tox.ini with the new name --- requirements-test.txt | 6 ++++++ testrequirements.txt | 5 ----- tox.ini | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 requirements-test.txt delete mode 100644 testrequirements.txt diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 00000000..93c2abb3 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,6 @@ +pytest-mock +pytest +requests-mock +mock +tox +oslo.versionedobjects[fixtures]>=1.23.0 diff --git a/testrequirements.txt b/testrequirements.txt deleted file mode 100644 index 16222260..00000000 --- a/testrequirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -pytest-mock -pytest -mock -tox -oslo.versionedobjects[fixtures]>=1.23.0 \ No newline at end of file diff --git a/tox.ini b/tox.ini index c92d6f00..8b8d3061 100644 --- a/tox.ini +++ b/tox.ini @@ -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 \ No newline at end of file +ignore=E302,H306 From f2143aef3ff87fabd0e299687db96225a7f29fd8 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Fri, 14 Jul 2017 17:21:12 -0500 Subject: [PATCH 11/26] Unit tests for drydock_client Create unit tests for drydock_client.session --- tests/unit/test_drydock_client.py | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/unit/test_drydock_client.py diff --git a/tests/unit/test_drydock_client.py b/tests/unit/test_drydock_client.py new file mode 100644 index 00000000..a8e52c21 --- /dev/null +++ b/tests/unit/test_drydock_client.py @@ -0,0 +1,64 @@ +# 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 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 + From 71d7607d5f97edb4d7ac1a34129f8da5b5a20f9d Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Sun, 16 Jul 2017 20:41:45 -0500 Subject: [PATCH 12/26] Manage API version by endpoint --- drydock_provisioner/drydock_client/client.py | 71 +++++--------------- 1 file changed, 18 insertions(+), 53 deletions(-) diff --git a/drydock_provisioner/drydock_client/client.py b/drydock_provisioner/drydock_client/client.py index 51941f22..17cb87f0 100644 --- a/drydock_provisioner/drydock_client/client.py +++ b/drydock_provisioner/drydock_client/client.py @@ -21,44 +21,10 @@ class DrydockClient(object): A client for the Drydock API :param DrydockSession session: A instance of DrydockSession to be used by this client - :param string version: Drydock API version to use, default is '1.0' """ - def __init__(self, session, version='1.0'): + def __init__(self, session): self.session = session - self.version = version - self.api_url = "%s/%s/" % (self.session.base_url, version) - - def __send_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.api_url + endpoint, params=query) - - return resp - - def __send_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.api_url + endpoint, params=query, data=body) - else: - resp = self.session.post(self.api_url + endpoint, params=query, json=data) - - return resp def get_design_ids(self): """ @@ -66,9 +32,9 @@ class DrydockClient(object): :return: A list of string design_ids """ - endpoint = '/designs' + endpoint = 'v1.0/designs' - resp = self.__send_get(endpoint) + resp = self.session.get(endpoint) if resp.status_code != 200: raise errors.ClientError("Received a %d from GET URL: %s" % (resp.status_code, endpoint), @@ -84,10 +50,10 @@ class DrydockClient(object): :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 = "/designs/%s" % design_id + endpoint = "v1.0/designs/%s" % design_id - resp = self.__send_get(endpoint, query={'source': source}) + 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) @@ -104,12 +70,12 @@ class DrydockClient(object): :param string base_design: String UUID of the base design to build this design upon :return string: String UUID of the design ID """ - endpoint = '/designs' + endpoint = 'v1.0/designs' if base_design is not None: - resp = self.__send_post(endpoint, data={'base_design_id': base_design}) + resp = self.session.post(endpoint, data={'base_design_id': base_design}) else: - resp = self.__send_post(endpoint) + resp = self.session.post(endpoint) if resp.status_code != 201: raise errors.ClientError("Received a %d from POST URL: %s" % (resp.status_code, endpoint), @@ -129,9 +95,9 @@ class DrydockClient(object): :return: A dict of the design part """ - endpoint = "/designs/%s/parts/%s/%s" % (design_id, kind, key) + endpoint = "v1.0/designs/%s/parts/%s/%s" % (design_id, kind, key) - resp = self.__send_get(endpoint, query={'source': source}) + 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) @@ -148,9 +114,9 @@ class DrydockClient(object): :return: Dict of the parsed design parts """ - endpoint = "/designs/%s/parts" % (design_id) + endpoint = "v1.0/designs/%s/parts" % (design_id) - resp = self.__send_post(endpoint, query={'ingester': 'yaml'}, body=yaml_string) + 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) @@ -168,9 +134,9 @@ class DrydockClient(object): :return: List of string uuid task IDs """ - endpoint = "/tasks" + endpoint = "v1.0/tasks" - resp = self.__send_get(endpoint) + resp = self.session.get(endpoint) if resp.status_code != 200: raise errors.ClientError("Server error: %s" % resp.text, code=resp.status_code) @@ -185,9 +151,9 @@ class DrydockClient(object): :return: A dict representing the current state of the task """ - endpoint = "/tasks/%s" % (task_id) + endpoint = "v1.0/tasks/%s" % (task_id) - resp = self.__send_get(endpoint) + resp = self.session.get(endpoint) if resp.status_code == 200: return resp.json() @@ -207,7 +173,7 @@ class DrydockClient(object): :return: The string uuid of the create task's id """ - endpoint = '/tasks' + endpoint = 'v1.0/tasks' task_dict = { 'action': task_action, @@ -215,7 +181,7 @@ class DrydockClient(object): 'node_filter': node_filter } - resp = self.__send_post(endpoint, data=task_dict) + resp = self.session.post(endpoint, data=task_dict) if resp.status_code == 201: return resp.json().get('id') @@ -223,4 +189,3 @@ class DrydockClient(object): raise errors.ClientError("Invalid inputs, received a %d: %s" % (resp.status_code, resp.text), code=resp.status_code) - From 9fc8fe270d31164f90df54eee14ade707bc67255 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Sun, 16 Jul 2017 20:42:11 -0500 Subject: [PATCH 13/26] Move request logic to DrydockSession --- drydock_provisioner/drydock_client/session.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/drydock_provisioner/drydock_client/session.py b/drydock_provisioner/drydock_client/session.py index 7bc5cea4..174a6670 100644 --- a/drydock_provisioner/drydock_client/session.py +++ b/drydock_provisioner/drydock_client/session.py @@ -36,3 +36,34 @@ class DrydockSession(object): 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 + From 7eddaf13a7f68097217f47602fab47681ef1d3ce Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Sun, 16 Jul 2017 20:43:07 -0500 Subject: [PATCH 14/26] Add DrydockClient unit tests --- requirements-test.txt | 2 +- tests/unit/test_drydock_client.py | 51 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 93c2abb3..a7a17ab5 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,6 +1,6 @@ pytest-mock pytest -requests-mock +responses mock tox oslo.versionedobjects[fixtures]>=1.23.0 diff --git a/tests/unit/test_drydock_client.py b/tests/unit/test_drydock_client.py index a8e52c21..fc6e01c8 100644 --- a/tests/unit/test_drydock_client.py +++ b/tests/unit/test_drydock_client.py @@ -12,6 +12,7 @@ # 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 @@ -62,3 +63,53 @@ def test_session_init_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]) + + 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) + + 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'] + From c09b86c2135f0e9f9d547177a8c35e8f7e314552 Mon Sep 17 00:00:00 2001 From: Bryan Strassner Date: Mon, 17 Jul 2017 17:45:02 -0500 Subject: [PATCH 15/26] Add commands to support parts in the cli Updated the setup of the session for the api client to use more standard fields and make less assumptions. Moved url parsing to cli code --- drydock_provisioner/cli/commands.py | 22 +++-- drydock_provisioner/cli/part/actions.py | 87 +++++++++++++++++++ drydock_provisioner/cli/part/commands.py | 81 +++++++++++++++++ drydock_provisioner/drydock_client/session.py | 12 ++- setup.py | 1 + 5 files changed, 192 insertions(+), 11 deletions(-) create mode 100644 drydock_provisioner/cli/part/actions.py create mode 100644 drydock_provisioner/cli/part/commands.py diff --git a/drydock_provisioner/cli/commands.py b/drydock_provisioner/cli/commands.py index 7448436b..75e4c625 100644 --- a/drydock_provisioner/cli/commands.py +++ b/drydock_provisioner/cli/commands.py @@ -11,15 +11,17 @@ # 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 entrypoint for the cli commands +""" The entry point for the cli commands """ import os import logging -import click +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 @click.group() @click.option('--debug/--no-debug', @@ -43,12 +45,12 @@ def drydock(ctx, debug, token, url): ctx.obj['DEBUG'] = debug if not token: - ctx.fail("Error: Token must be specified either by " - "--token or DD_TOKEN from the environment") + ctx.fail('Error: Token must be specified either by ' + '--token or DD_TOKEN from the environment') if not url: - ctx.fail("Error: URL must be specified either by " - "--url or DD_URL from the environment") + ctx.fail('Error: URL must be specified either by ' + '--url or DD_URL from the environment') # setup logging for the CLI # Setup root logger @@ -63,7 +65,13 @@ def drydock(ctx, debug, token, url): logger.debug('logging for cli initialized') # setup the drydock client using the passed parameters. - ctx.obj['CLIENT'] = DrydockClient(DrydockSession(host=url, + 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)) drydock.add_command(design.design) +drydock.add_command(part.part) diff --git a/drydock_provisioner/cli/part/actions.py b/drydock_provisioner/cli/part/actions.py new file mode 100644 index 00000000..1a189e19 --- /dev/null +++ b/drydock_provisioner/cli/part/actions.py @@ -0,0 +1,87 @@ +# 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 self.api_client.get_design_ids() + pass + +class PartCreate(PartBase): # pylint: disable=too-few-public-methods + """ Action to create parts of a design + """ + def __init__(self, api_client, design_id, yaml): + """ + :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 yaml: The file containing the specification of the part + """ + super().__init__(api_client, design_id) + self.yaml = yaml + self.logger.debug('PartCreate action initialized with yaml=%s', yaml[:100]) + + def invoke(self): + return self.api_client.load_parts(self.design_id, self.yaml) + +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) diff --git a/drydock_provisioner/cli/part/commands.py b/drydock_provisioner/cli/part/commands.py new file mode 100644 index 00000000..86d3251b --- /dev/null +++ b/drydock_provisioner/cli/part/commands.py @@ -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.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 yaml validation could be done + click.echo(PartCreate(ctx.obj['CLIENT'], + design_id=ctx.obj['DESIGN_ID'], + yaml=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()) diff --git a/drydock_provisioner/drydock_client/session.py b/drydock_provisioner/drydock_client/session.py index 174a6670..28c3fe37 100644 --- a/drydock_provisioner/drydock_client/session.py +++ b/drydock_provisioner/drydock_client/session.py @@ -23,15 +23,19 @@ class DrydockSession(object): :param string marker: (optional) external context marker """ - def __init__(self, host, *, port=None, 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.port = port + self.scheme = scheme + if port: - self.base_url = "http://%s:%d/api/" % (host, port) + self.port = port + self.base_url = "%s://%s:%s/api/" % (self.scheme, self.host, self.port) else: - self.base_url = "http://%s/api/" % (host) + #assume default port for scheme + self.base_url = "%s://%s/api/" % (self.scheme, self.host) + self.token = token self.marker = marker diff --git a/setup.py b/setup.py index 9b49a6a3..227af94e 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ setup(name='drydock_provisioner', 'drydock_provisioner.control', 'drydock_provisioner.cli', 'drydock_provisioner.cli.design', + 'drydock_provisioner.cli.part', 'drydock_provisioner.drydock_client'], install_requires=[ 'PyYAML', From 32b2db4c8e689bd4853bc1134f417b76d74ab085 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Tue, 18 Jul 2017 13:35:56 -0500 Subject: [PATCH 16/26] Fix some docker image issues preventing startup --- Dockerfile | 3 ++- entrypoint.sh | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5579dadf..af6c70e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/entrypoint.sh b/entrypoint.sh index d6ceca38..c981906f 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -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" From 607c79687ca82bc003547bb97694db0769d302bb Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Tue, 18 Jul 2017 13:36:34 -0500 Subject: [PATCH 17/26] Fix API middleware issue causing exceptions Use correct attribute name for the request_id in the logging middleware --- drydock_provisioner/control/middleware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drydock_provisioner/control/middleware.py b/drydock_provisioner/control/middleware.py index acbdb27e..2ee9981c 100644 --- a/drydock_provisioner/control/middleware.py +++ b/drydock_provisioner/control/middleware.py @@ -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) From 8c69c9febcd373cc323f3d4b38845d2ba8839578 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Tue, 18 Jul 2017 13:37:21 -0500 Subject: [PATCH 18/26] Return results for the get part API client --- drydock_provisioner/drydock_client/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drydock_provisioner/drydock_client/client.py b/drydock_provisioner/drydock_client/client.py index 17cb87f0..63481b67 100644 --- a/drydock_provisioner/drydock_client/client.py +++ b/drydock_provisioner/drydock_client/client.py @@ -104,6 +104,8 @@ class DrydockClient(object): 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): """ From 4ead07c737329c0e29254e70453bff5694947f2d Mon Sep 17 00:00:00 2001 From: Bryan Strassner Date: Tue, 18 Jul 2017 14:32:10 -0500 Subject: [PATCH 19/26] Add task commands to CLI with basic functionality task create, task list, task show included with this commit. --- drydock_provisioner/cli/commands.py | 2 + drydock_provisioner/cli/design/actions.py | 4 +- drydock_provisioner/cli/part/commands.py | 8 +- drydock_provisioner/cli/task/actions.py | 84 ++++++++++++++++++++ drydock_provisioner/cli/task/commands.py | 81 +++++++++++++++++++ drydock_provisioner/drydock_client/client.py | 2 +- setup.py | 1 + 7 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 drydock_provisioner/cli/task/actions.py create mode 100644 drydock_provisioner/cli/task/commands.py diff --git a/drydock_provisioner/cli/commands.py b/drydock_provisioner/cli/commands.py index 75e4c625..85f79fbb 100644 --- a/drydock_provisioner/cli/commands.py +++ b/drydock_provisioner/cli/commands.py @@ -22,6 +22,7 @@ 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', @@ -75,3 +76,4 @@ def drydock(ctx, debug, token, url): drydock.add_command(design.design) drydock.add_command(part.part) +drydock.add_command(task.task) diff --git a/drydock_provisioner/cli/design/actions.py b/drydock_provisioner/cli/design/actions.py index 224b8d69..edf8e2d3 100644 --- a/drydock_provisioner/cli/design/actions.py +++ b/drydock_provisioner/cli/design/actions.py @@ -27,9 +27,11 @@ class DesignList(CliAction): # pylint: disable=too-few-public-methods class DesignCreate(CliAction): # pylint: disable=too-few-public-methods """ Action to create designs - :param string base_design: A UUID of the base design to model after """ 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 diff --git a/drydock_provisioner/cli/part/commands.py b/drydock_provisioner/cli/part/commands.py index 86d3251b..078c9ea2 100644 --- a/drydock_provisioner/cli/part/commands.py +++ b/drydock_provisioner/cli/part/commands.py @@ -74,8 +74,12 @@ def part_show(ctx, source, kind, key): """ 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()) + click.echo(PartShow(ctx.obj['CLIENT'], + design_id=ctx.obj['DESIGN_ID'], + kind=kind, + key=key, + source=source).invoke()) diff --git a/drydock_provisioner/cli/task/actions.py b/drydock_provisioner/cli/task/actions.py new file mode 100644 index 00000000..1fc3d437 --- /dev/null +++ b/drydock_provisioner/cli/task/actions.py @@ -0,0 +1,84 @@ +# 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): + """ + node_filter : {node_names: [], rack_names:[], node-tags[]} + :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) diff --git a/drydock_provisioner/cli/task/commands.py b/drydock_provisioner/cli/task/commands.py new file mode 100644 index 00000000..c9ad25e9 --- /dev/null +++ b/drydock_provisioner/cli/task/commands.py @@ -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()) diff --git a/drydock_provisioner/drydock_client/client.py b/drydock_provisioner/drydock_client/client.py index 63481b67..3fe12f5f 100644 --- a/drydock_provisioner/drydock_client/client.py +++ b/drydock_provisioner/drydock_client/client.py @@ -186,7 +186,7 @@ class DrydockClient(object): resp = self.session.post(endpoint, data=task_dict) if resp.status_code == 201: - return resp.json().get('id') + 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) diff --git a/setup.py b/setup.py index 227af94e..3e1fa69a 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ setup(name='drydock_provisioner', 'drydock_provisioner.cli', 'drydock_provisioner.cli.design', 'drydock_provisioner.cli.part', + 'drydock_provisioner.cli.task', 'drydock_provisioner.drydock_client'], install_requires=[ 'PyYAML', From 683d783ebbc5df13165229c1846dfbf7c6ef8c8e Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Tue, 18 Jul 2017 15:30:09 -0500 Subject: [PATCH 20/26] Add API endpoint for design part list GET /designs/{id}/parts provides a list of design parts that can be parsed an interrogated individually --- drydock_provisioner/control/designs.py | 28 +++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/drydock_provisioner/control/designs.py b/drydock_provisioner/control/designs.py index 23afc457..8ef76bc1 100644 --- a/drydock_provisioner/control/designs.py +++ b/drydock_provisioner/control/designs.py @@ -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': 'Netowrk', '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) \ No newline at end of file + self.return_error(resp, falcon.HTTP_404, message=str(dex), retry=False) From b82f1b080cb8d3520b61a496c95580ed10b527ef Mon Sep 17 00:00:00 2001 From: Bryan Strassner Date: Tue, 18 Jul 2017 17:05:05 -0500 Subject: [PATCH 21/26] Update unimplemented function with message Updated sample yaml to end with ... Updated references to yaml in the code for input file for load parts to instead be in_file since the input may not be yaml someday. --- drydock_provisioner/cli/part/actions.py | 13 ++++++------- drydock_provisioner/cli/part/commands.py | 4 ++-- examples/designparts_v1.0.yaml | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/drydock_provisioner/cli/part/actions.py b/drydock_provisioner/cli/part/actions.py index 1a189e19..c1910818 100644 --- a/drydock_provisioner/cli/part/actions.py +++ b/drydock_provisioner/cli/part/actions.py @@ -38,24 +38,23 @@ class PartList(PartBase): # pylint: disable=too-few-public-methods def invoke(self): #TODO: change the api call - #return self.api_client.get_design_ids() - pass + 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, yaml): + 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 yaml: The file containing the specification of the part + :param in_file: The file containing the specification of the part """ super().__init__(api_client, design_id) - self.yaml = yaml - self.logger.debug('PartCreate action initialized with yaml=%s', yaml[:100]) + 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.yaml) + 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. diff --git a/drydock_provisioner/cli/part/commands.py b/drydock_provisioner/cli/part/commands.py index 078c9ea2..5846d3ff 100644 --- a/drydock_provisioner/cli/part/commands.py +++ b/drydock_provisioner/cli/part/commands.py @@ -46,10 +46,10 @@ def part_create(ctx, file=None): with open(file, 'r') as file_input: file_contents = file_input.read() - # here is where some yaml validation could be done + # 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'], - yaml=file_contents).invoke()) + in_file=file_contents).invoke()) @part.command(name='list') @click.pass_context diff --git a/examples/designparts_v1.0.yaml b/examples/designparts_v1.0.yaml index 9953e68b..f174b73b 100644 --- a/examples/designparts_v1.0.yaml +++ b/examples/designparts_v1.0.yaml @@ -346,4 +346,4 @@ spec: address: 'dhcp' - network: 'mgmt' address: '172.16.1.83' ---- \ No newline at end of file +... \ No newline at end of file From 179459adc0301775330d634ed4055cbf1580489a Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Wed, 19 Jul 2017 11:04:30 -0500 Subject: [PATCH 22/26] Fix some issues found during CLI testing Found some bugs during REST client / CLI testing in the API and downstream methods --- docs/drydock_client.rst | 82 +++++++++++++++++++ .../drivers/node/maasdriver/driver.py | 6 +- drydock_provisioner/ingester/plugins/yaml.py | 1 + 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 docs/drydock_client.rst diff --git a/docs/drydock_client.rst b/docs/drydock_client.rst new file mode 100644 index 00000000..66ac4187 --- /dev/null +++ b/docs/drydock_client.rst @@ -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 diff --git a/drydock_provisioner/drivers/node/maasdriver/driver.py b/drydock_provisioner/drivers/node/maasdriver/driver.py index 17082f96..1f4573e3 100644 --- a/drydock_provisioner/drivers/node/maasdriver/driver.py +++ b/drydock_provisioner/drivers/node/maasdriver/driver.py @@ -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), diff --git a/drydock_provisioner/ingester/plugins/yaml.py b/drydock_provisioner/ingester/plugins/yaml.py index f9e6b7d7..0aac87d8 100644 --- a/drydock_provisioner/ingester/plugins/yaml.py +++ b/drydock_provisioner/ingester/plugins/yaml.py @@ -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 = {} From 8431a4aaf936939e19ca8f950346d77339ff3711 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Wed, 19 Jul 2017 11:05:41 -0500 Subject: [PATCH 23/26] Additional unit tests for REST client Add unit test for get_task method --- tests/unit/test_drydock_client.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_drydock_client.py b/tests/unit/test_drydock_client.py index fc6e01c8..aa2b3ff2 100644 --- a/tests/unit/test_drydock_client.py +++ b/tests/unit/test_drydock_client.py @@ -83,7 +83,7 @@ def test_session_get(): 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]) + json=[design_id], status=200) host = 'foo.bar.baz' token = '5f1e08b6-38ec-4a99-9d0f-00d29c4e325b' @@ -101,7 +101,7 @@ def test_client_design_get(): } responses.add(responses.GET, 'http://foo.bar.baz/api/v1.0/designs/828e88dc-6a8b-11e7-97ae-080027ef795a', - json=design) + json=design, status=200) host = 'foo.bar.baz' @@ -113,3 +113,29 @@ def test_client_design_get(): 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'] From 8a46aa5761908fb3b277925d2328201cadbaf746 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Fri, 21 Jul 2017 13:47:17 -0500 Subject: [PATCH 24/26] Add click dependency Add click 6.7 to the requirements-direct.txt file --- requirements-direct.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-direct.txt b/requirements-direct.txt index bf099b84..5c210bb9 100644 --- a/requirements-direct.txt +++ b/requirements-direct.txt @@ -8,3 +8,4 @@ oauthlib uwsgi>1.4 bson===0.4.7 oslo.config +click===6.7 From 1f40fa4cc2cb390a95837cef642cf38ee82152a7 Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Fri, 21 Jul 2017 16:28:33 -0500 Subject: [PATCH 25/26] Fix typo in response data --- drydock_provisioner/control/designs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drydock_provisioner/control/designs.py b/drydock_provisioner/control/designs.py index 8ef76bc1..063d0394 100644 --- a/drydock_provisioner/control/designs.py +++ b/drydock_provisioner/control/designs.py @@ -126,7 +126,7 @@ class DesignsPartsResource(StatefulResource): part_catalog.append({'kind': 'Region', 'key': site.get_id()}) - part_catalog.extend([{'kind': 'Netowrk', 'key': n.get_id()} for n in design.networks]) + 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]) From ba92e8f11463bc65821bb71b5d43ed5de3d8260f Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Fri, 21 Jul 2017 16:31:21 -0500 Subject: [PATCH 26/26] Remove errant line in comment --- drydock_provisioner/cli/task/actions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/drydock_provisioner/cli/task/actions.py b/drydock_provisioner/cli/task/actions.py index 1fc3d437..c2d66683 100644 --- a/drydock_provisioner/cli/task/actions.py +++ b/drydock_provisioner/cli/task/actions.py @@ -34,7 +34,6 @@ class TaskCreate(CliAction): # pylint: disable=too-few-public-methods """ def __init__(self, api_client, design_id, action_name=None, node_names=None, rack_names=None, node_tags=None): """ - node_filter : {node_names: [], rack_names:[], node-tags[]} :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