Shipyard api client

This api client library provides access to the shipyard apis.

Change-Id: Id80af2643fbe4e97ec1a6376f2050c0dc10dd357
This commit is contained in:
Hassan Kaous 2017-10-04 15:25:10 -05:00
parent 6ee10408f8
commit a67028a8d2
11 changed files with 590 additions and 0 deletions

View File

View File

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
#
# 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 logging
import requests
from .client_error import ClientError
class BaseClient:
def __init__(self, context):
self.logger = logging.Logger('api_client')
self.context = context
def log_message(self, level, msg):
""" Logs a message with context, and extra populated. """
self.logger.log(level, msg)
def debug(self, msg):
""" Debug logger for resources, incorporating context. """
self.log_message(logging.DEBUG, msg)
def info(self, ctx, msg):
""" Info logger for resources, incorporating context. """
self.log_message(logging.INFO, msg)
def warn(self, msg):
""" Warn logger for resources, incorporating context. """
self.log_message(logging.WARN, msg)
def error(self, msg):
""" Error logger for resources, incorporating context. """
self.log_message(logging.ERROR, msg)
def post_resp(self,
url,
query_params=None,
data=None,
content_type='application/x-yaml'):
""" Thin wrapper of requests post """
if not query_params:
query_params = {}
if not data:
data = {}
try:
headers = {
'X-Context-Marker': self.context.context_marker,
'content-type': content_type,
'X-Auth-Token': self.context.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(
url, data=data, params=query_params, headers=headers)
except requests.exceptions.RequestException as e:
self.error(str(e))
raise ClientError(str(e))
def get_resp(self, url, query_params=None):
""" Thin wrapper of requests get """
if not query_params:
query_params = {}
try:
headers = {
'X-Context-Marker': self.context.context_marker,
'X-Auth-Token': self.context.get_token()
}
self.debug('url: ' + url)
self.debug('Query Params: ' + str(query_params))
return requests.get(url, params=query_params, headers=headers)
except requests.exceptions.RequestException as e:
self.error(str(e))
raise ClientError(str(e))

View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
#
# 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.
class ClientError(Exception):
pass

View File

@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
#
# 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 enum
import json
from .base_client import BaseClient
class ApiPaths(enum.Enum):
""" Enumeration of api paths.
This implementation assumes that the endpoint for shipyard
includes api/v1.0, so it is not repeated here.
"""
_BASE_URL = '{}/'
POST_GET_CONFIG = _BASE_URL + 'configdocs/{}'
GET_RENDERED = _BASE_URL + 'renderedconfigdocs'
COMMIT_CONFIG = _BASE_URL + 'commitconfigdocs'
POST_GET_ACTIONS = _BASE_URL + 'actions'
GET_ACTION_DETAIL = _BASE_URL + 'actions/{}'
GET_VALIDATION_DETAIL = _BASE_URL + 'actions/{}/validationdetails/{}'
GET_STEP_DETAIL = _BASE_URL + 'actions/{}/steps/{}'
POST_CONTROL_ACTION = _BASE_URL + 'actions/{}/control/{}'
GET_WORKFLOWS = _BASE_URL + 'workflows'
GET_DAG_DETAIL = _BASE_URL + 'workflows/{}'
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
def post_configdocs(self,
collection_id=None,
buffer_mode='rejectoncontents',
document_data=None):
"""
Ingests a collection of documents
:param str collection_id: identifies a collection of docs.Bucket_id
:param str buffermode: append|replace|rejectOnContents
:param str document_data: data in a format understood by Deckhand(YAML)
:returns: diff from last committed revision to new revision
:rtype: Response object
"""
query_params = {"buffermode": buffer_mode}
url = ApiPaths.POST_GET_CONFIG.value.format(self.shipyard_url,
collection_id)
return self.post_resp(url, query_params, document_data)
def get_configdocs(self, collection_id=None, version='buffer'):
"""
Get the collection of documents from deckhand specified by
collection id
:param collection_id: String, bucket_id in deckhand
:param version: String, committed|buffer
:rtype: Response object
"""
query_params = {"version": version}
url = ApiPaths.POST_GET_CONFIG.value.format(self.shipyard_url,
collection_id)
return self.get_resp(url, query_params)
def get_rendereddocs(self, version='buffer'):
"""
:param str version: committed|buffer
:returns: full set of configdocs in their rendered form.
:rtype: Response object
"""
query_params = {"version": version}
url = ApiPaths.GET_RENDERED.value.format(self.shipyard_url)
return self.get_resp(url, query_params)
def commit_configdocs(self, force=False):
"""
:param force: boolean, True|False
:returns: dictionary, validations from UCP components
:rtype: Response object
"""
query_params = {"force": force}
url = ApiPaths.COMMIT_CONFIG.value.format(self.shipyard_url)
return self.post_resp(url, query_params)
def get_actions(self):
"""
A list of actions that have been executed through shipyard's action API
:returns: lists all actions
:rtype: Response object
"""
url = ApiPaths.POST_GET_ACTIONS.value.format(self.shipyard_url)
return self.get_resp(url)
def post_actions(self, name=None, parameters=None):
"""
Creates an action in the system. This will cause some action to start.
:param str name: name of supported action to invoke
:param dict parameters: parameters to use for trigger invocation
:returns: action entity created successfully
:rtype: Response object
"""
action_data = {"name": name, "parameters": parameters}
url = ApiPaths.POST_GET_ACTIONS.value.format(self.shipyard_url)
return self.post_resp(url,
data=json.dumps(action_data),
content_type='application/json')
def get_action_detail(self, action_id=None):
"""
Used to get details about an action
:param str action_id: Unique ID for a particular action
:returns: information describing the action
:rtype: Response object
"""
url = ApiPaths.GET_ACTION_DETAIL.value.format(self.shipyard_url,
action_id)
return self.get_resp(url)
def get_validation_detail(self, action_id=None, validation_id=None):
"""
Allows for drilldown to validation detailed info.
:param str action_id: Unique action id
:param str validation_id: id of the validation
:returns: validation details about action
:rtype: Response object
"""
url = ApiPaths.GET_VALIDATION_DETAIL.value.format(
self.shipyard_url, action_id, validation_id)
return self.get_resp(url)
def get_step_detail(self, action_id=None, step_id=None):
"""
Allow for drilldown to step information
:param str action_id: Unique action id
:param str step_id: step id
: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)
return self.get_resp(url)
def post_control_action(self, action_id=None, control_verb=None):
"""
Allows for issuing DAG controls against an action.
:param str action_id: Unique action id
:param str control_verb: control action to be taken against an activity
:returns: containing the status of the action fail or success
:rtype: Response object
"""
url = ApiPaths.POST_CONTROL_ACTION.value.format(
self.shipyard_url, action_id, control_verb)
return self.post_resp(url)
def get_workflows(self, since=None):
"""
Queries airflow for DAGs that are running or have run
(successfully or unsuccessfully)
:param str since: iso8601 date optional
:returns: DAGS running or that have run
:rtype: Response object
"""
query_params = {'since': since}
url = ApiPaths.GET_WORKFLOWS.value.format(self.shipyard_url)
return self.get_resp(url, query_params)
def get_dag_detail(self, workflow_id=None):
"""
details of a particular scheduled DAG's output
:param str workflow_id: unique id for a DAG
:returns: details of a DAGs output
:rtype: Response object
"""
url = ApiPaths.GET_DAG_DETAIL.value.format(self.shipyard_url,
workflow_id)
return self.get_resp(url)

View File

@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
#
# 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 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:
def __init__(self, keystone_auth, context_marker, debug=False):
"""
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))

18
shipyard_client/setup.cfg Normal file
View File

@ -0,0 +1,18 @@
[metadata]
name = shipyard client
summary = Shipyard client library and CLI
description-file = README.md
author = undercloud team
home-page = https://github.com/att-comdev/shipyard/shipyard_client
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
[build_sphinx]
warning-is-error = True

View File

View File

View File

@ -0,0 +1,205 @@
# -*- coding: utf-8 -*-
#
# 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 mock
import json
from shipyard_client.api_client.shipyard_api_client import ShipyardClient
from shipyard_client.api_client.base_client import BaseClient
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_post_rep(self, url, query_params={}, data={}, content_type=''):
"""
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
: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()
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)
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)
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)
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)
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)
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)
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)
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)
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)
def test_get_actions(*args):
shipyard_client = get_api_client()
result = shipyard_client.get_actions()
assert result['url'] == '{}/actions'.format(
shipyard_client.shipyard_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)
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 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)
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)
@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)
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)
@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)
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)
@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)
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)
@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)
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)
@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)
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)