Add CLI formatted responses to Shipyard CLI

Rather than always returning JSON or YAML, add functionality to
return table responses when applicable for ease of reading.
Adds some marker exceptions to the Shipyard API Client to handle
cases where the user of the client would generally want to take a
specific and repeatable course of action instead of handling the
response in a case-by-case basis.

Moved cli action testing to use the responses library instead of
as many internal mocks. Added test response generators for standard
api responses.

Change-Id: I3a593fb29b6e76d971adc7f3bb3a4b7f378ed091
This commit is contained in:
Bryan Strassner 2017-11-15 18:29:50 -06:00
parent 4d0b16e1c9
commit b6d7af07fa
28 changed files with 2120 additions and 764 deletions

View File

@ -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

View File

@ -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

View File

@ -15,3 +15,11 @@
class ClientError(Exception):
pass
class UnauthorizedClientError(ClientError):
pass
class UnauthenticatedClientError(ClientError):
pass

View File

@ -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)

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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)
)

View File

@ -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)

View File

@ -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)
)

View File

@ -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', []))
)

View File

@ -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()

View File

@ -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)

View File

@ -11,6 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""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 <name>=<value> format"""
param_dictionary = {}

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')]
]
)

View File

@ -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

View File

@ -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()

View File

@ -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']

View File

@ -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)

View File

@ -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