diff --git a/requirements.txt b/requirements.txt index 8388a4b4..7783a70d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,11 +13,11 @@ # limitations under the License. alembic==0.9.5 -arrow==0.10.0 +arrow==0.10.0 # API and Client configparser==3.5.0 falcon==1.2.0 jsonschema==2.6.0 -keystoneauth1==2.13.0 +keystoneauth1==2.13.0 # API and Client keystonemiddleware==4.17.0 oslo.config==4.11.0 oslo.policy==1.25.1 @@ -26,11 +26,13 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 psycopg2==2.7.3.1 python-dateutil==2.6.1 python-memcached==1.58 -python-openstackclient==3.11.0 -requests==2.18.4 +requests==2.18.4 # API and Client SQLAlchemy==1.1.13 ulid==1.1 uwsgi==2.0.15 + +# Client click==6.7 click-default-group==1.2 +PTable==0.9.2 pyyaml==3.12 diff --git a/shipyard_client/api_client/base_client.py b/shipyard_client/api_client/base_client.py index f17e42c4..43538d20 100644 --- a/shipyard_client/api_client/base_client.py +++ b/shipyard_client/api_client/base_client.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# 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. @@ -11,17 +11,44 @@ # 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 abc import logging + +from keystoneauth1.exceptions.auth import AuthorizationFailure +from keystoneauth1.exceptions.catalog import EndpointNotFound +from keystoneauth1.identity import v3 +from keystoneauth1 import session import requests -from .client_error import ClientError +from shipyard_client.api_client.client_error import ClientError +from shipyard_client.api_client.client_error import UnauthenticatedClientError +from shipyard_client.api_client.client_error import UnauthorizedClientError -class BaseClient: +class BaseClient(metaclass=abc.ABCMeta): + """Abstract base client class + + Requrires the definition of service_type and interface by child classes + """ + @property + @abc.abstractmethod + def service_type(self): + """Specify the name/type used to lookup the service""" + pass + + @property + @abc.abstractmethod + def interface(self): + """The interface to choose from during service lookup + + Specify the interface to look up the service: public, internal admin + """ + pass + def __init__(self, context): self.logger = logging.Logger('api_client') self.context = context + self.endpoint = None def log_message(self, level, msg): """ Logs a message with context, and extra populated. """ @@ -57,14 +84,21 @@ class BaseClient: headers = { 'X-Context-Marker': self.context.context_marker, 'content-type': content_type, - 'X-Auth-Token': self.context.get_token() + 'X-Auth-Token': self.get_token() } self.debug('Post request url: ' + url) self.debug('Query Params: ' + str(query_params)) # This could use keystoneauth1 session, but that library handles # responses strangely (wraps all 400/500 in a keystone exception) - return requests.post( + response = requests.post( url, data=data, params=query_params, headers=headers) + # handle some cases where the response code is sufficient to know + # what needs to be done + if response.status_code == 401: + raise UnauthenticatedClientError() + if response.status_code == 403: + raise UnauthorizedClientError() + return response except requests.exceptions.RequestException as e: self.error(str(e)) raise ClientError(str(e)) @@ -76,11 +110,52 @@ class BaseClient: try: headers = { 'X-Context-Marker': self.context.context_marker, - 'X-Auth-Token': self.context.get_token() + 'X-Auth-Token': self.get_token() } self.debug('url: ' + url) self.debug('Query Params: ' + str(query_params)) - return requests.get(url, params=query_params, headers=headers) + response = requests.get(url, params=query_params, headers=headers) + # handle some cases where the response code is sufficient to know + # what needs to be done + if response.status_code == 401: + raise UnauthenticatedClientError() + if response.status_code == 403: + raise UnauthorizedClientError() + return response except requests.exceptions.RequestException as e: self.error(str(e)) raise ClientError(str(e)) + + def get_token(self): + """ + Returns the simple token string for a token acquired from keystone + """ + return self._get_ks_session().get_auth_headers().get('X-Auth-Token') + + def _get_ks_session(self): + self.logger.debug('Accessing keystone for keystone session') + try: + auth = v3.Password(**self.context.keystone_auth) + return session.Session(auth=auth) + except AuthorizationFailure as e: + self.logger.error('Could not authorize against keystone: %s', + str(e)) + raise ClientError(str(e)) + + def get_endpoint(self): + """Lookup the endpoint for the client. Cache it. + + Uses a keystone session to find an endpoint for the specified + service_type at the specified interface (public, internal, admin) + """ + if self.endpoint is None: + self.logger.debug('Accessing keystone for %s endpoint', + self.service_type) + try: + self.endpoint = self._get_ks_session().get_endpoint( + interface=self.interface, service_type=self.service_type) + except EndpointNotFound as e: + self.logger.error('Could not find %s interface for %s', + self.interface, self.service_type) + raise ClientError(str(e)) + return self.endpoint diff --git a/shipyard_client/api_client/client_error.py b/shipyard_client/api_client/client_error.py index b46076f0..f6af7423 100644 --- a/shipyard_client/api_client/client_error.py +++ b/shipyard_client/api_client/client_error.py @@ -15,3 +15,11 @@ class ClientError(Exception): pass + + +class UnauthorizedClientError(ClientError): + pass + + +class UnauthenticatedClientError(ClientError): + pass diff --git a/shipyard_client/api_client/shipyard_api_client.py b/shipyard_client/api_client/shipyard_api_client.py index 856b892e..a56596c5 100644 --- a/shipyard_client/api_client/shipyard_api_client.py +++ b/shipyard_client/api_client/shipyard_api_client.py @@ -42,10 +42,9 @@ class ShipyardClient(BaseClient): A client for shipyard API :param context: shipyardclient_context, context object """ - - def __init__(self, context): - super().__init__(context) - self.shipyard_url = context.shipyard_endpoint + # Set up the values used to look up the service endpoint. + interface = 'public' + service_type = 'shipyard' def post_configdocs(self, collection_id=None, @@ -60,8 +59,10 @@ class ShipyardClient(BaseClient): :rtype: Response object """ query_params = {"buffermode": buffer_mode} - url = ApiPaths.POST_GET_CONFIG.value.format(self.shipyard_url, - collection_id) + url = ApiPaths.POST_GET_CONFIG.value.format( + self.get_endpoint(), + collection_id + ) return self.post_resp(url, query_params, document_data) def get_configdocs(self, collection_id=None, version='buffer'): @@ -73,8 +74,9 @@ class ShipyardClient(BaseClient): :rtype: Response object """ query_params = {"version": version} - url = ApiPaths.POST_GET_CONFIG.value.format(self.shipyard_url, - collection_id) + url = ApiPaths.POST_GET_CONFIG.value.format( + self.get_endpoint(), + collection_id) return self.get_resp(url, query_params) def get_rendereddocs(self, version='buffer'): @@ -84,7 +86,9 @@ class ShipyardClient(BaseClient): :rtype: Response object """ query_params = {"version": version} - url = ApiPaths.GET_RENDERED.value.format(self.shipyard_url) + url = ApiPaths.GET_RENDERED.value.format( + self.get_endpoint() + ) return self.get_resp(url, query_params) def commit_configdocs(self, force=False): @@ -94,7 +98,7 @@ class ShipyardClient(BaseClient): :rtype: Response object """ query_params = {"force": force} - url = ApiPaths.COMMIT_CONFIG.value.format(self.shipyard_url) + url = ApiPaths.COMMIT_CONFIG.value.format(self.get_endpoint()) return self.post_resp(url, query_params) def get_actions(self): @@ -103,7 +107,9 @@ class ShipyardClient(BaseClient): :returns: lists all actions :rtype: Response object """ - url = ApiPaths.POST_GET_ACTIONS.value.format(self.shipyard_url) + url = ApiPaths.POST_GET_ACTIONS.value.format( + self.get_endpoint() + ) return self.get_resp(url) def post_actions(self, name=None, parameters=None): @@ -115,7 +121,9 @@ class ShipyardClient(BaseClient): :rtype: Response object """ action_data = {"name": name, "parameters": parameters} - url = ApiPaths.POST_GET_ACTIONS.value.format(self.shipyard_url) + url = ApiPaths.POST_GET_ACTIONS.value.format( + self.get_endpoint() + ) return self.post_resp( url, data=json.dumps(action_data), content_type='application/json') @@ -126,8 +134,10 @@ class ShipyardClient(BaseClient): :returns: information describing the action :rtype: Response object """ - url = ApiPaths.GET_ACTION_DETAIL.value.format(self.shipyard_url, - action_id) + url = ApiPaths.GET_ACTION_DETAIL.value.format( + self.get_endpoint(), + action_id + ) return self.get_resp(url) def get_validation_detail(self, action_id=None, validation_id=None): @@ -139,7 +149,7 @@ class ShipyardClient(BaseClient): :rtype: Response object """ url = ApiPaths.GET_VALIDATION_DETAIL.value.format( - self.shipyard_url, action_id, validation_id) + self.get_endpoint(), action_id, validation_id) return self.get_resp(url) def get_step_detail(self, action_id=None, step_id=None): @@ -150,8 +160,11 @@ class ShipyardClient(BaseClient): :returns: details for a step by id for the given action by Id :rtype: Response object """ - url = ApiPaths.GET_STEP_DETAIL.value.format(self.shipyard_url, - action_id, step_id) + url = ApiPaths.GET_STEP_DETAIL.value.format( + self.get_endpoint(), + action_id, + step_id + ) return self.get_resp(url) def post_control_action(self, action_id=None, control_verb=None): @@ -163,7 +176,7 @@ class ShipyardClient(BaseClient): :rtype: Response object """ url = ApiPaths.POST_CONTROL_ACTION.value.format( - self.shipyard_url, action_id, control_verb) + self.get_endpoint(), action_id, control_verb) return self.post_resp(url) def get_workflows(self, since=None): @@ -175,7 +188,7 @@ class ShipyardClient(BaseClient): :rtype: Response object """ query_params = {'since': since} - url = ApiPaths.GET_WORKFLOWS.value.format(self.shipyard_url) + url = ApiPaths.GET_WORKFLOWS.value.format(self.get_endpoint()) return self.get_resp(url, query_params) def get_dag_detail(self, workflow_id=None): @@ -185,6 +198,6 @@ class ShipyardClient(BaseClient): :returns: details of a DAGs output :rtype: Response object """ - url = ApiPaths.GET_DAG_DETAIL.value.format(self.shipyard_url, + url = ApiPaths.GET_DAG_DETAIL.value.format(self.get_endpoint(), workflow_id) return self.get_resp(url) diff --git a/shipyard_client/api_client/shipyardclient_context.py b/shipyard_client/api_client/shipyardclient_context.py index fafb5cf2..d80a977a 100644 --- a/shipyard_client/api_client/shipyardclient_context.py +++ b/shipyard_client/api_client/shipyardclient_context.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# 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. @@ -11,64 +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 logging -from keystoneauth1 import session -from keystoneauth1.identity import v3 -from keystoneauth1.exceptions.auth import AuthorizationFailure -from keystoneauth1.exceptions.catalog import EndpointNotFound -from .client_error import ClientError - LOG = logging.getLogger(__name__) class ShipyardClientContext: + """A context object for ShipyardClient instances.""" + def __init__(self, keystone_auth, context_marker, debug=False): - """ - shipyard context object + """Shipyard context object + :param bool debug: true, or false :param str context_marker: :param dict keystone_auth: auth_url, password, project_domain_name, project_name, username, user_domain_name """ - self.debug = debug - self.keystone_auth = keystone_auth - # the service type will for now just be shipyard will change later - self.service_type = 'shipyard' - self.shipyard_endpoint = self.get_endpoint() - self.set_debug() - self.context_marker = context_marker - - def set_debug(self): if self.debug: LOG.setLevel(logging.DEBUG) - def get_token(self): - """ - Returns the simple token string for a token acquired from keystone - """ - return self._get_ks_session().get_auth_headers().get('X-Auth-Token') - - def _get_ks_session(self): - LOG.debug('Accessing keystone for keystone session') - try: - auth = v3.Password(**self.keystone_auth) - return session.Session(auth=auth) - except AuthorizationFailure as e: - LOG.error('Could not authorize against keystone: %s', str(e)) - raise ClientError(str(e)) - - def get_endpoint(self): - """ - Wraps calls to keystone for lookup with overrides from configuration - """ - LOG.debug('Accessing keystone for %s endpoint', self.service_type) - try: - return self._get_ks_session().get_endpoint( - interface='public', service_type=self.service_type) - except EndpointNotFound as e: - LOG.error('Could not find a public interface for %s', - self.service_type) - raise ClientError(str(e)) + self.keystone_auth = keystone_auth + self.context_marker = context_marker diff --git a/shipyard_client/cli/action.py b/shipyard_client/cli/action.py index 9836c361..4b24860e 100644 --- a/shipyard_client/cli/action.py +++ b/shipyard_client/cli/action.py @@ -13,60 +13,164 @@ # limitations under the License. # Base classes for cli actions intended to invoke the api - +import abc import logging +from shipyard_client.api_client.client_error import ClientError +from shipyard_client.api_client.client_error import UnauthenticatedClientError +from shipyard_client.api_client.client_error import UnauthorizedClientError from shipyard_client.api_client.shipyard_api_client import ShipyardClient from shipyard_client.api_client.shipyardclient_context import \ ShipyardClientContext -from shipyard_client.api_client.client_error import ClientError -from shipyard_client.cli.input_checks import validate_auth_vars +from shipyard_client.cli import format_utils -class CliAction(object): +class AuthValuesError(Exception): + """Signals a failure in the authentication values provided to an action + + Daignostic parameter is forced since newlines in exception text apparently + do not print with the exception. + """ + def __init__(self, *, diagnostic): + self.diagnostic = diagnostic + + +class AbstractCliAction(metaclass=abc.ABCMeta): + """Abstract base class for CLI actions + + Base class to encapsulate the items that must be implemented by + concrete actions + """ + + @abc.abstractmethod + def invoke(self): + """Default invoke for CLI actions + + Descendent classes must override this method to perform the actual + needed invocation. The expected response from this method is a response + object or raise an exception. + """ + pass + + @property + @abc.abstractmethod + def cli_handled_err_resp_codes(self): + """Error response codes + + Descendent classes shadow this for those response codes from invocation + that should be handled using the format_utils.cli_format_error_handler + Note that 401, 403 responses are handled prior to this via exception, + and should not be represented here. e.g.: [400, 409]. + """ + pass + + @property + @abc.abstractmethod + def cli_handled_succ_resp_codes(self): + """Success response codes + + Concrete actions must implement cli_handled_succ_resp_codes to indicate + the response code that should utilize the overridden + cli_format_response_handler of the sepecific action + """ + pass + + @abc.abstractmethod + def cli_format_response_handler(self, response): + """Abstract format handler for cli output "good" responses + + Overridden by descendent classes to indicate the specific output format + when the ation is invoked with a output format of "cli". + + Expected to return the string of the output. + + For those actions that do not have a valid "cli" output, the following + would be generally appropriate for an implementation of this method to + return the api client's response: + + return format_utils.formatted_response_handler(response) + """ + pass + + +class CliAction(AbstractCliAction): """Action base for CliActions""" def __init__(self, ctx): - """Sets api_client""" + """Initialize CliAction""" self.logger = logging.getLogger('shipyard_cli') self.api_parameters = ctx.obj['API_PARAMETERS'] self.resp_txt = "" self.needs_credentials = False + self.output_format = ctx.obj['FORMAT'] - auth_vars = self.api_parameters['auth_vars'] - context_marker = self.api_parameters['context_marker'] - debug = self.api_parameters['debug'] + self.auth_vars = self.api_parameters.get('auth_vars') + self.context_marker = self.api_parameters.get('context_marker') + self.debug = self.api_parameters.get('debug') - validate_auth_vars(ctx, self.api_parameters.get('auth_vars')) + self.client_context = ShipyardClientContext( + self.auth_vars, self.context_marker, self.debug) - self.logger.debug("Passing environment varibles to the API client") - try: - shipyard_client_context = ShipyardClientContext( - auth_vars, context_marker, debug) - self.api_client = ShipyardClient(shipyard_client_context) - except ClientError as e: - self.logger.debug("The shipyard Client Context could not be set.") - ctx.fail('Client Error: %s.' % str(e)) + def get_api_client(self): + """Returns the api client for this action""" + return ShipyardClient(self.client_context) def invoke_and_return_resp(self): - """ - calls the invoke method in the approiate actions.py and returns the - formatted response - """ + """Lifecycle method to invoke and return a response - self.logger.debug("Inoking action.") - env_vars = self.api_parameters['auth_vars'] + Calls the invoke method in the child action and returns the formatted + response. + """ + self.logger.debug("Invoking: %s", self.__class__.__name__) try: - self.invoke() - except ClientError as e: - self.resp_txt = "Client Error: %s." % str(e) - except Exception as e: - self.resp_txt = "Error: Unable to invoke action because %s." % str( - e) + self.validate_auth_vars() + self.resp_txt = self.output_formatter(self.invoke()) + except AuthValuesError as ave: + self.resp_txt = "Error: {}".format(ave.diagnostic) + except UnauthenticatedClientError: + self.resp_txt = ("Error: Command requires authentication. " + "Check credential values") + except UnauthorizedClientError: + self.resp_txt = "Error: Unauthorized to perform this action." + except ClientError as ex: + self.resp_txt = "Error: Client Error: {}".format(str(ex)) + except Exception as ex: + self.resp_txt = ( + "Error: Unable to invoke action due to: {}".format(str(ex))) return self.resp_txt - def invoke(self): - """Default invoke""" - self.resp_txt = "Error: Invoke method is not defined for this action." + def output_formatter(self, response): + """Formats response (Requests library) from api_client + + Dispatches to the appropriate response format handler. + """ + if self.output_format == 'raw': + return format_utils.raw_format_response_handler(response) + elif self.output_format == 'cli': + if response.status_code in self.cli_handled_err_resp_codes: + return format_utils.cli_format_error_handler(response) + elif response.status_code in self.cli_handled_succ_resp_codes: + return self.cli_format_response_handler(response) + else: + self.logger.debug("Unexpected response received") + return format_utils.cli_format_error_handler(response) + else: # assume formatted + return format_utils.formatted_response_handler(response) + + def validate_auth_vars(self): + """Checks that the required authorization varible have been entered""" + required_auth_vars = ['auth_url'] + err_txt = [] + for var in required_auth_vars: + if self.auth_vars[var] is None: + err_txt.append( + 'Missing the required authorization variable: ' + '--os_{}'.format(var)) + if err_txt: + for var in self.auth_vars: + if (self.auth_vars.get(var) is None and + var not in required_auth_vars): + err_txt.append('- Also not set: --os_{}'.format(var)) + raise AuthValuesError(diagnostic='\n'.join(err_txt)) diff --git a/shipyard_client/cli/cli_format_common.py b/shipyard_client/cli/cli_format_common.py new file mode 100644 index 00000000..e28db1f4 --- /dev/null +++ b/shipyard_client/cli/cli_format_common.py @@ -0,0 +1,211 @@ +# 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. +"""Reusable parts for outputting Shipyard results in CLI format""" + +from shipyard_client.cli import format_utils + + +def gen_action_steps(step_list, action_id): + """Generate a table from the list of steps. + + Assumes that the input list contains dictionaries with 'id', 'index', and + 'state' fields. + Returns a string representation of the table. + """ + # Generate the steps table. + steps = format_utils.table_factory( + field_names=['Steps', 'Index', 'State'] + ) + if step_list: + for step in step_list: + steps.add_row( + ['step/{}/{}'.format(action_id, step.get('id')), + step.get('index'), + step.get('state')] + ) + else: + steps.add_row(['None', '', '']) + + return format_utils.table_get_string(steps) + + +def gen_action_commands(command_list): + """Generate a table from the list of commands + + Assumes command_list is a list of dictionaries with 'command', 'user', and + 'datetime'. + """ + cmds = format_utils.table_factory( + field_names=['Commands', 'User', 'Datetime'] + ) + if command_list: + for cmd in command_list: + cmds.add_row( + [cmd.get('command'), cmd.get('user'), cmd.get('datetime')] + ) + else: + cmds.add_row(['None', '', '']) + + return format_utils.table_get_string(cmds) + + +def gen_action_validations(validation_list): + """Generates a CLI formatted listing of validations + + Assumes validation_list is a list of dictionaries with 'validation_name', + 'action_id', 'id', and 'details'. + """ + if validation_list: + validations = [] + for val in validation_list: + validations.append('{} : validation/{}/{}\n'.format( + val.get('validation_name'), + val.get('action_id'), + val.get('id') + )) + validations.append(val.get('details')) + validations.append('\n\n') + return 'Validations: {}'.format('\n'.join(validations)) + else: + return 'Validations: {}'.format('None') + + +def gen_action_details(action_dict): + """Generates the detailed information for an action + + Assumes action_dict is a dictionary with 'name', 'id', 'action_lifecycle', + 'parameters', 'datetime', 'dag_status', 'context_marker', and 'user' + """ + details = format_utils.table_factory() + details.add_row(['Name:', action_dict.get('name')]) + details.add_row(['Action:', 'action/{}'.format(action_dict.get('id'))]) + details.add_row(['Lifecycle:', action_dict.get('action_lifecycle')]) + details.add_row(['Parameters:', str(action_dict.get('parameters'))]) + details.add_row(['Datetime:', action_dict.get('datetime')]) + details.add_row(['Dag Status:', action_dict.get('dag_status')]) + details.add_row(['Context Marker:', action_dict.get('context_marker')]) + details.add_row(['User:', action_dict.get('user')]) + return format_utils.table_get_string(details) + + +def gen_action_step_details(step_dict, action_id): + """Generates the detailed information for an action step + + Assumes action_dict is a dictionary with 'index', 'state', 'start_date', + 'end_date', 'duration', 'try_number', and 'operator' + """ + details = format_utils.table_factory() + details.add_row(['Name:', step_dict.get('task_id')]) + details.add_row(['Task ID:', 'step/{}/{}'.format( + action_id, + step_dict.get('task_id') + )]) + details.add_row(['Index:', step_dict.get('index')]) + details.add_row(['State:', step_dict.get('state')]) + details.add_row(['Start Date:', step_dict.get('start_date')]) + details.add_row(['End Date:', step_dict.get('end_date')]) + details.add_row(['Duration:', step_dict.get('duration')]) + details.add_row(['Try Number:', step_dict.get('try_number')]) + details.add_row(['Operator:', step_dict.get('operator')]) + return format_utils.table_get_string(details) + + +def gen_action_table(action_list): + """Generates a list of actions + + Assumes action_list is a list of dictionaries with 'name', 'id', and + 'action_lifecycle' + """ + actions = format_utils.table_factory( + field_names=['Name', 'Action', 'Lifecycle'] + ) + if action_list: + for action in action_list: + actions.add_row( + [action.get('name'), + 'action/{}'.format(action.get('id')), + action.get('action_lifecycle')] + ) + else: + actions.add_row(['None', '', '']) + + return format_utils.table_get_string(actions) + + +def gen_workflow_table(workflow_list): + """Generates a list of workflows + + Assumes workflow_list is a list of dictionaries with 'workflow_id' and + 'state' + """ + workflows = format_utils.table_factory( + field_names=['Workflows', 'State'] + ) + if workflow_list: + for workflow in workflow_list: + workflows.add_row( + [workflow.get('workflow_id'), workflow.get('state')]) + else: + workflows.add_row(['None', '']) + + return format_utils.table_get_string(workflows) + + +def gen_workflow_details(workflow_dict): + """Generates a workflow detail + + Assumes workflow_dict has 'execution_date', 'end_date', 'workflow_id', + 'start_date', 'external_trigger', 'steps', 'dag_id', 'state', 'run_id', + and 'sub_dags' + """ + details = format_utils.table_factory() + details.add_row(['Workflow:', workflow_dict.get('workflow_id')]) + + details.add_row(['State:', workflow_dict.get('state')]) + details.add_row(['Dag ID:', workflow_dict.get('dag_id')]) + details.add_row(['Execution Date:', workflow_dict.get('execution_date')]) + details.add_row(['Start Date:', workflow_dict.get('start_date')]) + details.add_row(['End Date:', workflow_dict.get('end_date')]) + details.add_row(['External Trigger:', + workflow_dict.get('external_trigger')]) + return format_utils.table_get_string(details) + + +def gen_workflow_steps(step_list): + """Generates a table of steps for a workflow + + Assumes step_list is a list of dictionaries with 'task_id' and 'state' + """ + steps = format_utils.table_factory( + field_names=['Steps', 'State'] + ) + if step_list: + for step in step_list: + steps.add_row([step.get('task_id'), step.get('state')]) + else: + steps.add_row(['None', '']) + + return format_utils.table_get_string(steps) + + +def gen_sub_workflows(wf_list): + """Generates the list of Sub Workflows + + Assumes wf_list is a list of dictionaries with the same contents as a + standard workflow + """ + wfs = [] + for wf in wf_list: + wfs.append(gen_workflow_details(wf)) + return '\n\n'.join(wfs) diff --git a/shipyard_client/cli/commit/actions.py b/shipyard_client/cli/commit/actions.py index 970ff6e6..169245cf 100644 --- a/shipyard_client/cli/commit/actions.py +++ b/shipyard_client/cli/commit/actions.py @@ -13,23 +13,38 @@ # limitations under the License. from shipyard_client.cli.action import CliAction -from shipyard_client.cli.output_formatting import output_formatting +from shipyard_client.cli import format_utils class CommitConfigdocs(CliAction): """Actions to Commit Configdocs""" def __init__(self, ctx, force): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.force = force - self.output_format = ctx.obj['FORMAT'] self.logger.debug("CommitConfigdocs action initialized with force=%s", force) def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client commit_configdocs.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.commit_configdocs(force=self.force)) + return self.get_api_client().commit_configdocs(force=self.force) + + # Handle 400, 409 with default error handler for cli. + cli_handled_err_resp_codes = [400, 409] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + outfmt_string = "Configuration documents committed.\n{}" + return outfmt_string.format( + format_utils.cli_format_status_handler(response) + ) diff --git a/shipyard_client/cli/control/actions.py b/shipyard_client/cli/control/actions.py index 04f68025..8667b560 100644 --- a/shipyard_client/cli/control/actions.py +++ b/shipyard_client/cli/control/actions.py @@ -11,24 +11,38 @@ # limitations under the License. from shipyard_client.cli.action import CliAction -from shipyard_client.cli.output_formatting import output_formatting class Control(CliAction): """Action to Pause Process""" def __init__(self, ctx, control_verb, action_id): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.action_id = action_id self.control_verb = control_verb - self.output_format = ctx.obj['FORMAT'] self.logger.debug("ControlPause action initialized") def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client post_control_action.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.post_control_action( - action_id=self.action_id, - control_verb=self.control_verb)) + return self.get_api_client().post_control_action( + action_id=self.action_id, + control_verb=self.control_verb + ) + + # Handle 400, 409 with default error handler for cli. + cli_handled_err_resp_codes = [400, 409] + + # Handle 202 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [202] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 202 responses + """ + return "{} successfully submitted for action {}".format( + self.control_verb, self.action_id) diff --git a/shipyard_client/cli/create/actions.py b/shipyard_client/cli/create/actions.py index 990fe78e..030d49ba 100644 --- a/shipyard_client/cli/create/actions.py +++ b/shipyard_client/cli/create/actions.py @@ -9,37 +9,51 @@ # 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. - from shipyard_client.cli.action import CliAction -from shipyard_client.cli.output_formatting import output_formatting +from shipyard_client.cli import cli_format_common +from shipyard_client.cli import format_utils class CreateAction(CliAction): """Action to Create Action""" def __init__(self, ctx, action_name, param): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug("CreateAction action initialized with action command" "%s and parameters %s", action_name, param) self.action_name = action_name self.param = param - self.output_format = ctx.obj['FORMAT'] def invoke(self): - """Calls API Client and formats response from API Client""" + """Returns the response from API Client""" self.logger.debug("Calling API Client post_actions.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.post_actions( - name=self.action_name, - parameters=self.param)) + return self.get_api_client().post_actions(name=self.action_name, + parameters=self.param) + + # Handle 400, 409 with default error handler for cli. + cli_handled_err_resp_codes = [400, 409] + + # Handle 201 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [201] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 201 responses + """ + resp_j = response.json() + action_list = [resp_j] if resp_j else [] + return cli_format_common.gen_action_table(action_list) class CreateConfigdocs(CliAction): """Action to Create Configdocs""" def __init__(self, ctx, collection, buffer, data): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug("CreateConfigdocs action initialized with" + " collection=%s,buffer=%s and data=%s", collection, @@ -47,13 +61,30 @@ class CreateConfigdocs(CliAction): self.collection = collection self.buffer = buffer self.data = data - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client post_configdocs.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.post_configdocs( - collection_id=self.collection, - buffer_mode=self.buffer, - document_data=self.data)) + return self.get_api_client().post_configdocs( + collection_id=self.collection, + buffer_mode=self.buffer, + document_data=self.data + ) + + # Handle 409 with default error handler for cli. + cli_handled_err_resp_codes = [409] + + # Handle 201 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [201] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 201 responses + """ + outfmt_string = "Configuration documents added.\n{}" + return outfmt_string.format( + format_utils.cli_format_status_handler(response) + ) diff --git a/shipyard_client/cli/describe/actions.py b/shipyard_client/cli/describe/actions.py index 8e47ab1a..ae94c842 100644 --- a/shipyard_client/cli/describe/actions.py +++ b/shipyard_client/cli/describe/actions.py @@ -11,87 +11,158 @@ # limitations under the License. from shipyard_client.cli.action import CliAction -from shipyard_client.cli.output_formatting import output_formatting +from shipyard_client.cli import cli_format_common class DescribeAction(CliAction): """Action to Describe Action""" def __init__(self, ctx, action_id): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug( "DescribeAction action initialized with action_id=%s", action_id) self.action_id = action_id - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_action_detail.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.get_action_detail(action_id=self.action_id)) + return self.get_api_client().get_action_detail( + action_id=self.action_id) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + # Assemble the sections of the action details + return '{}\n\n{}\n\n{}\n\n{}\n'.format( + cli_format_common.gen_action_details(resp_j), + cli_format_common.gen_action_steps(resp_j.get('steps'), + resp_j.get('id')), + cli_format_common.gen_action_commands(resp_j.get('commands')), + cli_format_common.gen_action_validations( + resp_j.get('validations') + ) + ) class DescribeStep(CliAction): """Action to Describe Step""" def __init__(self, ctx, action_id, step_id): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug( "DescribeStep action initialized with action_id=%s and step_id=%s", action_id, step_id) self.action_id = action_id self.step_id = step_id - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_step_detail.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.get_step_detail( - action_id=self.action_id, - step_id=self.step_id)) + return self.get_api_client().get_step_detail(action_id=self.action_id, + step_id=self.step_id) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + return cli_format_common.gen_action_step_details(resp_j, + self.action_id) class DescribeValidation(CliAction): """Action to Describe Validation""" def __init__(self, ctx, action_id, validation_id): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug( 'DescribeValidation action initialized with action_id=%s' 'and validation_id=%s', action_id, validation_id) self.validation_id = validation_id self.action_id = action_id - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_validation_detail.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.get_validation_detail( - action_id=self.action_id, validation_id=self.validation_id)) + return self.get_api_client().get_validation_detail( + action_id=self.action_id, validation_id=self.validation_id) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + val_list = [resp_j] if resp_j else [] + return cli_format_common.gen_action_validations(val_list) class DescribeWorkflow(CliAction): """Action to describe a workflow""" def __init__(self, ctx, workflow_id): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug( "DescribeWorkflow action initialized with workflow_id=%s", workflow_id) self.workflow_id = workflow_id - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_action_detail.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.get_dag_detail(workflow_id=self.workflow_id)) + return self.get_api_client().get_dag_detail( + workflow_id=self.workflow_id) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + # Assemble the workflow details + + return '{}\n\n{}\n\nSubworkflows:\n{}\n'.format( + cli_format_common.gen_workflow_details(resp_j), + cli_format_common.gen_workflow_steps(resp_j.get('steps', [])), + cli_format_common.gen_sub_workflows(resp_j.get('sub_dags', [])) + ) diff --git a/shipyard_client/cli/format_utils.py b/shipyard_client/cli/format_utils.py new file mode 100644 index 00000000..a6cc0acf --- /dev/null +++ b/shipyard_client/cli/format_utils.py @@ -0,0 +1,139 @@ +# 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 json +import yaml + +from prettytable import PrettyTable +from prettytable.prettytable import PLAIN_COLUMNS + + +def cli_format_error_handler(response): + """Generic handler for standard Shipyard error responses + + Method is intended for use when there is no special handling needed + for the response. + :param client_response: a requests response object assuming the + standard error format + :returns: a generically formatted error response formulated from the + client_repsonse. The response will be in the format: + + Error: {{message}} + Reason: {{Reason}} + Additional: {{details message list messages}} + ... + """ + return cli_format_status_handler(response, is_error=True) + + +def cli_format_status_handler(response, is_error=False): + """Handler for standard Shipyard status and status-based error responses + + Method is intended for use when there is no special handling needed + for the response. If the response is empty, returns empty string + :param client_response: a requests response object assuming the + standard error format + :is_error: toggles the use of status or error verbiage + :returns: a generically formatted error response formulated from the + client_repsonse. The response will be in the format: + + [Error|Status]: {{message}} + Reason: {{Reason}} + Additional: {{details message list messages}} + ... + """ + formatted = "Error: {}\nReason: {}" if is_error \ + else "Status: {}\nReason: {}" + try: + if response.text: + resp_j = response.json() + resp = formatted.format(resp_j.get('message', 'Not specified'), + resp_j.get('reason', 'Not specified')) + if resp_j.get('details'): + for message in resp_j.get('details').get('messageList', []): + if message.get('error', False): + resp = resp + '\n- Error: {}'.format( + message.get('message')) + else: + resp = resp + '\n- Info: {}'.format( + message.get('message')) + return resp + else: + return '' + except ValueError: + return "Error: Unable to decode response. Value: {}".format( + response.text) + + +def raw_format_response_handler(response): + """Basic format handler to return raw response text""" + return response.text + + +def formatted_response_handler(response): + """Base format handler for either json or yaml depending on call""" + call = response.headers['Content-Type'] + if 'json' in call: + try: + return json.dumps(response.json(), sort_keys=True, indent=4) + except ValueError: + return ( + "This is not json and could not be printed as such. \n" + + response.text + ) + + else: # all others should be yaml + try: + return (yaml.dump_all( + yaml.safe_load_all(response.content), + width=79, + indent=4, + default_flow_style=False)) + except ValueError: + return ( + "This is not yaml and could not be printed as such.\n" + + response.text + ) + + +def table_factory(field_names=None, rows=None, style=None): + """Generate a table using prettytable + + Factory method for a prettytable using the PLAIN_COLUMN style unless + ovrridden by the style parameter. + If a field_names list of strings is passed in, the column names + will be initialized. + If rows are supplied (list of lists), the will be added as rows in + order. + """ + p = PrettyTable() + if style is None: + p.set_style(PLAIN_COLUMNS) + else: + p.set_style(style) + if field_names: + p.field_names = field_names + else: + p.header = False + if rows: + for row in rows: + p.add_row(row) + # This alignment only works if columns and rows are set up. + p.align = 'l' + return p + + +def table_get_string(table, align='l'): + """Wrapper to return a prettytable string with the supplied alignment""" + table.align = 'l' + return table.get_string() diff --git a/shipyard_client/cli/get/actions.py b/shipyard_client/cli/get/actions.py index 527b4c14..a2a3925d 100644 --- a/shipyard_client/cli/get/actions.py +++ b/shipyard_client/cli/get/actions.py @@ -11,78 +11,133 @@ # limitations under the License. from shipyard_client.cli.action import CliAction -from shipyard_client.cli.output_formatting import output_formatting +from shipyard_client.cli import cli_format_common +from shipyard_client.cli import format_utils class GetActions(CliAction): """Action to Get Actions""" def __init__(self, ctx): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug("GetActions action initialized.") - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_actions.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.get_actions()) + return self.get_api_client().get_actions() + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + return cli_format_common.gen_action_table(resp_j) class GetConfigdocs(CliAction): """Action to Get Configdocs""" def __init__(self, ctx, collection, version): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug( "GetConfigdocs action initialized with collection=%s and " "version=%s" % (collection, version)) self.collection = collection self.version = version - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_configdocs.") - self.resp_txt = output_formatting(self.output_format, - self.api_client.get_configdocs( - collection_id=self.collection, - version=self.version)) + return self.get_api_client().get_configdocs( + collection_id=self.collection, version=self.version) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + Effectively passes through the YAML received. + :param response: a requests response object + :returns: a string representing a CLI appropriate response + Handles 200 responses + """ + return format_utils.raw_format_response_handler(response) class GetRenderedConfigdocs(CliAction): """Action to Get Rendered Configdocs""" def __init__(self, ctx, version): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug("GetRenderedConfigdocs action initialized") self.version = version - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_rendereddocs.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.get_rendereddocs(version=self.version)) + return self.get_api_client().get_rendereddocs(version=self.version) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + Effectively passes through the YAML received. + :param response: a requests response object + :returns: a string representing a CLI appropriate response + Handles 200 responses + """ + return format_utils.raw_format_response_handler(response) class GetWorkflows(CliAction): """Action to get workflows""" def __init__(self, ctx, since=None): - """Initializes api_client, sets parameters, and sets output_format""" + """Sets parameters.""" super().__init__(ctx) self.logger.debug("GetWorkflows action initialized.") self.since = since - self.output_format = ctx.obj['FORMAT'] def invoke(self): """Calls API Client and formats response from API Client""" self.logger.debug("Calling API Client get_actions.") - self.resp_txt = output_formatting( - self.output_format, - self.api_client.get_workflows(self.since)) + return self.get_api_client().get_workflows(self.since) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [404] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + wf_list = resp_j if resp_j else [] + return cli_format_common.gen_workflow_table(wf_list) diff --git a/shipyard_client/cli/input_checks.py b/shipyard_client/cli/input_checks.py index 08907eb7..32f289e2 100644 --- a/shipyard_client/cli/input_checks.py +++ b/shipyard_client/cli/input_checks.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. +"""CLI value checks invoked from commands""" import arrow from arrow.parser import ParserError @@ -59,25 +60,6 @@ def check_workflow_id(ctx, workflow_id): 'YYYY-MM-DDTHH:mm:ss.SSSSSS') -def validate_auth_vars(ctx, auth_vars): - """Checks that the required authurization varible have been entered""" - - required_auth_vars = ['auth_url'] - err_txt = "" - for var in required_auth_vars: - if auth_vars[var] is None: - err_txt += ( - 'Missing the required authorization variable: ' - '--os_{}\n'.format(var)) - if err_txt != "": - err_txt += ('\nMissing the following additional authorization ' - 'options: ') - for var in auth_vars: - if auth_vars[var] is None and var not in required_auth_vars: - err_txt += '\n--os_{}'.format(var) - ctx.fail(err_txt) - - def check_reformat_parameter(ctx, param): """Checks for = format""" param_dictionary = {} diff --git a/shipyard_client/cli/output_formatting.py b/shipyard_client/cli/output_formatting.py deleted file mode 100644 index 30026c33..00000000 --- a/shipyard_client/cli/output_formatting.py +++ /dev/null @@ -1,47 +0,0 @@ -# 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-1.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 json -import yaml - - -def output_formatting(output_format, response): - """formats response from api_client""" - if output_format == 'raw': - return response.text - else: # assume formatted - return formatted(response) - - -def formatted(response): - """Formats either json or yaml depending on call""" - call = response.headers['Content-Type'] - if 'json' in call: - try: - input = response.json() - return (json.dumps(input, sort_keys=True, indent=4)) - except ValueError: - return ("This is not json and could not be printed as such. \n " + - response.text) - - else: # all others should be yaml - try: - return (yaml.dump_all( - yaml.safe_load_all(response.content), - width=79, - indent=4, - default_flow_style=False)) - except ValueError: - return ("This is not yaml and could not be printed as such.\n " + - response.text) diff --git a/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py b/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py index a93b3f3c..7e596d2d 100644 --- a/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py +++ b/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# 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. @@ -11,192 +11,204 @@ # 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 mock import json +import mock -from shipyard_client.api_client.shipyard_api_client import ShipyardClient from shipyard_client.api_client.base_client import BaseClient +from shipyard_client.api_client.shipyard_api_client import ShipyardClient +from shipyard_client.api_client.shipyardclient_context import \ + ShipyardClientContext -class TemporaryContext: - def __init__(self): - self.debug = True - self.keystone_Auth = {} - self.token = 'abcdefgh' - self.service_type = 'http://shipyard' - self.shipyard_endpoint = 'http://shipyard/api/v1.0' - self.context_marker = '123456' +def replace_get_endpoint(self): + """Fake get endpoint method to isolate testing""" + return 'http://shipyard/api/v1.0' def replace_post_rep(self, url, query_params={}, data={}, content_type=''): - """ - replaces call to shipyard client + """Replaces call to shipyard client + :returns: dict with url and parameters """ return {'url': url, 'params': query_params, 'data': data} def replace_get_resp(self, url, query_params={}, json=False): - """ - replaces call to shipyard client + """Replaces call to shipyard client. + :returns: dict with url and parameters """ return {'url': url, 'params': query_params} -def replace_base_constructor(self, context): - pass - - def get_api_client(): """ get a instance of shipyard client :returns: shipyard client with no context object """ - context = TemporaryContext() + keystone_auth = { + 'project_domain_name': 'projDomainTest', + 'user_domain_name': 'userDomainTest', + 'project_name': 'projectTest', + 'username': 'usernameTest', + 'password': 'passwordTest', + 'auth_url': 'urlTest' + }, + + context = ShipyardClientContext( + debug=True, + keystone_auth=keystone_auth, + context_marker='88888888-4444-4444-4444-121212121212' + ) return ShipyardClient(context) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_post_config_docs(*args): shipyard_client = get_api_client() buffermode = 'rejectoncontents' result = shipyard_client.post_configdocs('ABC', buffer_mode=buffermode) params = result['params'] assert result['url'] == '{}/configdocs/ABC'.format( - shipyard_client.shipyard_url) + shipyard_client.get_endpoint()) assert params['buffermode'] == buffermode -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_config_docs(*args): shipyard_client = get_api_client() version = 'buffer' result = shipyard_client.get_configdocs('ABC', version=version) params = result['params'] assert result['url'] == '{}/configdocs/ABC'.format( - shipyard_client.shipyard_url) + shipyard_client.get_endpoint()) assert params['version'] == version -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_rendered_config_docs(*args): shipyard_client = get_api_client() version = 'buffer' result = shipyard_client.get_rendereddocs(version=version) params = result['params'] assert result['url'] == '{}/renderedconfigdocs'.format( - shipyard_client.shipyard_url) + shipyard_client.get_endpoint()) assert params['version'] == version -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_commit_configs(*args): shipyard_client = get_api_client() force_mode = True result = shipyard_client.commit_configdocs(force_mode) params = result['params'] assert result['url'] == '{}/commitconfigdocs'.format( - shipyard_client.shipyard_url) + shipyard_client.get_endpoint()) assert params['force'] == force_mode -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_actions(*args): shipyard_client = get_api_client() result = shipyard_client.get_actions() - assert result['url'] == '{}/actions'.format(shipyard_client.shipyard_url) + assert result['url'] == '{}/actions'.format( + shipyard_client.get_endpoint() + ) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_post_actions(*args): shipyard_client = get_api_client() name = 'good action' parameters = {'hello': 'world'} result = shipyard_client.post_actions(name, parameters) data = json.loads(result['data']) - assert result['url'] == '{}/actions'.format(shipyard_client.shipyard_url) + assert result['url'] == '{}/actions'.format( + shipyard_client.get_endpoint() + ) assert data['name'] == name assert data['parameters']['hello'] == 'world' -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_action_details(*args): shipyard_client = get_api_client() action_id = 'GoodAction' result = shipyard_client.get_action_detail(action_id) assert result['url'] == '{}/actions/{}'.format( - shipyard_client.shipyard_url, action_id) + shipyard_client.get_endpoint(), action_id) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_val_details(*args): shipyard_client = get_api_client() action_id = 'GoodAction' validation_id = 'Validation' result = shipyard_client.get_validation_detail(action_id, validation_id) assert result['url'] == '{}/actions/{}/validationdetails/{}'.format( - shipyard_client.shipyard_url, action_id, validation_id) + shipyard_client.get_endpoint(), action_id, validation_id) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_step_details(*args): shipyard_client = get_api_client() action_id = 'GoodAction' step_id = 'TestStep' result = shipyard_client.get_step_detail(action_id, step_id) assert result['url'] == '{}/actions/{}/steps/{}'.format( - shipyard_client.shipyard_url, action_id, step_id) + shipyard_client.get_endpoint(), action_id, step_id) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_post_control(*args): shipyard_client = get_api_client() action_id = 'GoodAction' control_verb = 'Control' result = shipyard_client.post_control_action(action_id, control_verb) assert result['url'] == '{}/actions/{}/control/{}'.format( - shipyard_client.shipyard_url, action_id, control_verb) + shipyard_client.get_endpoint(), action_id, control_verb) -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_workflows(*args): shipyard_client = get_api_client() since_mode = 'TestSince' result = shipyard_client.get_workflows(since_mode) - assert result['url'] == '{}/workflows'.format(shipyard_client.shipyard_url, - since_mode) + assert result['url'] == '{}/workflows'.format( + shipyard_client.get_endpoint()) + + params = result['params'] + assert 'since' in params -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) @mock.patch.object(BaseClient, 'post_resp', replace_post_rep) @mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) def test_get_dag_details(*args): shipyard_client = get_api_client() workflow_id = 'TestWorkflow' result = shipyard_client.get_dag_detail(workflow_id) assert result['url'] == '{}/workflows/{}'.format( - shipyard_client.shipyard_url, workflow_id) + shipyard_client.get_endpoint(), workflow_id) diff --git a/shipyard_client/tests/unit/cli/commit/test_commit_actions.py b/shipyard_client/tests/unit/cli/commit/test_commit_actions.py index 8962eb2f..cee036f2 100644 --- a/shipyard_client/tests/unit/cli/commit/test_commit_actions.py +++ b/shipyard_client/tests/unit/cli/commit/test_commit_actions.py @@ -11,56 +11,65 @@ # 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 mock -from shipyard_client.cli.commit.actions import CommitConfigdocs +import responses + from shipyard_client.api_client.base_client import BaseClient -from shipyard_client.tests.unit.cli.replace_api_client import \ - replace_base_constructor, replace_post_rep, replace_get_resp, \ - replace_output_formatting -from shipyard_client.tests.unit.cli.utils import temporary_context -from shipyard_client.api_client.shipyardclient_context import \ - ShipyardClientContext - -auth_vars = { - 'project_domain_name': 'projDomainTest', - 'user_domain_name': 'userDomainTest', - 'project_name': 'projectTest', - 'username': 'usernameTest', - 'password': 'passwordTest', - 'auth_url': 'urlTest' -} - -api_parameters = { - 'auth_vars': auth_vars, - 'context_marker': 'UUID', - 'debug': False -} +from shipyard_client.cli.commit.actions import CommitConfigdocs +from shipyard_client.tests.unit.cli import stubs -class MockCTX(): - pass +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_commit_configdocs(*args): + responses.add(responses.POST, + 'http://shiptest/commitconfigdocs?force=false', + body=None, + status=200) + response = CommitConfigdocs(stubs.StubCliContext(), + False).invoke_and_return_resp() + assert response == 'Configuration documents committed.\n' -ctx = MockCTX() -ctx.obj = {} -ctx.obj['API_PARAMETERS'] = api_parameters -ctx.obj['FORMAT'] = 'format' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_commit_configdocs_409(*args): + api_resp = stubs.gen_err_resp(message="Conflicts message", + sub_message='Another bucket message', + sub_error_count=1, + sub_info_count=0, + reason='Conflicts reason', + code=409) + responses.add(responses.POST, + 'http://shiptest/commitconfigdocs?force=false', + body=api_resp, + status=409) + response = CommitConfigdocs(stubs.StubCliContext(), + False).invoke_and_return_resp() + assert 'Error: Conflicts message' in response + assert 'Configuration documents committed' not in response + assert 'Reason: Conflicts reason' in response -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.commit.actions.output_formatting', - side_effect=replace_output_formatting) -def test_CommitConfigdocs(*args): - response = CommitConfigdocs(ctx, True).invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'commitconfigdocs' in url - # test function was called with correct parameters - params = response.get('params') - assert params.get('force') is True +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_commit_configdocs_forced(*args): + api_resp = stubs.gen_err_resp(message="Conflicts message forced", + sub_message='Another bucket message', + sub_error_count=1, + sub_info_count=0, + reason='Conflicts reason', + code=200) + responses.add(responses.POST, + 'http://shiptest/commitconfigdocs?force=true', + body=api_resp, + status=200) + response = CommitConfigdocs(stubs.StubCliContext(), + True).invoke_and_return_resp() + assert 'Status: Conflicts message forced' in response + assert 'Configuration documents committed' in response + assert 'Reason: Conflicts reason' in response diff --git a/shipyard_client/tests/unit/cli/control/test_control_actions.py b/shipyard_client/tests/unit/cli/control/test_control_actions.py index 0078cba4..e11d41da 100644 --- a/shipyard_client/tests/unit/cli/control/test_control_actions.py +++ b/shipyard_client/tests/unit/cli/control/test_control_actions.py @@ -11,59 +11,109 @@ # 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 mock -from shipyard_client.cli.control.actions import Control +import responses + from shipyard_client.api_client.base_client import BaseClient -from shipyard_client.tests.unit.cli.replace_api_client import \ - replace_base_constructor, replace_post_rep, replace_get_resp, \ - replace_output_formatting -from shipyard_client.tests.unit.cli.utils import temporary_context -from shipyard_client.api_client.shipyardclient_context import \ - ShipyardClientContext - -auth_vars = { - 'project_domain_name': 'projDomainTest', - 'user_domain_name': 'userDomainTest', - 'project_name': 'projectTest', - 'username': 'usernameTest', - 'password': 'passwordTest', - 'auth_url': 'urlTest' -} - -api_parameters = { - 'auth_vars': auth_vars, - 'context_marker': 'UUID', - 'debug': False -} +from shipyard_client.cli.control.actions import Control +from shipyard_client.tests.unit.cli import stubs -class MockCTX(): - pass - - -ctx = MockCTX() -ctx.obj = {} -ctx.obj['API_PARAMETERS'] = api_parameters -ctx.obj['FORMAT'] = 'format' - - -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.control.actions.output_formatting', - side_effect=replace_output_formatting) +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') def test_Control(*args): + responses.add( + responses.POST, + 'http://shiptest/actions/01BTG32JW87G0YKA1K29TKNAFX/control/pause', + body=None, + status=202 + ) control_verb = 'pause' id = '01BTG32JW87G0YKA1K29TKNAFX' - response = Control(ctx, control_verb, id).invoke_and_return_resp() + response = Control(stubs.StubCliContext(), + control_verb, + id).invoke_and_return_resp() # test correct function was called - url = response.get('url') - assert 'control' in url + assert response == ('pause successfully submitted for action' + ' 01BTG32JW87G0YKA1K29TKNAFX') - # test function was called with correct parameters - assert control_verb in url - assert id in url + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_control_unpause(*args): + responses.add( + responses.POST, + 'http://shiptest/actions/01BTG32JW87G0YKA1K29TKNAFX/control/unpause', + body=None, + status=202 + ) + control_verb = 'unpause' + id = '01BTG32JW87G0YKA1K29TKNAFX' + response = Control(stubs.StubCliContext(), + control_verb, + id).invoke_and_return_resp() + # test correct function was called + assert response == ('unpause successfully submitted for action' + ' 01BTG32JW87G0YKA1K29TKNAFX') + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_control_stop(*args): + responses.add( + responses.POST, + 'http://shiptest/actions/01BTG32JW87G0YKA1K29TKNAFX/control/stop', + body=None, + status=202 + ) + control_verb = 'stop' + id = '01BTG32JW87G0YKA1K29TKNAFX' + response = Control(stubs.StubCliContext(), + control_verb, + id).invoke_and_return_resp() + # test correct function was called + assert response == ('stop successfully submitted for action' + ' 01BTG32JW87G0YKA1K29TKNAFX') + + +resp_body = """ +{ + "message": "Unable to pause action", + "details": { + "messageList": [ + { + "message": "Conflicting things", + "error": true + }, + { + "message": "Try soup", + "error": false + } + ] + }, + "reason": "Conflicts" +} +""" + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_control_409(*args): + responses.add( + responses.POST, + 'http://shiptest/actions/01BTG32JW87G0YKA1K29TKNAFX/control/pause', + body=resp_body, + status=409 + ) + control_verb = 'pause' + id = '01BTG32JW87G0YKA1K29TKNAFX' + response = Control(stubs.StubCliContext(), + control_verb, + id).invoke_and_return_resp() + # test correct function was called + assert 'Unable to pause action' in response diff --git a/shipyard_client/tests/unit/cli/create/test_create_actions.py b/shipyard_client/tests/unit/cli/create/test_create_actions.py index 7f4c0bb0..f478fa0c 100644 --- a/shipyard_client/tests/unit/cli/create/test_create_actions.py +++ b/shipyard_client/tests/unit/cli/create/test_create_actions.py @@ -15,81 +15,156 @@ import mock import yaml -from shipyard_client.cli.create.actions import CreateAction, CreateConfigdocs +import responses + from shipyard_client.api_client.base_client import BaseClient -from shipyard_client.tests.unit.cli.replace_api_client import \ - replace_base_constructor, replace_post_rep, replace_get_resp, \ - replace_output_formatting -from shipyard_client.tests.unit.cli.utils import temporary_context -from shipyard_client.api_client.shipyardclient_context import \ - ShipyardClientContext +from shipyard_client.cli.create.actions import CreateAction +from shipyard_client.cli.create.actions import CreateConfigdocs +from shipyard_client.tests.unit.cli import stubs -auth_vars = { - 'project_domain_name': 'projDomainTest', - 'user_domain_name': 'userDomainTest', - 'project_name': 'projectTest', - 'username': 'usernameTest', - 'password': 'passwordTest', - 'auth_url': 'urlTest' -} - -api_parameters = { - 'auth_vars': auth_vars, - 'context_marker': 'UUID', - 'debug': False +resp_body = """ +{ + "dag_status": "SCHEDULED", + "parameters": {}, + "dag_execution_date": "2017-09-24T19:05:49", + "id": "01BTTMFVDKZFRJM80FGD7J1AKN", + "dag_id": "deploy_site", + "name": "deploy_site", + "user": "shipyard", + "context_marker": "629f2ea2-c59d-46b9-8641-7367a91a7016", + "timestamp": "2017-09-24 19:05:43.603591" } +""" -class MockCTX(): - pass +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_action(*args): + responses.add(responses.POST, + 'http://shiptest/actions', + body=resp_body, + status=201) + response = CreateAction(stubs.StubCliContext(), + action_name='deploy_site', + param=None).invoke_and_return_resp() + assert 'Name' in response + assert 'Action' in response + assert 'Lifecycle' in response + assert 'action/01BTTMFVDKZFRJM80FGD7J1AKN' in response + assert 'Error:' not in response -ctx = MockCTX() -ctx.obj = {} -ctx.obj['API_PARAMETERS'] = api_parameters -ctx.obj['FORMAT'] = 'format' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_action_400(*args): + responses.add(responses.POST, + 'http://shiptest/actions', + body=stubs.gen_err_resp(message='Error_400', + reason='bad action'), + status=400) + response = CreateAction(stubs.StubCliContext(), + action_name='deploy_dogs', + param=None).invoke_and_return_resp() + assert 'Error_400' in response + assert 'bad action' in response + assert 'action/01BTTMFVDKZFRJM80FGD7J1AKN' not in response -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.create.actions.output_formatting', - side_effect=replace_output_formatting) -def test_CreateAction(*args): - action_name = 'redeploy_server' - param = {'server-name': 'mcp'} - response = CreateAction(ctx, action_name, param).invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'actions' in url - # test function was called with correct parameters - data = response.get('data') - assert '"name": "redeploy_server"' in data - assert '"parameters": {"server-name": "mcp"}' in data +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_action_409(*args): + responses.add(responses.POST, + 'http://shiptest/actions', + body=stubs.gen_err_resp(message='Error_409', + reason='bad validations'), + status=409) + response = CreateAction(stubs.StubCliContext(), + action_name='deploy_site', + param=None).invoke_and_return_resp() + assert 'Error_409' in response + assert 'bad validations' in response + assert 'action/01BTTMFVDKZFRJM80FGD7J1AKN' not in response -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.create.actions.output_formatting', - side_effect=replace_output_formatting) -def test_CreateConfigdocs(*args): - collection = 'design' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_configdocs(*args): + succ_resp = stubs.gen_err_resp(message='Validations succeeded', + sub_error_count=0, + sub_info_count=0, + reason='Validation', + code=200) + responses.add(responses.POST, + 'http://shiptest/configdocs/design', + body=succ_resp, + status=201) + filename = 'shipyard_client/tests/unit/cli/create/sample_yaml/sample.yaml' document_data = yaml.dump_all(filename) - buffer = 'append' - response = CreateConfigdocs(ctx, collection, buffer, + + response = CreateConfigdocs(stubs.StubCliContext(), + 'design', + 'append', document_data).invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'configdocs' in url - # test function was called with correct parameters - assert collection in url - data = response.get('data') - assert document_data in data - params = response.get('params') - assert params.get('buffermode') == buffer + assert 'Configuration documents added.' + assert 'Status: Validations succeeded' in response + assert 'Reason: Validation' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_configdocs_201_with_val_fails(*args): + succ_resp = stubs.gen_err_resp(message='Validations failed', + sub_message='Some reason', + sub_error_count=2, + sub_info_count=1, + reason='Validation', + code=400) + responses.add(responses.POST, + 'http://shiptest/configdocs/design', + body=succ_resp, + status=201) + + filename = 'shipyard_client/tests/unit/cli/create/sample_yaml/sample.yaml' + document_data = yaml.dump_all(filename) + + response = CreateConfigdocs(stubs.StubCliContext(), + 'design', + 'append', + document_data).invoke_and_return_resp() + assert 'Configuration documents added.' in response + assert 'Status: Validations failed' in response + assert 'Reason: Validation' in response + assert 'Some reason-1' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_create_configdocs_409(*args): + err_resp = stubs.gen_err_resp(message='Invalid collection', + sub_message='Buffer is either not...', + sub_error_count=1, + sub_info_count=0, + reason='Buffermode : append', + code=409) + responses.add(responses.POST, + 'http://shiptest/configdocs/design', + body=err_resp, + status=409) + + filename = 'shipyard_client/tests/unit/cli/create/sample_yaml/sample.yaml' + document_data = yaml.dump_all(filename) + + response = CreateConfigdocs(stubs.StubCliContext(), + 'design', + 'append', + document_data).invoke_and_return_resp() + assert 'Error: Invalid collection' in response + assert 'Reason: Buffermode : append' in response + assert 'Buffer is either not...' in response diff --git a/shipyard_client/tests/unit/cli/describe/test_describe_actions.py b/shipyard_client/tests/unit/cli/describe/test_describe_actions.py index 8606b992..cf9d7800 100644 --- a/shipyard_client/tests/unit/cli/describe/test_describe_actions.py +++ b/shipyard_client/tests/unit/cli/describe/test_describe_actions.py @@ -11,106 +11,317 @@ # 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 mock -from shipyard_client.cli.describe.actions import \ - DescribeAction, DescribeStep, DescribeValidation, DescribeWorkflow +import responses + from shipyard_client.api_client.base_client import BaseClient -from shipyard_client.tests.unit.cli.replace_api_client import \ - replace_base_constructor, replace_post_rep, replace_get_resp, \ - replace_output_formatting -from shipyard_client.tests.unit.cli.utils import temporary_context -from shipyard_client.api_client.shipyardclient_context import \ - ShipyardClientContext +from shipyard_client.cli.describe.actions import DescribeAction +from shipyard_client.cli.describe.actions import DescribeStep +from shipyard_client.cli.describe.actions import DescribeValidation +from shipyard_client.cli.describe.actions import DescribeWorkflow +from shipyard_client.tests.unit.cli import stubs -auth_vars = { - 'project_domain_name': 'projDomainTest', - 'user_domain_name': 'userDomainTest', - 'project_name': 'projectTest', - 'username': 'usernameTest', - 'password': 'passwordTest', - 'auth_url': 'urlTest' -} - -api_parameters = { - 'auth_vars': auth_vars, - 'context_marker': 'UUID', - 'debug': False + +GET_ACTION_API_RESP = """ +{ + "name": "deploy_site", + "dag_execution_date": "2017-09-24T19:05:49", + "validations": [], + "id": "01BTTMFVDKZFRJM80FGD7J1AKN", + "dag_id": "deploy_site", + "command_audit": [ + { + "id": "01BTTMG16R9H3Z4JVQNBMRV1MZ", + "action_id": "01BTTMFVDKZFRJM80FGD7J1AKN", + "datetime": "2017-09-24 19:05:49.530223+00:00", + "user": "shipyard", + "command": "invoke" + } + ], + "user": "shipyard", + "context_marker": "629f2ea2-c59d-46b9-8641-7367a91a7016", + "datetime": "2017-09-24 19:05:43.603591+00:00", + "dag_status": "failed", + "parameters": {}, + "steps": [ + { + "id": "action_xcom", + "url": "/actions/01BTTMFVDKZFRJM80FGD7J1AKN/steps/action_xcom", + "index": 1, + "state": "success" + } + ], + "action_lifecycle": "Failed" } +""" -class MockCTX(): - pass +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_action(*args): + responses.add(responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN', + body=GET_ACTION_API_RESP, + status=200) - -ctx = MockCTX() -ctx.obj = {} -ctx.obj['API_PARAMETERS'] = api_parameters -ctx.obj['FORMAT'] = 'format' - - -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.describe.actions.output_formatting', - side_effect=replace_output_formatting) -def test_DescribeAction(*args): response = DescribeAction( - ctx, '01BTG32JW87G0YKA1K29TKNAFX').invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'actions/01BTG32JW87G0YKA1K29TKNAFX' in url - - -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.describe.actions.output_formatting', - side_effect=replace_output_formatting) -def test_DescribeStep(*args): - response = DescribeStep(ctx, '01BTG32JW87G0YKA1K29TKNAFX', - 'preflight').invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'actions/01BTG32JW87G0YKA1K29TKNAFX/steps/preflight' in url - - -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.describe.actions.output_formatting', - side_effect=replace_output_formatting) -def test_DescribeValidation(*args): - response = DescribeValidation( - ctx, '01BTG32JW87G0YKA1K29TKNAFX', - '01BTG3PKBS15KCKFZ56XXXBGF2').invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'actions' in url - assert '01BTG32JW87G0YKA1K29TKNAFX' in url - assert 'validationdetails' in url - assert '01BTG3PKBS15KCKFZ56XXXBGF2' in url - - -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.describe.actions.output_formatting', - side_effect=replace_output_formatting) -def test_DescribeWorkflow(*args): - response = DescribeWorkflow( - ctx, 'deploy_site__2017-01-01T12:34:56.123456' + stubs.StubCliContext(), + '01BTTMFVDKZFRJM80FGD7J1AKN' ).invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'workflows' in url - assert 'deploy_site__2017-01-01T12:34:56.123456' in url + assert 'action/01BTTMFVDKZFRJM80FGD7J1AKN' in response + assert 'step/01BTTMFVDKZFRJM80FGD7J1AKN/action_xcom' in response + assert 'Steps' in response + assert 'Commands' in response + assert 'Validations:' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_action_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + responses.add(responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN', + body=api_resp, + status=404) + + response = DescribeAction( + stubs.StubCliContext(), + '01BTTMFVDKZFRJM80FGD7J1AKN' + ).invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response + + +GET_STEP_API_RESP = """ +{ + "end_date": "2017-09-24 19:05:59.446213", + "duration": 0.165181, + "queued_dttm": "2017-09-24 19:05:52.993983", + "operator": "PythonOperator", + "try_number": 1, + "task_id": "preflight", + "state": "success", + "execution_date": "2017-09-24 19:05:49", + "dag_id": "deploy_site", + "index": 1, + "start_date": "2017-09-24 19:05:59.281032" +} +""" + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_step(*args): + responses.add( + responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN/steps/preflight', + body=GET_STEP_API_RESP, + status=200) + + response = DescribeStep(stubs.StubCliContext(), + '01BTTMFVDKZFRJM80FGD7J1AKN', + 'preflight').invoke_and_return_resp() + assert 'step/01BTTMFVDKZFRJM80FGD7J1AKN/preflight' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_step_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + responses.add( + responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN/steps/preflight', + body=api_resp, + status=404) + + response = DescribeStep(stubs.StubCliContext(), + '01BTTMFVDKZFRJM80FGD7J1AKN', + 'preflight').invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response + + +GET_VALIDATION_API_RESP = """ +{ + "validation_name": "validation_1", + "action_id": "01BTTMFVDKZFRJM80FGD7J1AKN", + "id": "02AURNEWAAAESKN99EBF8J2BHD", + "details": "Validations failed for field 'abc'" +} +""" + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_validation(*args): + responses.add( + responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN/' + 'validationdetails/02AURNEWAAAESKN99EBF8J2BHD', + body=GET_VALIDATION_API_RESP, + status=200) + + response = DescribeValidation( + stubs.StubCliContext(), + action_id='01BTTMFVDKZFRJM80FGD7J1AKN', + validation_id='02AURNEWAAAESKN99EBF8J2BHD').invoke_and_return_resp() + + v_str = "validation/01BTTMFVDKZFRJM80FGD7J1AKN/02AURNEWAAAESKN99EBF8J2BHD" + assert v_str in response + assert "Validations failed for field 'abc'" in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_validation_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + responses.add( + responses.GET, + 'http://shiptest/actions/01BTTMFVDKZFRJM80FGD7J1AKN/' + 'validationdetails/02AURNEWAAAESKN99EBF8J2BHD', + body=api_resp, + status=404) + + response = DescribeValidation( + stubs.StubCliContext(), + action_id='01BTTMFVDKZFRJM80FGD7J1AKN', + validation_id='02AURNEWAAAESKN99EBF8J2BHD').invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response + + +WF_API_RESP = """ +{ + "execution_date": "2017-10-09 21:19:03", + "end_date": null, + "workflow_id": "deploy_site__2017-10-09T21:19:03.000000", + "start_date": "2017-10-09 21:19:03.361522", + "external_trigger": true, + "steps": [ + { + "end_date": "2017-10-09 21:19:14.916220", + "task_id": "action_xcom", + "start_date": "2017-10-09 21:19:14.798053", + "duration": 0.118167, + "queued_dttm": "2017-10-09 21:19:08.432582", + "try_number": 1, + "state": "success", + "operator": "PythonOperator", + "dag_id": "deploy_site", + "execution_date": "2017-10-09 21:19:03" + }, + { + "end_date": "2017-10-09 21:19:25.283785", + "task_id": "dag_concurrency_check", + "start_date": "2017-10-09 21:19:25.181492", + "duration": 0.102293, + "queued_dttm": "2017-10-09 21:19:19.283132", + "try_number": 1, + "state": "success", + "operator": "ConcurrencyCheckOperator", + "dag_id": "deploy_site", + "execution_date": "2017-10-09 21:19:03" + }, + { + "end_date": "2017-10-09 21:20:05.394677", + "task_id": "preflight", + "start_date": "2017-10-09 21:19:34.994775", + "duration": 30.399902, + "queued_dttm": "2017-10-09 21:19:28.449848", + "try_number": 1, + "state": "failed", + "operator": "SubDagOperator", + "dag_id": "deploy_site", + "execution_date": "2017-10-09 21:19:03" + } + ], + "dag_id": "deploy_site", + "state": "failed", + "run_id": "manual__2017-10-09T21:19:03", + "sub_dags": [ + { + "execution_date": "2017-10-09 21:19:03", + "end_date": null, + "workflow_id": "deploy_site.preflight__2017-10-09T21:19:03.000000", + "start_date": "2017-10-09 21:19:35.082479", + "external_trigger": false, + "dag_id": "deploy_site.preflight", + "state": "failed", + "run_id": "backfill_2017-10-09T21:19:03" + }, + { + "execution_date": "2017-10-09 21:19:03", + "end_date": null, + "workflow_id": "deploy_site.postflight__2017-10-09T21:19:03.000000", + "start_date": "2017-10-09 21:19:35.082479", + "external_trigger": false, + "dag_id": "deploy_site.postflight", + "state": "failed", + "run_id": "backfill_2017-10-09T21:19:03" + } + ] +} +""" + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_workflow(*args): + responses.add( + responses.GET, + 'http://shiptest/workflows/deploy_site__2017-10-09T21:19:03.000000', + body=WF_API_RESP, + status=200) + + response = DescribeWorkflow( + stubs.StubCliContext(), + 'deploy_site__2017-10-09T21:19:03.000000' + ).invoke_and_return_resp() + assert 'deploy_site__2017-10-09T21:19:03.000000' in response + assert 'deploy_site.preflight__2017-10-09T21:19:03.000000' in response + assert 'deploy_site.postflight__2017-10-09T21:19:03.000000' in response + assert 'dag_concurrency_check' in response + assert 'Subworkflows:' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_describe_workflow_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + responses.add( + responses.GET, + 'http://shiptest/workflows/deploy_site__2017-10-09T21:19:03.000000', + body=api_resp, + status=404) + + response = DescribeWorkflow( + stubs.StubCliContext(), + 'deploy_site__2017-10-09T21:19:03.000000' + ).invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response diff --git a/shipyard_client/tests/unit/cli/get/test_get_actions.py b/shipyard_client/tests/unit/cli/get/test_get_actions.py index c5ca333d..44ba5a6f 100644 --- a/shipyard_client/tests/unit/cli/get/test_get_actions.py +++ b/shipyard_client/tests/unit/cli/get/test_get_actions.py @@ -1,5 +1,4 @@ -# Copyright 2017 AT&T Intellectual Property. replace_shipyard All other rights -# reserved. +# 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. @@ -12,113 +11,218 @@ # 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 mock -from shipyard_client.cli.get.actions import GetActions, GetConfigdocs, \ - GetRenderedConfigdocs, GetWorkflows +import responses + from shipyard_client.api_client.base_client import BaseClient -from shipyard_client.tests.unit.cli.replace_api_client import \ - replace_base_constructor, replace_post_rep, replace_get_resp, \ - replace_output_formatting -from shipyard_client.tests.unit.cli.utils import temporary_context -from shipyard_client.api_client.shipyardclient_context import \ - ShipyardClientContext +from shipyard_client.cli.get.actions import GetActions +from shipyard_client.cli.get.actions import GetConfigdocs +from shipyard_client.cli.get.actions import GetRenderedConfigdocs +from shipyard_client.cli.get.actions import GetWorkflows +from shipyard_client.tests.unit.cli import stubs -auth_vars = { - 'project_domain_name': 'projDomainTest', - 'user_domain_name': 'userDomainTest', - 'project_name': 'projectTest', - 'username': 'usernameTest', - 'password': 'passwordTest', - 'auth_url': 'urlTest' -} -api_parameters = { - 'auth_vars': auth_vars, - 'context_marker': '88888888-4444-4444-4444-121212121212', - 'debug': False -} +GET_ACTIONS_API_RESP = """ +[ + { + "dag_status": "failed", + "parameters": {}, + "steps": [ + { + "id": "action_xcom", + "url": "/actions/01BTP9T2WCE1PAJR2DWYXG805V/steps/action_xcom", + "index": 1, + "state": "success" + }, + { + "id": "concurrency_check", + "url": "/actions/01BTP9T2WCE1PAJR2DWYXG805V/steps/concurrency_check", + "index": 2, + "state": "success" + }, + { + "id": "preflight", + "url": "/actions/01BTP9T2WCE1PAJR2DWYXG805V/steps/preflight", + "index": 3, + "state": "failed" + } + ], + "action_lifecycle": "Failed", + "dag_execution_date": "2017-09-23T02:42:12", + "id": "01BTP9T2WCE1PAJR2DWYXG805V", + "dag_id": "deploy_site", + "datetime": "2017-09-23 02:42:06.860597+00:00", + "user": "shipyard", + "context_marker": "416dec4b-82f9-4339-8886-3a0c4982aec3", + "name": "deploy_site" + } +] +""" -class MockCTX(): - pass +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_actions(*args): + responses.add(responses.GET, + 'http://shiptest/actions', + body=GET_ACTIONS_API_RESP, + status=200) + response = GetActions(stubs.StubCliContext()).invoke_and_return_resp() + assert 'deploy_site' in response + assert 'action/01BTP9T2WCE1PAJR2DWYXG805V' in response + assert 'Lifecycle' in response -ctx = MockCTX() -ctx.obj = {} -ctx.obj['API_PARAMETERS'] = api_parameters -ctx.obj['FORMAT'] = 'format' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_actions_empty(*args): + responses.add(responses.GET, + 'http://shiptest/actions', + body="[]", + status=200) + response = GetActions(stubs.StubCliContext()).invoke_and_return_resp() + assert 'None' in response + assert 'Lifecycle' in response -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.get.actions.output_formatting', - side_effect=replace_output_formatting) -def test_GetActions(*args): - response = GetActions(ctx).invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'actions' in url - assert response.get('params') == {} +GET_CONFIGDOCS_API_RESP = """ +--- +yaml: yaml +--- +yaml2: yaml2 +... +""" -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.get.actions.output_formatting', - side_effect=replace_output_formatting) -def test_GetConfigdocs(*args): - response = GetConfigdocs(ctx, 'design', 'buffer').invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'configdocs/design' in url - params = response.get('params') - assert params.get('version') == 'buffer' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_configdocs(*args): + responses.add(responses.GET, + 'http://shiptest/configdocs/design?version=buffer', + body=GET_CONFIGDOCS_API_RESP, + status=200) + response = GetConfigdocs(stubs.StubCliContext(), + collection='design', + version='buffer').invoke_and_return_resp() + assert response == GET_CONFIGDOCS_API_RESP -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.get.actions.output_formatting', - side_effect=replace_output_formatting) -def test_GetRenderedConfigdocs(*args): - response = GetRenderedConfigdocs(ctx, 'buffer').invoke_and_return_resp() - # test correct function was called - url = response.get('url') - assert 'renderedconfigdocs' in url - params = response.get('params') - assert params.get('version') == 'buffer' +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_configdocs_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + + responses.add(responses.GET, + 'http://shiptest/configdocs/design?version=buffer', + body=api_resp, + status=404) + response = GetConfigdocs(stubs.StubCliContext(), + collection='design', + version='buffer').invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response -@mock.patch.object(BaseClient, '__init__', replace_base_constructor) -@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) -@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) -@mock.patch.object(ShipyardClientContext, '__init__', temporary_context) -@mock.patch( - 'shipyard_client.cli.get.actions.output_formatting', - side_effect=replace_output_formatting) -def test_GetWorkflows(*args): - response = GetWorkflows(ctx, since=None).invoke_and_return_resp() - url = response.get('url') - assert 'workflows' in url - assert 'since' not in url +GET_RENDEREDCONFIGDOCS_API_RESP = """ +--- +yaml: yaml +--- +yaml2: yaml2 +... +""" - response = GetWorkflows(ctx).invoke_and_return_resp() - url = response.get('url') - assert 'workflows' in url - assert 'since' not in url - since_val = '2017-01-01T12:34:56Z' - response = GetWorkflows(ctx, - since=since_val).invoke_and_return_resp() - url = response.get('url') - assert 'workflows' in url - params = response.get('params') - assert params.get('since') == since_val +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_renderedconfigdocs(*args): + responses.add(responses.GET, + 'http://shiptest/renderedconfigdocs?version=buffer', + body=GET_RENDEREDCONFIGDOCS_API_RESP, + status=200) + response = GetRenderedConfigdocs( + stubs.StubCliContext(), + version='buffer').invoke_and_return_resp() + assert response == GET_RENDEREDCONFIGDOCS_API_RESP + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_renderedconfigdocs_not_found(*args): + api_resp = stubs.gen_err_resp(message='Not Found', + sub_error_count=0, + sub_info_count=0, + reason='It does not exist', + code=404) + + responses.add(responses.GET, + 'http://shiptest/renderedconfigdocs?version=buffer', + body=api_resp, + status=404) + response = GetRenderedConfigdocs(stubs.StubCliContext(), + version='buffer').invoke_and_return_resp() + assert 'Error: Not Found' in response + assert 'Reason: It does not exist' in response + + +GET_WORKFLOWS_API_RESP = """ +[ + { + "execution_date": "2017-10-09 21:18:56", + "end_date": null, + "workflow_id": "deploy_site__2017-10-09T21:18:56.000000", + "start_date": "2017-10-09 21:18:56.685999", + "external_trigger": true, + "dag_id": "deploy_site", + "state": "failed", + "run_id": "manual__2017-10-09T21:18:56" + }, + { + "execution_date": "2017-10-09 21:19:03", + "end_date": null, + "workflow_id": "deploy_site__2017-10-09T21:19:03.000000", + "start_date": "2017-10-09 21:19:03.361522", + "external_trigger": true, + "dag_id": "deploy_site", + "state": "failed", + "run_id": "manual__2017-10-09T21:19:03" + } +] +""" + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_workflows(*args): + responses.add(responses.GET, + 'http://shiptest/workflows', + body=GET_WORKFLOWS_API_RESP, + status=200) + response = GetWorkflows(stubs.StubCliContext()).invoke_and_return_resp() + assert 'deploy_site__2017-10-09T21:19:03.000000' in response + assert 'deploy_site__2017-10-09T21:18:56.000000' in response + assert 'State' in response + assert 'Workflow' in response + + +@responses.activate +@mock.patch.object(BaseClient, 'get_endpoint', lambda x: 'http://shiptest') +@mock.patch.object(BaseClient, 'get_token', lambda x: 'abc') +def test_get_workflows_empty(*args): + responses.add(responses.GET, + 'http://shiptest/workflows', + body="[]", + status=200) + response = GetWorkflows(stubs.StubCliContext()).invoke_and_return_resp() + assert 'None' in response + assert 'State' in response diff --git a/shipyard_client/tests/unit/cli/replace_api_client.py b/shipyard_client/tests/unit/cli/replace_api_client.py index 38b32eff..0d72a82c 100644 --- a/shipyard_client/tests/unit/cli/replace_api_client.py +++ b/shipyard_client/tests/unit/cli/replace_api_client.py @@ -15,14 +15,9 @@ # For testing purposes only -class TemporaryContext(object): - def __init__(self): - self.debug = True - self.keystone_Auth = {} - self.token = 'abcdefgh' - self.service_type = 'http://shipyard' - self.shipyard_endpoint = 'http://shipyard/api/v1.0' - self.context_marker = '123456' +def replace_get_endpoint(): + """Replaces the get endpoint call to isolate tests""" + return 'http://shipyard-test' def replace_post_rep(self, url, query_params={}, data={}, content_type=''): @@ -39,11 +34,3 @@ def replace_get_resp(self, url, query_params={}, json=False): :returns: dict with url and parameters """ return {'url': url, 'params': query_params} - - -def replace_base_constructor(self, context): - pass - - -def replace_output_formatting(format, response): - return response diff --git a/shipyard_client/tests/unit/cli/stubs.py b/shipyard_client/tests/unit/cli/stubs.py new file mode 100644 index 00000000..c34ccad3 --- /dev/null +++ b/shipyard_client/tests/unit/cli/stubs.py @@ -0,0 +1,164 @@ +# 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 responses + +from shipyard_client.cli.action import CliAction +from shipyard_client.cli import format_utils + +DEFAULT_AUTH_VARS = { + 'project_domain_name': 'projDomainTest', + 'user_domain_name': 'userDomainTest', + 'project_name': 'projectTest', + 'username': 'usernameTest', + 'password': 'passwordTest', + 'auth_url': 'urlTest' +} + +DEFAULT_API_PARAMS = { + 'auth_vars': DEFAULT_AUTH_VARS, + 'context_marker': '88888888-4444-4444-4444-121212121212', + 'debug': False +} + +DEFAULT_BODY = """ +{ + "message": "Sample status response", + "details": { + "messageList": [ + { + "message": "Message1", + "error": false + }, + { + "message": "Message2", + "error": false + } + ] + }, + "reason": "Testing" +} +""" + +STATUS_TEMPL = """ +{{ + "kind": "Status", + "apiVersion": "v1", + "metadata": {{}}, + "status": "Valid", + "message": "{}", + "reason": "{}", + "details": {{ + "errorCount": {}, + "messageList": {} + }}, + "code": {} +}} +""" + +STATUS_MSG_TEMPL = """ +{{ + "message": "{}-{}", + "error": {} +}} +""" + + +def gen_err_resp(message='Err Message', + sub_message='Submessage', + sub_error_count=1, + sub_info_count=0, + reason='Reason Text', + code=400): + """Generates a fake status/error response for testing purposes""" + sub_messages = [] + for i in range(0, sub_error_count): + sub_messages.append(STATUS_MSG_TEMPL.format(sub_message, i, 'true')) + for i in range(0, sub_info_count): + sub_messages.append(STATUS_MSG_TEMPL.format(sub_message, i, 'false')) + msg_list = '[{}]'.format(','.join(sub_messages)) + resp_str = STATUS_TEMPL.format(message, + reason, + sub_error_count, + msg_list, + code) + return resp_str + + +def gen_api_param(auth_vars=None, + context_marker='88888888-4444-4444-4444-121212121212', + debug=False): + """Generates an object that is useful as input to a StubCliContext""" + if auth_vars is None: + auth_vars = DEFAULT_AUTH_VARS + return { + 'auth_vars': auth_vars, + 'context_marker': context_marker, + 'debug': debug + } + + +class StubCliContext(): + """A stub CLI context that can be configured for tests""" + def __init__(self, + fmt='cli', + api_parameters=None): + if api_parameters is None: + api_parameters = gen_api_param() + self.obj = {} + self.obj['API_PARAMETERS'] = api_parameters + self.obj['FORMAT'] = fmt + + +class StubAction(CliAction): + """A modifiable action that can be used to drive specific behaviors""" + def __init__(self, + ctx, + body=DEFAULT_BODY, + status_code=200, + method='GET'): + super().__init__(ctx) + self.body = body + self.status_code = status_code + self.method = method + + def invoke(self): + """Uses responses package to return a fake response""" + return responses.Response( + method=self.method, + url='http://shiptest/stub', + body=self.body, + status=self.status_code + ) + + # Handle 404 with default error handler for cli. + cli_handled_err_resp_codes = [400] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a formatted response + Handles 200 responses + """ + resp_j = response.json() + return format_utils.table_factory( + field_names=['Col1', 'Col2'], + rows=[ + ['message', resp_j.get('message')], + ['reason', resp_j.get('reason')] + ] + ) diff --git a/shipyard_client/tests/unit/cli/test_auth_validations.py b/shipyard_client/tests/unit/cli/test_auth_validations.py new file mode 100644 index 00000000..5a489ddc --- /dev/null +++ b/shipyard_client/tests/unit/cli/test_auth_validations.py @@ -0,0 +1,69 @@ +# 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 + +from shipyard_client.cli.action import AuthValuesError +from shipyard_client.tests.unit.cli import stubs + + +def test_validate_auth_vars_valid(): + action = stubs.StubAction(stubs.StubCliContext()) + try: + action.validate_auth_vars() + except AuthValuesError: + # Valid parameters should not raise an AuthValuesError + assert False + + +def test_validate_auth_vars_missing_required(): + auth_vars = { + 'project_domain_name': 'default', + 'user_domain_name': 'default', + 'project_name': 'service', + 'username': 'shipyard', + 'password': 'password', + 'auth_url': None + } + + param = stubs.gen_api_param(auth_vars=auth_vars) + action = stubs.StubAction(stubs.StubCliContext(api_parameters=param)) + with pytest.raises(AuthValuesError): + try: + action.validate_auth_vars() + except AuthValuesError as ex: + assert 'os_auth_url' in ex.diagnostic + assert 'os_username' not in ex.diagnostic + assert 'os_password' not in ex.diagnostic + raise + + +def test_validate_auth_vars_missing_required_and_others(): + auth_vars = { + 'project_domain_name': 'default', + 'user_domain_name': 'default', + 'project_name': 'service', + 'username': None, + 'password': 'password', + 'auth_url': None + } + param = stubs.gen_api_param(auth_vars=auth_vars) + action = stubs.StubAction(stubs.StubCliContext(api_parameters=param)) + with pytest.raises(AuthValuesError): + try: + action.validate_auth_vars() + except AuthValuesError as ex: + assert 'os_auth_url' in ex.diagnostic + assert 'os_username' in ex.diagnostic + assert 'os_password' not in ex.diagnostic + raise diff --git a/shipyard_client/tests/unit/cli/test_format_utils.py b/shipyard_client/tests/unit/cli/test_format_utils.py new file mode 100644 index 00000000..676c572a --- /dev/null +++ b/shipyard_client/tests/unit/cli/test_format_utils.py @@ -0,0 +1,152 @@ +# 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 json +from unittest.mock import MagicMock + +from prettytable.prettytable import DEFAULT + +import shipyard_client.cli.format_utils as format_utils + + +def test_cli_format_error_handler_bogus_json(): + """Tests the generic handler for shipyard error response if passed + unrecognized json + """ + resp = MagicMock() + resp.json = MagicMock(return_value=json.loads('{"key": "value"}')) + output = format_utils.cli_format_error_handler(resp) + assert 'Error: Not specified' in output + assert 'Reason: Not specified' in output + + +def test_cli_format_error_handler_broken_json(): + """Tests the generic handler for shipyard error response if passed + unrecognized json + """ + resp = MagicMock() + resp.json.side_effect = ValueError("") + resp.text = "Not JSON" + output = format_utils.cli_format_error_handler(resp) + assert 'Error: Unable to decode response. Value: Not JSON' in output + + +def test_cli_format_error_handler_no_messages(): + """Tests the generic handler for shipyard error response if passed + json in the right format, but with no messages + """ + resp_val = """ +{ + "apiVersion": "v1.0", + "status": "Failure", + "metadata": {}, + "message": "Unauthenticated", + "code": "401 Unauthorized", + "details": {}, + "kind": "status", + "reason": "Credentials are not established" +} +""" + resp = MagicMock() + resp.json = MagicMock(return_value=json.loads(resp_val)) + output = format_utils.cli_format_error_handler(resp) + print(output) + assert "Error: Unauthenticated" in output + assert "Reason: Credentials are not established" in output + + +def test_cli_format_error_handler_messages(): + """Tests the generic handler for shipyard error response if passed + a response with messages in the detail + """ + resp_val = """ +{ + "apiVersion": "v1.0", + "status": "Failure", + "metadata": {}, + "message": "Unauthenticated", + "code": "401 Unauthorized", + "details": { + "messageList": [ + { "message":"Hello1", "error": false }, + { "message":"Hello2", "error": false }, + { "message":"Hello3", "error": true } + ] + }, + "kind": "status", + "reason": "Credentials are not established" +} +""" + resp = MagicMock() + resp.json = MagicMock(return_value=json.loads(resp_val)) + output = format_utils.cli_format_error_handler(resp) + assert "Error: Unauthenticated" in output + assert "Reason: Credentials are not established" in output + assert "- Error: Hello3" in output + assert "- Info: Hello2" in output + + +def test_cli_format_error_handler_messages_broken(): + """Tests the generic handler for shipyard error response if passed + a response with messages in the detail, but missing error or message + elements + """ + resp_val = """ +{ + "apiVersion": "v1.0", + "status": "Failure", + "metadata": {}, + "message": "Unauthenticated", + "code": "401 Unauthorized", + "details": { + "messageList": [ + { "message":"Hello1", "error": false }, + { "error": true }, + { "message":"Hello3" } + ] + }, + "kind": "status", + "reason": "Credentials are not established" +} +""" + resp = MagicMock() + resp.json = MagicMock(return_value=json.loads(resp_val)) + output = format_utils.cli_format_error_handler(resp) + assert "Error: Unauthenticated" in output + assert "Reason: Credentials are not established" in output + assert "- Error: None" in output + assert "- Info: Hello3" in output + + +def test_table_factory(): + t = format_utils.table_factory() + assert t.get_string() == '' + + +def test_table_factory_fields(): + t = format_utils.table_factory(field_names=['a', 'b', 'c']) + t.add_row(['1', '2', '3']) + assert 'a' in t.get_string() + assert 'b' in t.get_string() + assert 'c' in t.get_string() + + +def test_table_factory_fields_data(): + t = format_utils.table_factory(style=DEFAULT, + field_names=['a', 'b', 'c'], + rows=[['1', '2', '3'], ['4', '5', '6']]) + assert 'a' in t.get_string() + assert 'b' in t.get_string() + assert 'c' in t.get_string() + assert '1' in t.get_string() + assert '6' in t.get_string() diff --git a/shipyard_client/tests/unit/cli/test_input_checks.py b/shipyard_client/tests/unit/cli/test_input_checks.py index 2a716e7b..290ee2fc 100644 --- a/shipyard_client/tests/unit/cli/test_input_checks.py +++ b/shipyard_client/tests/unit/cli/test_input_checks.py @@ -200,62 +200,6 @@ def test_check_action_commands_none(): assert 'call.fail(' in str(ctx.mock_calls[0]) -def test_validate_auth_vars_valid(): - ctx = Mock(side_effect=Exception("failed")) - auth_vars = { - 'project_domain_name': 'default', - 'user_domain_name': 'default', - 'project_name': 'service', - 'username': 'shipyard', - 'password': 'password', - 'auth_url': 'abcdefg' - } - input_checks.validate_auth_vars(ctx, auth_vars) - ctx.fail.assert_not_called() - - -def test_validate_auth_vars_missing_required(): - ctx = Mock(side_effect=Exception("failed")) - auth_vars = { - 'project_domain_name': 'default', - 'user_domain_name': 'default', - 'project_name': 'service', - 'username': 'shipyard', - 'password': 'password', - 'auth_url': None - } - try: - input_checks.validate_auth_vars(ctx, auth_vars) - except Exception: - pass - # py 3.6: ctx.fail.assert_called() - assert 'call.fail(' in str(ctx.mock_calls[0]) - assert 'os_auth_url' in str(ctx.mock_calls[0]) - assert 'os_username' not in str(ctx.mock_calls[0]) - assert 'os_password' not in str(ctx.mock_calls[0]) - - -def test_validate_auth_vars_missing_required_and_others(): - ctx = Mock(side_effect=Exception("failed")) - auth_vars = { - 'project_domain_name': 'default', - 'user_domain_name': 'default', - 'project_name': 'service', - 'username': None, - 'password': 'password', - 'auth_url': None - } - try: - input_checks.validate_auth_vars(ctx, auth_vars) - except Exception: - pass - # py 3.6: ctx.fail.assert_called() - assert 'call.fail(' in str(ctx.mock_calls[0]) - assert 'os_auth_url' in str(ctx.mock_calls[0]) - assert 'os_username' in str(ctx.mock_calls[0]) - assert 'os_password' not in str(ctx.mock_calls[0]) - - def test_check_reformat_parameter_valid(): ctx = Mock(side_effect=Exception("failed")) param = ['this=that'] diff --git a/shipyard_client/tests/unit/cli/test_output_formatting.py b/shipyard_client/tests/unit/cli/test_output_formatting.py deleted file mode 100644 index 8e613df1..00000000 --- a/shipyard_client/tests/unit/cli/test_output_formatting.py +++ /dev/null @@ -1,56 +0,0 @@ -# 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 json -import yaml -from mock import patch, ANY -from requests.models import Response - -from shipyard_client.cli.output_formatting import output_formatting - -json_response = Response() -json_response._content = b'{ "key" : "a" }' -json_response.status_code = 200 -json_response.headers['content-type'] = 'application/json' - -yaml_response = Response() -yaml_response._content = b'''Projects: - C/C++ Libraries: - - libyaml # "C" Fast YAML 1.1 - - Syck # (dated) "C" YAML 1.0 - - yaml-cpp # C++ YAML 1.2 implementation - Ruby: - - psych # libyaml wrapper (in Ruby core for 1.9.2) - - RbYaml # YAML 1.1 (PyYAML Port) - - yaml4r # YAML 1.0, standard library syck binding - Python: - - PyYAML # YAML 1.1, pure python and libyaml binding - - ruamel.yaml # YAML 1.2, update of PyYAML with round-tripping of comments - - PySyck # YAML 1.0, syck binding''' - -yaml_response.headers['content-type'] = 'application/yaml' - - -def test_output_formatting(): - """call output formatting and check correct one was given""" - - with patch.object(json, 'dumps') as mock_method: - output_formatting('format', json_response) - mock_method.assert_called_once_with( - json_response.json(), sort_keys=True, indent=4) - - with patch.object(yaml, 'dump_all') as mock_method: - output_formatting('format', yaml_response) - mock_method.assert_called_once_with( - ANY, width=79, indent=4, default_flow_style=False) diff --git a/test-requirements.txt b/test-requirements.txt index 799b505a..f1c8685b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,6 +2,7 @@ pytest==3.2.1 pytest-cov==2.5.1 mock==2.0.0 +responses==0.8.1 testfixtures==5.1.1 apache-airflow[crypto,celery,postgres,hive,hdfs,jdbc]==1.8.1