commit 640ee2a980c72b6f97fdd5aef1a66196b4c09272 Author: Scott Little Date: Tue Aug 7 11:51:16 2018 -0400 StarlingX open source release updates Signed-off-by: Scott Little diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..8de18a2 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = dcmanagerclient +omit = dcmanagerclient/openstack/* + +[report] +ignore_errors = True diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..963e589 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg* +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +cover/ +.coverage* +!.coveragerc +.tox +nosetests.xml +.testrepository +.venv + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +doc/build + +# pbr generates these +AUTHORS +ChangeLog + +# Editors +*~ +.*.swp +.*sw? + +# Files created by releasenotes build +releasenotes/build \ No newline at end of file diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..516ae6f --- /dev/null +++ b/.mailmap @@ -0,0 +1,3 @@ +# Format is: +# +# diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..4da4d7d --- /dev/null +++ b/.testr.conf @@ -0,0 +1,7 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ + OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ + OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ + ${PYTHON:-python} -m subunit.run discover $DISCOVER_DIRECTORY $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..5f31096 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,14 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: + + http://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + diff --git a/CONTRIBUTORS.wrs b/CONTRIBUTORS.wrs new file mode 100644 index 0000000..13fd8a8 --- /dev/null +++ b/CONTRIBUTORS.wrs @@ -0,0 +1,11 @@ +The following contributors from Wind River have developed the seed code in this +repository. We look forward to community collaboration and contributions for +additional features, enhancements and refactoring. +Contributors: +============= +Bart Wensley +Kevin Smith +Lachlan Plant +Saju Oommen +Tyler Smith + diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..3aa1eff --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +python-dcmanagerclient Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c978a52 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..4cbc43c --- /dev/null +++ b/README.rst @@ -0,0 +1,17 @@ +Dcmanagerclient +================ + +Wind River's Distributed Cloud system supports an edge computing solution by providing +central management and orchestration for a geographically distributed network of Titanium +Cloud systems. + +=============================== +python-dcmanagerclient +=============================== + +Python client for dcmanager + +This is a client library for Dcmanager built on the Dcmanager API. It +provides a Python API (the ``dcmanagerclient`` module) and a command-line tool +(``dcmanager``). + diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..15cd6cb --- /dev/null +++ b/babel.cfg @@ -0,0 +1,2 @@ +[python: **.py] + diff --git a/dcmanagerclient/__init__.py b/dcmanagerclient/__init__.py new file mode 100644 index 0000000..6c5e801 --- /dev/null +++ b/dcmanagerclient/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2016 Ericsson AB +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import pbr.version + + +__version__ = pbr.version.VersionInfo( + 'distributedcloud_client').version_string() diff --git a/dcmanagerclient/api/__init__.py b/dcmanagerclient/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dcmanagerclient/api/base.py b/dcmanagerclient/api/base.py new file mode 100644 index 0000000..a025c30 --- /dev/null +++ b/dcmanagerclient/api/base.py @@ -0,0 +1,112 @@ +# Copyright (c) 2016 Ericsson AB +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +from bs4 import BeautifulSoup +import json + +from dcmanagerclient import exceptions + + +class Resource(object): + # This will be overridden by the actual resource + resource_name = 'Something' + + +class ResourceManager(object): + resource_class = None + + def __init__(self, http_client): + self.http_client = http_client + + def _generate_resource(self, json_response_key): + json_objects = [json_response_key[item] for item in json_response_key] + resource = [] + for json_object in json_objects: + for resource_data in json_object: + resource.append(self.resource_class(self, resource_data, + json_object[resource_data])) + return resource + + def _list(self, url, response_key=None): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_response_key = get_json(resp) + resource = self._generate_resource(json_response_key) + return resource + + def _update(self, url, data): + data = json.dumps(data) + resp = self.http_client.put(url, data) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_response_key = get_json(resp) + result = self._generate_resource(json_response_key) + return result + + def _sync(self, url, data=None): + resp = self.http_client.put(url, data) + if resp.status_code != 200: + self._raise_api_exception(resp) + + def _detail(self, url): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_response_key = get_json(resp) + json_objects = [json_response_key[item] for item in json_response_key] + resource = [] + for json_object in json_objects: + data = json_object.get('usage').keys() + for values in data: + resource.append(self.resource_class(self, values, + json_object['limits'][values], + json_object['usage'][values])) + return resource + + def _delete(self, url): + resp = self.http_client.delete(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + + def _raise_api_exception(self, resp): + error_html = resp.content + soup = BeautifulSoup(error_html, 'html.parser') + # Get the raw html with get_text, strip out the blank lines on + # front and back, then get rid of the 2 lines of error code number + # and error code explanation so that we are left with just the + # meaningful error text. + try: + error_msg = soup.body.get_text().lstrip().rstrip().split('\n')[2] + except Exception: + error_msg = resp.content + + raise exceptions.APIException(error_code=resp.status_code, + error_message=error_msg) + + +def get_json(response): + """Get JSON representation of response.""" + json_field_or_function = getattr(response, 'json', None) + if callable(json_field_or_function): + return response.json() + else: + return json.loads(response.content) diff --git a/dcmanagerclient/api/client.py b/dcmanagerclient/api/client.py new file mode 100644 index 0000000..4e70d06 --- /dev/null +++ b/dcmanagerclient/api/client.py @@ -0,0 +1,62 @@ +# Copyright 2016 - Ericsson AB +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import six + +from dcmanagerclient.api.v1 import client as client_v1 + + +def client(dcmanager_url=None, username=None, api_key=None, + project_name=None, auth_url=None, project_id=None, + endpoint_type='publicURL', service_type='dcmanager', + auth_token=None, user_id=None, cacert=None, insecure=False, + profile=None, auth_type='keystone', client_id=None, + client_secret=None, session=None, **kwargs): + if dcmanager_url and not isinstance(dcmanager_url, six.string_types): + raise RuntimeError('DC Manager url should be a string.') + + return client_v1.Client( + dcmanager_url=dcmanager_url, + username=username, + api_key=api_key, + project_name=project_name, + auth_url=auth_url, + project_id=project_id, + endpoint_type=endpoint_type, + service_type=service_type, + auth_token=auth_token, + user_id=user_id, + cacert=cacert, + insecure=insecure, + profile=profile, + auth_type=auth_type, + client_id=client_id, + client_secret=client_secret, + session=session, + **kwargs + ) + + +def determine_client_version(dcmanager_version): + if dcmanager_version.find("v1.0") != -1: + return 1 + + raise RuntimeError("Cannot determine DC Manager API version") diff --git a/dcmanagerclient/api/httpclient.py b/dcmanagerclient/api/httpclient.py new file mode 100644 index 0000000..f68dadb --- /dev/null +++ b/dcmanagerclient/api/httpclient.py @@ -0,0 +1,128 @@ +# Copyright 2013 - Mirantis, Inc. +# Copyright 2016 - StackStorm, Inc. +# Copyright 2016 - Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import copy +import os + +import requests + +import logging + +import osprofiler.web + +LOG = logging.getLogger(__name__) + + +def log_request(func): + def decorator(self, *args, **kwargs): + resp = func(self, *args, **kwargs) + LOG.debug("HTTP %s %s %d" % (resp.request.method, resp.url, + resp.status_code)) + return resp + + return decorator + + +class HTTPClient(object): + def __init__(self, base_url, token=None, project_id=None, user_id=None, + cacert=None, insecure=False): + self.base_url = base_url + self.token = token + self.project_id = project_id + self.user_id = user_id + self.ssl_options = {} + + if self.base_url.startswith('https'): + if cacert and not os.path.exists(cacert): + raise ValueError('Unable to locate cacert file ' + 'at %s.' % cacert) + + if cacert and insecure: + LOG.warning('Client is set to not verify even though ' + 'cacert is provided.') + + self.ssl_options['verify'] = not insecure + self.ssl_options['cert'] = cacert + + @log_request + def get(self, url, headers=None): + options = self._get_request_options('get', headers) + + return requests.get(self.base_url + url, **options) + + @log_request + def post(self, url, body, headers=None): + options = self._get_request_options('post', headers) + + return requests.post(self.base_url + url, body, **options) + + @log_request + def put(self, url, body, headers=None): + options = self._get_request_options('put', headers) + + return requests.put(self.base_url + url, body, **options) + + @log_request + def patch(self, url, body, headers=None): + options = self._get_request_options('patch', headers) + + return requests.patch(self.base_url + url, body, **options) + + @log_request + def delete(self, url, headers=None): + options = self._get_request_options('delete', headers) + + return requests.delete(self.base_url + url, **options) + + def _get_request_options(self, method, headers): + headers = self._update_headers(headers) + + if method in ['post', 'put', 'patch']: + content_type = headers.get('content-type', 'application/json') + headers['content-type'] = content_type + + options = copy.deepcopy(self.ssl_options) + options['headers'] = headers + + return options + + def _update_headers(self, headers): + if not headers: + headers = {} + + token = headers.get('x-auth-token', self.token) + if token: + headers['x-auth-token'] = token + + project_id = headers.get('X-Project-Id', self.project_id) + if project_id: + headers['X-Project-Id'] = project_id + + user_id = headers.get('X-User-Id', self.user_id) + if user_id: + headers['X-User-Id'] = user_id + + # Add headers for osprofiler. + headers.update(osprofiler.web.get_trace_id_headers()) + + return headers diff --git a/dcmanagerclient/api/v1/__init__.py b/dcmanagerclient/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dcmanagerclient/api/v1/alarm_manager.py b/dcmanagerclient/api/v1/alarm_manager.py new file mode 100644 index 0000000..c7142cc --- /dev/null +++ b/dcmanagerclient/api/v1/alarm_manager.py @@ -0,0 +1,65 @@ +# Copyright (c) 2017 Ericsson AB. +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +from dcmanagerclient.api import base +from dcmanagerclient.api.base import get_json + + +class AlarmSumary(base.Resource): + resource_name = 'alarms' + + def __init__(self, manager, name, critical, major, + minor, warnings, status): + self.manger = manager + self.name = name + self.critical = critical + self.major = major + self.minor = minor + self.warnings = warnings + self.status = status + + +class alarm_manager(base.ResourceManager): + resource_class = AlarmSumary + + def alarm_summary_list(self, url): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_response_key = get_json(resp) + json_objects = json_response_key['alarm_summary'] + resource = [] + for json_object in json_objects: + resource.append( + self.resource_class( + self, + name=json_object['region_name'], + critical=json_object['critical_alarms'], + major=json_object['major_alarms'], + minor=json_object['minor_alarms'], + warnings=json_object['warnings'], + status=json_object['cloud_status'])) + return resource + + def list_alarms(self): + url = '/alarms/' + return self.alarm_summary_list(url) diff --git a/dcmanagerclient/api/v1/client.py b/dcmanagerclient/api/v1/client.py new file mode 100644 index 0000000..d789a77 --- /dev/null +++ b/dcmanagerclient/api/v1/client.py @@ -0,0 +1,166 @@ +# Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. +# Copyright 2016 - Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import keystoneauth1.identity.generic as auth_plugin +from keystoneauth1 import session as ks_session + +from dcmanagerclient.api import httpclient +from dcmanagerclient.api.v1 import alarm_manager as am +from dcmanagerclient.api.v1 import subcloud_manager as sm +from dcmanagerclient.api.v1 import sw_update_manager as sum +from dcmanagerclient.api.v1 import sw_update_options_manager as suom + + +import osprofiler.profiler + +import six + + +_DEFAULT_DCMANAGER_URL = "http://localhost:8119/v1.0" + + +class Client(object): + """Class where the communication from KB to Keystone happens.""" + + def __init__(self, dcmanager_url=None, username=None, api_key=None, + project_name=None, auth_url=None, project_id=None, + endpoint_type='publicURL', service_type='dcmanager', + auth_token=None, user_id=None, cacert=None, insecure=False, + profile=None, auth_type='keystone', client_id=None, + client_secret=None, session=None, **kwargs): + """DC Manager communicates with Keystone to fetch necessary values.""" + if dcmanager_url and not isinstance(dcmanager_url, six.string_types): + raise RuntimeError('DC Manager url should be a string.') + + if auth_url or session: + if auth_type == 'keystone': + (dcmanager_url, auth_token, project_id, user_id) = ( + authenticate( + dcmanager_url, + username, + api_key, + project_name, + auth_url, + project_id, + endpoint_type, + service_type, + auth_token, + user_id, + session, + cacert, + insecure, + **kwargs + ) + ) + else: + raise RuntimeError( + 'Invalid authentication type [value=%s, valid_values=%s]' + % (auth_type, 'keystone') + ) + + if not dcmanager_url: + dcmanager_url = _DEFAULT_DCMANAGER_URL + + if profile: + osprofiler.profiler.init(profile) + + self.http_client = httpclient.HTTPClient( + dcmanager_url, + auth_token, + project_id, + user_id, + cacert=cacert, + insecure=insecure + ) + + # Create all managers + self.subcloud_manager = sm.subcloud_manager(self.http_client) + self.alarm_manager = am.alarm_manager(self.http_client) + self.sw_update_manager = sum.sw_update_manager(self.http_client) + self.sw_update_options_manager = \ + suom.sw_update_options_manager(self.http_client) + self.strategy_step_manager = sum.strategy_step_manager( + self.http_client) + + +def authenticate(dcmanager_url=None, username=None, + api_key=None, project_name=None, auth_url=None, + project_id=None, endpoint_type='publicURL', + service_type='dcmanager', auth_token=None, user_id=None, + session=None, cacert=None, insecure=False, **kwargs): + """Get token, project_id, user_id and Endpoint.""" + if project_name and project_id: + raise RuntimeError( + 'Only project name or project id should be set' + ) + + if username and user_id: + raise RuntimeError( + 'Only user name or user id should be set' + ) + user_domain_name = kwargs.get('user_domain_name') + user_domain_id = kwargs.get('user_domain_id') + project_domain_name = kwargs.get('project_domain_name') + project_domain_id = kwargs.get('project_domain_id') + + if session is None: + if auth_token: + auth = auth_plugin.Token( + auth_url=auth_url, + token=auth_token, + project_id=project_id, + project_name=project_name, + project_domain_name=project_domain_name, + project_domain_id=project_domain_id, + cacert=cacert, + insecure=insecure) + + elif api_key and (username or user_id): + auth = auth_plugin.Password( + auth_url=auth_url, + username=username, + user_id=user_id, + password=api_key, + project_id=project_id, + project_name=project_name, + user_domain_name=user_domain_name, + user_domain_id=user_domain_id, + project_domain_name=project_domain_name, + project_domain_id=project_domain_id) + + else: + raise RuntimeError('You must either provide a valid token or' + 'a password (api_key) and a user.') + if auth: + session = ks_session.Session(auth=auth) + + if session: + token = session.get_token() + project_id = session.get_project_id() + user_id = session.get_user_id() + if not dcmanager_url: + dcmanager_url = session.get_endpoint( + service_type=service_type, + interface=endpoint_type) + + return dcmanager_url, token, project_id, user_id diff --git a/dcmanagerclient/api/v1/subcloud_manager.py b/dcmanagerclient/api/v1/subcloud_manager.py new file mode 100644 index 0000000..d7c87f9 --- /dev/null +++ b/dcmanagerclient/api/v1/subcloud_manager.py @@ -0,0 +1,204 @@ +# Copyright (c) 2017 Ericsson AB. +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import json + +from dcmanagerclient.api import base +from dcmanagerclient.api.base import get_json + + +class Subcloud(base.Resource): + resource_name = 'subclouds' + + def __init__(self, manager, subcloud_id, name, description, location, + software_version, management_state, availability_status, + management_subnet, management_start_ip, management_end_ip, + management_gateway_ip, systemcontroller_gateway_ip, + created_at, updated_at, sync_status="unknown", + endpoint_sync_status={}): + self.manager = manager + self.subcloud_id = subcloud_id + self.name = name + self.description = description + self.location = location + self.software_version = software_version + self.management_subnet = management_subnet + self.management_state = management_state + self.availability_status = availability_status + self.management_start_ip = management_start_ip + self.management_end_ip = management_end_ip + self.management_gateway_ip = management_gateway_ip + self.systemcontroller_gateway_ip = systemcontroller_gateway_ip + self.created_at = created_at + self.updated_at = updated_at + self.sync_status = sync_status + self.endpoint_sync_status = endpoint_sync_status + + +class subcloud_manager(base.ResourceManager): + resource_class = Subcloud + + def subcloud_create(self, url, data): + data = json.dumps(data) + resp = self.http_client.post(url, data) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + resource = list() + resource.append( + self.resource_class( + self, + subcloud_id=json_object['id'], + name=json_object['name'], + description=json_object['description'], + location=json_object['location'], + software_version=json_object['software-version'], + management_state=json_object['management-state'], + availability_status=json_object['availability-status'], + management_subnet=json_object['management-subnet'], + management_start_ip=json_object['management-start-ip'], + management_end_ip=json_object['management-end-ip'], + management_gateway_ip=json_object['management-gateway-ip'], + systemcontroller_gateway_ip=json_object[ + 'systemcontroller-gateway-ip'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'])) + return resource + + def subcloud_update(self, url, data): + data = json.dumps(data) + resp = self.http_client.patch(url, data) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + resource = list() + resource.append( + self.resource_class( + self, + subcloud_id=json_object['id'], + name=json_object['name'], + description=json_object['description'], + location=json_object['location'], + software_version=json_object['software-version'], + management_state=json_object['management-state'], + availability_status=json_object['availability-status'], + management_subnet=json_object['management-subnet'], + management_start_ip=json_object['management-start-ip'], + management_end_ip=json_object['management-end-ip'], + management_gateway_ip=json_object['management-gateway-ip'], + systemcontroller_gateway_ip=json_object[ + 'systemcontroller-gateway-ip'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'])) + return resource + + def subcloud_list(self, url): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_response_key = get_json(resp) + json_objects = json_response_key['subclouds'] + resource = [] + for json_object in json_objects: + resource.append( + self.resource_class( + self, + subcloud_id=json_object['id'], + name=json_object['name'], + description=json_object['description'], + location=json_object['location'], + software_version=json_object['software-version'], + management_state=json_object['management-state'], + availability_status=json_object['availability-status'], + management_subnet=json_object['management-subnet'], + management_start_ip=json_object['management-start-ip'], + management_end_ip=json_object['management-end-ip'], + management_gateway_ip=json_object['management-gateway-ip'], + systemcontroller_gateway_ip=json_object[ + 'systemcontroller-gateway-ip'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'], + sync_status=json_object['sync_status'], + endpoint_sync_status=json_object['endpoint_sync_status'])) + return resource + + def _subcloud_detail(self, url): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + resource = list() + resource.append( + self.resource_class( + self, + subcloud_id=json_object['id'], + name=json_object['name'], + description=json_object['description'], + location=json_object['location'], + software_version=json_object['software-version'], + management_state=json_object['management-state'], + availability_status=json_object['availability-status'], + management_subnet=json_object['management-subnet'], + management_start_ip=json_object['management-start-ip'], + management_end_ip=json_object['management-end-ip'], + management_gateway_ip=json_object['management-gateway-ip'], + systemcontroller_gateway_ip=json_object[ + 'systemcontroller-gateway-ip'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'], + endpoint_sync_status=json_object['endpoint_sync_status'])) + return resource + + def subcloud_generate_config(self, url, data): + data = json.dumps(data) + resp = self.http_client.post(url, data) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + return json_object['config'] + + def add_subcloud(self, **kwargs): + data = kwargs + url = '/subclouds/' + return self.subcloud_create(url, data) + + def list_subclouds(self): + url = '/subclouds/' + return self.subcloud_list(url) + + def subcloud_detail(self, subcloud_ref): + url = '/subclouds/%s' % subcloud_ref + return self._subcloud_detail(url) + + def delete_subcloud(self, subcloud_ref): + url = '/subclouds/%s' % subcloud_ref + return self._delete(url) + + def update_subcloud(self, subcloud_ref, **kwargs): + data = kwargs + url = '/subclouds/%s' % subcloud_ref + return self.subcloud_update(url, data) + + def generate_config_subcloud(self, subcloud_ref, **kwargs): + data = kwargs + url = '/subclouds/%s/config' % subcloud_ref + return self.subcloud_generate_config(url, data) diff --git a/dcmanagerclient/api/v1/sw_update_manager.py b/dcmanagerclient/api/v1/sw_update_manager.py new file mode 100644 index 0000000..e5f1364 --- /dev/null +++ b/dcmanagerclient/api/v1/sw_update_manager.py @@ -0,0 +1,209 @@ +# Copyright (c) 2017 Ericsson AB. +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import json + +from dcmanagerclient.api import base +from dcmanagerclient.api.base import get_json + + +class SwUpdateStrategy(base.Resource): + resource_name = 'sw_update_strategy' + + def __init__(self, manager, subcloud_apply_type, max_parallel_subclouds, + stop_on_failure, state, + created_at, updated_at): + self.manager = manager + self.subcloud_apply_type = subcloud_apply_type + self.max_parallel_subclouds = max_parallel_subclouds + self.stop_on_failure = stop_on_failure + self.state = state + self.created_at = created_at + self.updated_at = updated_at + + +class StrategyStep(base.Resource): + resource_name = 'strategy_step' + + def __init__(self, manager, cloud, stage, state, details, + started_at, finished_at, created_at, updated_at): + self.manager = manager + self.cloud = cloud + self.stage = stage + self.state = state + self.details = details + self.started_at = started_at + self.finished_at = finished_at + self.created_at = created_at + self.updated_at = updated_at + + +class sw_update_manager(base.ResourceManager): + resource_class = SwUpdateStrategy + + def create_patch_strategy(self, **kwargs): + data = kwargs + data.update({'type': 'patch'}) + url = '/sw-update-strategy/' + return self.sw_update_create(url, data) + + def patch_strategy_detail(self): + url = '/sw-update-strategy' + return self.sw_update_detail(url) + + def delete_patch_strategy(self): + url = '/sw-update-strategy' + return self.sw_update_delete(url) + + def apply_patch_strategy(self): + data = {'action': 'apply'} + url = '/sw-update-strategy/actions' + return self.sw_update_action(url, data) + + def abort_patch_strategy(self): + data = {'action': 'abort'} + url = '/sw-update-strategy/actions' + return self.sw_update_action(url, data) + + def sw_update_create(self, url, data): + data = json.dumps(data) + resp = self.http_client.post(url, data) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + resource = list() + resource.append( + self.resource_class( + self, + subcloud_apply_type=json_object['subcloud-apply-type'], + max_parallel_subclouds=json_object['max-parallel-subclouds'], + stop_on_failure=json_object['stop-on-failure'], + state=json_object['state'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'])) + return resource + + def sw_update_delete(self, url): + resp = self.http_client.delete(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + resource = list() + resource.append( + self.resource_class( + self, + subcloud_apply_type=json_object['subcloud-apply-type'], + max_parallel_subclouds=json_object['max-parallel-subclouds'], + stop_on_failure=json_object['stop-on-failure'], + state=json_object['state'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'])) + return resource + + def sw_update_detail(self, url): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + resource = list() + resource.append( + self.resource_class( + self, + subcloud_apply_type=json_object['subcloud-apply-type'], + max_parallel_subclouds=json_object['max-parallel-subclouds'], + stop_on_failure=json_object['stop-on-failure'], + state=json_object['state'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'])) + return resource + + def sw_update_action(self, url, data): + data = json.dumps(data) + resp = self.http_client.post(url, data) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + resource = list() + resource.append( + self.resource_class( + self, + subcloud_apply_type=json_object['subcloud-apply-type'], + max_parallel_subclouds=json_object['max-parallel-subclouds'], + stop_on_failure=json_object['stop-on-failure'], + state=json_object['state'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'])) + return resource + + +class strategy_step_manager(base.ResourceManager): + resource_class = StrategyStep + + def list_strategy_steps(self): + url = '/sw-update-strategy/steps' + return self.strategy_step_list(url) + + def strategy_step_detail(self, cloud_name): + url = '/sw-update-strategy/steps/%s' % cloud_name + return self._strategy_step_detail(url) + + def strategy_step_list(self, url): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_response_key = get_json(resp) + json_objects = json_response_key['strategy-steps'] + resource = [] + for json_object in json_objects: + resource.append( + self.resource_class( + self, + cloud=json_object['cloud'], + stage=json_object['stage'], + state=json_object['state'], + details=json_object['details'], + started_at=json_object['started-at'], + finished_at=json_object['finished-at'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'], + )) + return resource + + def _strategy_step_detail(self, url): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + resource = list() + resource.append( + self.resource_class( + self, + cloud=json_object['cloud'], + stage=json_object['stage'], + state=json_object['state'], + details=json_object['details'], + started_at=json_object['started-at'], + finished_at=json_object['finished-at'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'], + )) + return resource diff --git a/dcmanagerclient/api/v1/sw_update_options_manager.py b/dcmanagerclient/api/v1/sw_update_options_manager.py new file mode 100644 index 0000000..5ace811 --- /dev/null +++ b/dcmanagerclient/api/v1/sw_update_options_manager.py @@ -0,0 +1,143 @@ +# Copyright (c) 2017 Ericsson AB. +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import json + +from dcmanagerclient.api import base +from dcmanagerclient.api.base import get_json + +DEFAULT_REGION_NAME = "RegionOne" + + +class SwUpdateOptions(base.Resource): + resource_name = 'sw_update_options' + + def __init__(self, manager, cloud, storage_apply_type, compute_apply_type, + max_parallel_computes, alarm_restriction_type, + default_instance_action, + created_at, updated_at): + self.manager = manager + self.cloud = cloud + self.storage_apply_type = storage_apply_type + self.compute_apply_type = compute_apply_type + self.max_parallel_computes = max_parallel_computes + self.alarm_restriction_type = alarm_restriction_type + self.default_instance_action = default_instance_action + self.created_at = created_at + self.updated_at = updated_at + + +class sw_update_options_manager(base.ResourceManager): + resource_class = SwUpdateOptions + + def sw_update_options_update(self, subcloud_ref, **kwargs): + data = kwargs + if subcloud_ref: + url = '/sw-update-options/%s' % subcloud_ref + else: + url = '/sw-update-options/%s' % DEFAULT_REGION_NAME + return self._sw_update_options_update(url, data) + + def sw_update_options_list(self): + url = '/sw-update-options' + return self._sw_update_options_list(url) + + def sw_update_options_detail(self, subcloud_ref): + if subcloud_ref: + url = '/sw-update-options/%s' % subcloud_ref + else: + url = '/sw-update-options/%s' % DEFAULT_REGION_NAME + return self._sw_update_options_detail(url) + + def sw_update_options_delete(self, subcloud_ref): + if subcloud_ref: + url = '/sw-update-options/%s' % subcloud_ref + else: + url = '/sw-update-options/%s' % DEFAULT_REGION_NAME + return self._sw_update_options_delete(url) + + def _sw_update_options_detail(self, url): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + resource = list() + resource.append( + self.resource_class( + self, + cloud=json_object['name'], + storage_apply_type=json_object['storage-apply-type'], + compute_apply_type=json_object['compute-apply-type'], + max_parallel_computes=json_object['max-parallel-computes'], + alarm_restriction_type=json_object['alarm-restriction-type'], + default_instance_action=json_object['default-instance-action'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'])) + return resource + + def _sw_update_options_list(self, url): + resp = self.http_client.get(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_response_key = get_json(resp) + json_objects = json_response_key['sw-update-options'] + resource = [] + for json_object in json_objects: + resource.append( + self.resource_class( + self, + cloud=json_object['name'], + storage_apply_type=json_object['storage-apply-type'], + compute_apply_type=json_object['compute-apply-type'], + max_parallel_computes=json_object['max-parallel-computes'], + alarm_restriction_type=json_object[ + 'alarm-restriction-type'], + default_instance_action=json_object[ + 'default-instance-action'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'])) + return resource + + def _sw_update_options_delete(self, url): + resp = self.http_client.delete(url) + if resp.status_code != 200: + self._raise_api_exception(resp) + + def _sw_update_options_update(self, url, data): + data = json.dumps(data) + resp = self.http_client.post(url, data) + if resp.status_code != 200: + self._raise_api_exception(resp) + json_object = get_json(resp) + resource = list() + resource.append( + self.resource_class( + self, + cloud=json_object['name'], + storage_apply_type=json_object['storage-apply-type'], + compute_apply_type=json_object['compute-apply-type'], + max_parallel_computes=json_object['max-parallel-computes'], + alarm_restriction_type=json_object['alarm-restriction-type'], + default_instance_action=json_object['default-instance-action'], + created_at=json_object['created-at'], + updated_at=json_object['updated-at'])) + return resource diff --git a/dcmanagerclient/commands/README.rst b/dcmanagerclient/commands/README.rst new file mode 100644 index 0000000..569cba9 --- /dev/null +++ b/dcmanagerclient/commands/README.rst @@ -0,0 +1,5 @@ +=============================== +Commands +================================ + +This module helps in mapping dcmanager commands to APIs. diff --git a/dcmanagerclient/commands/__init__.py b/dcmanagerclient/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dcmanagerclient/commands/v1/__init__.py b/dcmanagerclient/commands/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dcmanagerclient/commands/v1/alarm_manager.py b/dcmanagerclient/commands/v1/alarm_manager.py new file mode 100644 index 0000000..3829d16 --- /dev/null +++ b/dcmanagerclient/commands/v1/alarm_manager.py @@ -0,0 +1,63 @@ +# Copyright (c) 2017 Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +from dcmanagerclient.commands.v1 import base + + +def format(alarms=None): + columns = ( + 'NAME', + 'CRITICAL_ALARMS', + 'MAJOR_ALARMS', + 'MINOR_ALARMS', + 'WARNINGS', + 'STATUS' + ) + + if alarms: + data = ( + alarms.name if alarms.name >= 0 else '-', + alarms.critical if alarms.critical >= 0 else '-', + alarms.major if alarms.major >= 0 else '-', + alarms.minor if alarms.minor >= 0 else '-', + alarms.warnings if alarms.warnings >= 0 else '-', + alarms.status + ) + + else: + data = (tuple('' for _ in range(len(columns))),) + + return columns, data + + +class ListAlarmSummary(base.DCManagerLister): + """List alarm summaries of subclouds.""" + + def _get_format_function(self): + return format + + def get_parser(self, parsed_args): + parser = super(ListAlarmSummary, self).get_parser(parsed_args) + return parser + + def _get_resources(self, parsed_args): + dcmanager_client = self.app.client_manager.alarm_manager + return dcmanager_client.alarm_manager.list_alarms() diff --git a/dcmanagerclient/commands/v1/base.py b/dcmanagerclient/commands/v1/base.py new file mode 100644 index 0000000..000d74d --- /dev/null +++ b/dcmanagerclient/commands/v1/base.py @@ -0,0 +1,90 @@ +# Copyright (c) 2016 Ericsson AB +# All 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. +# +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import abc + +from osc_lib.command import command +import six + + +@six.add_metaclass(abc.ABCMeta) +class DCManagerLister(command.Lister): + @abc.abstractmethod + def _get_format_function(self): + raise NotImplementedError + + @abc.abstractmethod + def _get_resources(self, parsed_args): + """Get a list of API resources (e.g. using client).""" + raise NotImplementedError + + def _validate_parsed_args(self, parsed_args): + # No-op by default. + pass + + def take_action(self, parsed_args): + self._validate_parsed_args(parsed_args) + f = self._get_format_function() + + ret = self._get_resources(parsed_args) + if not isinstance(ret, list): + ret = [ret] + + data = [f(r)[1] for r in ret] + + if data: + return f()[0], data + else: + return f() + + +@six.add_metaclass(abc.ABCMeta) +class DCManagerShowOne(command.ShowOne): + @abc.abstractmethod + def _get_format_function(self): + raise NotImplementedError + + @abc.abstractmethod + def _get_resources(self, parsed_args): + """Get a list of API resources (e.g. using client).""" + raise NotImplementedError + + def _validate_parsed_args(self, parsed_args): + # No-op by default. + pass + + def take_action(self, parsed_args): + self._validate_parsed_args(parsed_args) + f = self._get_format_function() + + ret = self._get_resources(parsed_args) + if not isinstance(ret, list): + ret = [ret] + + columns = [f(r)[0] for r in ret] + data = [f(r)[1] for r in ret] + + if data: + return (columns[0], data[0]) + else: + return f() diff --git a/dcmanagerclient/commands/v1/subcloud_manager.py b/dcmanagerclient/commands/v1/subcloud_manager.py new file mode 100644 index 0000000..6a20e75 --- /dev/null +++ b/dcmanagerclient/commands/v1/subcloud_manager.py @@ -0,0 +1,473 @@ +# Copyright (c) 2017 Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +from osc_lib.command import command + +from dcmanagerclient.commands.v1 import base +from dcmanagerclient import exceptions + + +def format(subcloud=None): + columns = ( + 'id', + 'name', + 'management', + 'availability', + 'sync' + ) + + if subcloud: + data = ( + subcloud.subcloud_id, + subcloud.name, + subcloud.management_state, + subcloud.availability_status, + subcloud.sync_status + ) + + else: + data = (tuple('' for _ in range(len(columns))),) + + return columns, data + + +def detail_format(subcloud=None): + columns = ( + 'id', + 'name', + 'description', + 'location', + 'software_version', + 'management', + 'availability', + 'management_subnet', + 'management_start_ip', + 'management_end_ip', + 'management_gateway_ip', + 'systemcontroller_gateway_ip', + 'created_at', + 'updated_at', + ) + + if subcloud: + data = ( + subcloud.subcloud_id, + subcloud.name, + subcloud.description, + subcloud.location, + subcloud.software_version, + subcloud.management_state, + subcloud.availability_status, + subcloud.management_subnet, + subcloud.management_start_ip, + subcloud.management_end_ip, + subcloud.management_gateway_ip, + subcloud.systemcontroller_gateway_ip, + subcloud.created_at, + subcloud.updated_at, + ) + + for listitem, sync_status in enumerate(subcloud.endpoint_sync_status + ): + added_field = (sync_status['endpoint_type'] + + "_sync_status",) + added_value = (sync_status['sync_status'],) + columns += tuple(added_field) + data += tuple(added_value) + else: + data = (tuple('' for _ in range(len(columns))),) + + return columns, data + + +class AddSubcloud(base.DCManagerShowOne): + """Add a new subcloud.""" + + def _get_format_function(self): + return detail_format + + def get_parser(self, parsed_args): + parser = super(AddSubcloud, self).get_parser(parsed_args) + + parser.add_argument( + '--name', + required=True, + help='Name of subcloud.' + ) + + parser.add_argument( + '--description', + required=False, + help='Description of subcloud.' + ) + + parser.add_argument( + '--location', + required=False, + help='Location of subcloud.' + ) + + parser.add_argument( + '--management-subnet', + required=True, + help='Management subnet for subcloud in CIDR format.' + ) + + parser.add_argument( + '--management-start-ip', + required=True, + help='Start of management IP address range for subcloud' + ) + + parser.add_argument( + '--management-end-ip', + required=True, + help='End of management IP address range for subcloud', + ) + + parser.add_argument( + '--management-gateway-ip', + required=True, + help='Management gateway IP for subcloud', + ) + + parser.add_argument( + '--systemcontroller-gateway-ip', + required=True, + help='Central gateway IP', + ) + + return parser + + def _get_resources(self, parsed_args): + dcmanager_client = self.app.client_manager.subcloud_manager + kwargs = dict() + kwargs['name'] = parsed_args.name + if parsed_args.description: + kwargs['description'] = parsed_args.description + if parsed_args.location: + kwargs['location'] = parsed_args.location + kwargs['management-subnet'] = parsed_args.management_subnet + kwargs['management-start-ip'] = parsed_args.management_start_ip + kwargs['management-end-ip'] = parsed_args.management_end_ip + kwargs['management-gateway-ip'] = parsed_args.management_gateway_ip + kwargs['systemcontroller-gateway-ip'] = \ + parsed_args.systemcontroller_gateway_ip + return dcmanager_client.subcloud_manager.add_subcloud(**kwargs) + + +class ListSubcloud(base.DCManagerLister): + """List subclouds.""" + + def _get_format_function(self): + return format + + def get_parser(self, parsed_args): + parser = super(ListSubcloud, self).get_parser(parsed_args) + return parser + + def _get_resources(self, parsed_args): + dcmanager_client = self.app.client_manager.subcloud_manager + return dcmanager_client.subcloud_manager.list_subclouds() + + +class ShowSubcloud(base.DCManagerShowOne): + """Show the details of a subcloud.""" + + def _get_format_function(self): + return detail_format + + def get_parser(self, parsed_args): + parser = super(ShowSubcloud, self).get_parser(parsed_args) + + parser.add_argument( + 'subcloud', + help='Name or ID of subcloud to view the details.' + ) + + return parser + + def _get_resources(self, parsed_args): + subcloud_ref = parsed_args.subcloud + dcmanager_client = self.app.client_manager.subcloud_manager + return dcmanager_client.subcloud_manager.subcloud_detail(subcloud_ref) + + +class DeleteSubcloud(command.Command): + """Delete subcloud details from the database.""" + + def get_parser(self, prog_name): + parser = super(DeleteSubcloud, self).get_parser(prog_name) + + parser.add_argument( + 'subcloud', + help='Name or ID of the subcloud to delete.' + ) + return parser + + def take_action(self, parsed_args): + subcloud_ref = parsed_args.subcloud + dcmanager_client = self.app.client_manager.subcloud_manager + try: + dcmanager_client.subcloud_manager.delete_subcloud(subcloud_ref) + except Exception as e: + print (e) + error_msg = "Unable to delete subcloud %s" % (subcloud_ref) + raise exceptions.DCManagerClientException(error_msg) + + +class UnmanageSubcloud(base.DCManagerShowOne): + """Unmanage a subcloud.""" + + def _get_format_function(self): + return detail_format + + def get_parser(self, prog_name): + parser = super(UnmanageSubcloud, self).get_parser(prog_name) + + parser.add_argument( + 'subcloud', + help='Name or ID of the subcloud to unmanage.' + ) + return parser + + def _get_resources(self, parsed_args): + subcloud_ref = parsed_args.subcloud + dcmanager_client = self.app.client_manager.subcloud_manager + kwargs = dict() + kwargs['management-state'] = 'unmanaged' + try: + return dcmanager_client.subcloud_manager.update_subcloud( + subcloud_ref, **kwargs) + except Exception as e: + print (e) + error_msg = "Unable to unmanage subcloud %s" % (subcloud_ref) + raise exceptions.DCManagerClientException(error_msg) + + +class ManageSubcloud(base.DCManagerShowOne): + """Manage a subcloud.""" + + def _get_format_function(self): + return detail_format + + def get_parser(self, prog_name): + parser = super(ManageSubcloud, self).get_parser(prog_name) + + parser.add_argument( + 'subcloud', + help='Name or ID of the subcloud to manage.' + ) + return parser + + def _get_resources(self, parsed_args): + subcloud_ref = parsed_args.subcloud + dcmanager_client = self.app.client_manager.subcloud_manager + kwargs = dict() + kwargs['management-state'] = 'managed' + try: + return dcmanager_client.subcloud_manager.update_subcloud( + subcloud_ref, **kwargs) + except Exception as e: + print (e) + error_msg = "Unable to manage subcloud %s" % (subcloud_ref) + raise exceptions.DCManagerClientException(error_msg) + + +class UpdateSubcloud(base.DCManagerShowOne): + """Update attributes of a subcloud.""" + + def _get_format_function(self): + return detail_format + + def get_parser(self, prog_name): + parser = super(UpdateSubcloud, self).get_parser(prog_name) + + parser.add_argument( + 'subcloud', + help='Name or ID of the subcloud to update.' + ) + + parser.add_argument( + '--description', + required=False, + help='Description of subcloud.' + ) + + parser.add_argument( + '--location', + required=False, + help='Location of subcloud.' + ) + + return parser + + def _get_resources(self, parsed_args): + subcloud_ref = parsed_args.subcloud + dcmanager_client = self.app.client_manager.subcloud_manager + kwargs = dict() + if parsed_args.description: + kwargs['description'] = parsed_args.description + if parsed_args.location: + kwargs['location'] = parsed_args.location + if len(kwargs) == 0: + error_msg = "Nothing to update" + raise exceptions.DCManagerClientException(error_msg) + + try: + return dcmanager_client.subcloud_manager.update_subcloud( + subcloud_ref, **kwargs) + except Exception as e: + print (e) + error_msg = "Unable to update subcloud %s" % (subcloud_ref) + raise exceptions.DCManagerClientException(error_msg) + + +class GenerateConfigSubcloud(command.Command): + """Generate configuration for a subcloud.""" + + def get_parser(self, prog_name): + parser = super(GenerateConfigSubcloud, self).get_parser(prog_name) + + parser.add_argument( + 'subcloud', + help='Name or ID of the subcloud to generate config.' + ) + + parser.add_argument( + '--pxe-subnet', + required=False, + help='PXE boot subnet for subcloud in CIDR format.' + ) + + parser.add_argument( + '--management-vlan', + required=False, + help='VLAN for subcloud management network.' + ) + + parser.add_argument( + '--management-interface-port', + required=False, + help='Subcloud management interface port.' + ) + + parser.add_argument( + '--management-interface-mtu', + required=False, + help='Subcloud management interface mtu.' + ) + + parser.add_argument( + '--oam-subnet', + required=False, + help='OAM subnet for subcloud in CIDR format.' + ) + + parser.add_argument( + '--oam-gateway-ip', + required=False, + help='OAM gateway IP for subcloud.' + ) + + parser.add_argument( + '--oam-floating-ip', + required=False, + help='OAM floating IP address for subcloud.' + ) + + parser.add_argument( + '--oam-unit-0-ip', + required=False, + help='OAM unit 0 IP address for subcloud.' + ) + + parser.add_argument( + '--oam-unit-1-ip', + required=False, + help='OAM unit 1 IP address for subcloud.' + ) + + parser.add_argument( + '--oam-interface-port', + required=False, + help='Subcloud OAM interface port.' + ) + + parser.add_argument( + '--oam-interface-mtu', + required=False, + help='Subcloud OAM interface mtu.' + ) + + parser.add_argument( + '--system-mode', + required=False, + help='System mode', + choices=['simplex', 'duplex', 'duplex-direct'] + ) + + return parser + + def take_action(self, parsed_args): + subcloud_ref = parsed_args.subcloud + dcmanager_client = self.app.client_manager.subcloud_manager + + kwargs = dict() + if parsed_args.pxe_subnet: + kwargs['pxe-subnet'] = \ + parsed_args.pxe_subnet + if parsed_args.management_vlan: + kwargs['management-vlan'] = \ + parsed_args.management_vlan + if parsed_args.management_interface_port: + kwargs['management-interface-port'] = \ + parsed_args.management_interface_port + if parsed_args.management_interface_mtu: + kwargs['management-interface-mtu'] = \ + parsed_args.management_interface_mtu + if parsed_args.oam_subnet: + kwargs['oam-subnet'] = parsed_args.oam_subnet + if parsed_args.oam_gateway_ip: + kwargs['oam-gateway-ip'] = parsed_args.oam_gateway_ip + if parsed_args.oam_floating_ip: + kwargs['oam-floating-ip'] = parsed_args.oam_floating_ip + if parsed_args.oam_unit_0_ip: + kwargs['oam-unit-0-ip'] = parsed_args.oam_unit_0_ip + if parsed_args.oam_unit_1_ip: + kwargs['oam-unit-1-ip'] = parsed_args.oam_unit_1_ip + if parsed_args.oam_interface_port: + kwargs['oam-interface-port'] = parsed_args.oam_interface_port + if parsed_args.oam_interface_mtu: + kwargs['oam-interface-mtu'] = parsed_args.oam_interface_mtu + if parsed_args.system_mode: + kwargs['system-mode'] = parsed_args.system_mode + + try: + subcloud_config = dcmanager_client.subcloud_manager.\ + generate_config_subcloud(subcloud_ref, **kwargs) + return subcloud_config + + except Exception as e: + print (e) + error_msg = "Unable to generate config for subcloud %s" % \ + (subcloud_ref) + raise exceptions.DCManagerClientException(error_msg) diff --git a/dcmanagerclient/commands/v1/sw_update_manager.py b/dcmanagerclient/commands/v1/sw_update_manager.py new file mode 100644 index 0000000..dc705a6 --- /dev/null +++ b/dcmanagerclient/commands/v1/sw_update_manager.py @@ -0,0 +1,272 @@ +# Copyright (c) 2017 Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +from dcmanagerclient.commands.v1 import base +from dcmanagerclient import exceptions + + +def detail_format(sw_update_strategy=None): + columns = ( + 'subcloud apply type', + 'max parallel subclouds', + 'stop on failure', + 'state', + 'created_at', + 'updated_at', + ) + + if sw_update_strategy: + data = ( + sw_update_strategy.subcloud_apply_type, + sw_update_strategy.max_parallel_subclouds, + sw_update_strategy.stop_on_failure, + sw_update_strategy.state, + sw_update_strategy.created_at, + sw_update_strategy.updated_at, + ) + else: + data = (tuple('' for _ in range(len(columns))),) + + return columns, data + + +def strategy_step_format(strategy_step=None): + columns = ( + 'cloud', + 'stage', + 'state', + 'details', + 'started_at', + 'finished_at', + ) + + if strategy_step: + data = ( + strategy_step.cloud, + strategy_step.stage, + strategy_step.state, + strategy_step.details, + strategy_step.started_at, + strategy_step.finished_at, + ) + + else: + data = (tuple('' for _ in range(len(columns))),) + + return columns, data + + +def detail_strategy_step_format(strategy_step=None): + columns = ( + 'cloud', + 'stage', + 'state', + 'details', + 'started_at', + 'finished_at', + 'created_at', + 'updated_at', + ) + + if strategy_step: + data = ( + strategy_step.cloud, + strategy_step.stage, + strategy_step.state, + strategy_step.details, + strategy_step.started_at, + strategy_step.finished_at, + strategy_step.created_at, + strategy_step.updated_at, + ) + + else: + data = (tuple('' for _ in range(len(columns))),) + + return columns, data + + +class CreatePatchStrategy(base.DCManagerShowOne): + """Create a patch strategy.""" + + def _get_format_function(self): + return detail_format + + def get_parser(self, parsed_args): + parser = super(CreatePatchStrategy, self).get_parser(parsed_args) + + parser.add_argument( + '--subcloud-apply-type', + required=False, + choices=['parallel', 'serial'], + help='Subcloud apply type (parallel or serial).' + ) + + parser.add_argument( + '--max-parallel-subclouds', + required=False, + type=int, + help='Maximum number of parallel subclouds.' + ) + + parser.add_argument( + '--stop-on-failure', + required=False, + action='store_true', + help='Do not patch any additional subclouds after a failure.' + ) + + parser.add_argument( + 'cloud_name', + nargs='?', + default=None, + help='Name of a single cloud to patch.' + ) + + return parser + + def _get_resources(self, parsed_args): + dcmanager_client = self.app.client_manager.sw_update_manager + kwargs = dict() + if parsed_args.subcloud_apply_type: + kwargs['subcloud-apply-type'] = parsed_args.subcloud_apply_type + if parsed_args.max_parallel_subclouds: + kwargs['max-parallel-subclouds'] = \ + parsed_args.max_parallel_subclouds + if parsed_args.stop_on_failure: + kwargs['stop-on-failure'] = 'true' + if parsed_args.cloud_name is not None: + kwargs['cloud_name'] = parsed_args.cloud_name + return dcmanager_client.sw_update_manager.create_patch_strategy( + **kwargs) + + +class ShowPatchStrategy(base.DCManagerShowOne): + """Show the details of a patch strategy for a subcloud.""" + + def _get_format_function(self): + return detail_format + + def get_parser(self, parsed_args): + parser = super(ShowPatchStrategy, self).get_parser(parsed_args) + return parser + + def _get_resources(self, parsed_args): + dcmanager_client = self.app.client_manager.sw_update_manager + return dcmanager_client.sw_update_manager.patch_strategy_detail() + + +class DeletePatchStrategy(base.DCManagerShowOne): + """Delete patch strategy from the database.""" + + def _get_format_function(self): + return detail_format + + def get_parser(self, prog_name): + parser = super(DeletePatchStrategy, self).get_parser(prog_name) + return parser + + def _get_resources(self, parsed_args): + dcmanager_client = self.app.client_manager.sw_update_manager + try: + return dcmanager_client.sw_update_manager.delete_patch_strategy() + except Exception as e: + print (e) + error_msg = "Unable to delete patch strategy" + raise exceptions.DCManagerClientException(error_msg) + + +class ApplyPatchStrategy(base.DCManagerShowOne): + """Apply a patch strategy.""" + + def _get_format_function(self): + return detail_format + + def get_parser(self, prog_name): + parser = super(ApplyPatchStrategy, self).get_parser(prog_name) + return parser + + def _get_resources(self, parsed_args): + dcmanager_client = self.app.client_manager.sw_update_manager + try: + return dcmanager_client.sw_update_manager.apply_patch_strategy() + except Exception as e: + print (e) + error_msg = "Unable to apply patch strategy" + raise exceptions.DCManagerClientException(error_msg) + + +class AbortPatchStrategy(base.DCManagerShowOne): + """Abort a patch strategy.""" + + def _get_format_function(self): + return detail_format + + def get_parser(self, prog_name): + parser = super(AbortPatchStrategy, self).get_parser(prog_name) + return parser + + def _get_resources(self, parsed_args): + dcmanager_client = self.app.client_manager.sw_update_manager + try: + return dcmanager_client.sw_update_manager.abort_patch_strategy() + except Exception as e: + print (e) + error_msg = "Unable to abort patch strategy" + raise exceptions.DCManagerClientException(error_msg) + + +class ListStrategyStep(base.DCManagerLister): + """List strategy steps.""" + + def _get_format_function(self): + return strategy_step_format + + def get_parser(self, parsed_args): + parser = super(ListStrategyStep, self).get_parser(parsed_args) + return parser + + def _get_resources(self, parsed_args): + dcmanager_client = self.app.client_manager.strategy_step_manager + return dcmanager_client.strategy_step_manager.list_strategy_steps() + + +class ShowStrategyStep(base.DCManagerShowOne): + """Show the details of a strategy step.""" + + def _get_format_function(self): + return detail_strategy_step_format + + def get_parser(self, parsed_args): + parser = super(ShowStrategyStep, self).get_parser(parsed_args) + + parser.add_argument( + 'cloud_name', + help='Name of cloud to view the details.' + ) + + return parser + + def _get_resources(self, parsed_args): + cloud_name = parsed_args.cloud_name + dcmanager_client = self.app.client_manager.strategy_step_manager + return dcmanager_client.strategy_step_manager.strategy_step_detail( + cloud_name) diff --git a/dcmanagerclient/commands/v1/sw_update_options_manager.py b/dcmanagerclient/commands/v1/sw_update_options_manager.py new file mode 100644 index 0000000..a7a00ba --- /dev/null +++ b/dcmanagerclient/commands/v1/sw_update_options_manager.py @@ -0,0 +1,223 @@ +# Copyright (c) 2017 Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +from osc_lib.command import command + +from dcmanagerclient.commands.v1 import base +from dcmanagerclient import exceptions + +DEFAULT_REGION_NAME = "RegionOne" + + +def options_detail_format(sw_update_options=None): + + columns = ( + 'cloud', + 'storage apply type', + 'compute apply type', + 'max parallel computes', + 'alarm restriction type', + 'default instance action', + 'created_at', + 'updated_at', + ) + + if sw_update_options: + data = ( + sw_update_options.cloud, + sw_update_options.storage_apply_type, + sw_update_options.compute_apply_type, + sw_update_options.max_parallel_computes, + sw_update_options.alarm_restriction_type, + sw_update_options.default_instance_action, + sw_update_options.created_at, + sw_update_options.updated_at, + ) + else: + data = (tuple('' for _ in range(len(columns))),) + + return columns, data + + +def options_list_format(sw_update_option=None): + columns = ( + 'cloud', + 'storage apply type', + 'compute apply type', + 'max parallel computes', + 'alarm restriction type', + 'default instance action', + ) + + if sw_update_option: + data = ( + sw_update_option.cloud, + sw_update_option.storage_apply_type, + sw_update_option.compute_apply_type, + sw_update_option.max_parallel_computes, + sw_update_option.alarm_restriction_type, + sw_update_option.default_instance_action, + ) + + else: + data = (tuple('' for _ in range(len(columns))),) + + return columns, data + + +class UpdateSwUpdateOptions(base.DCManagerShowOne): + """Update patch options, defaults or per subcloud.""" + + def _get_format_function(self): + return options_detail_format + + def get_parser(self, parsed_args): + parser = super(UpdateSwUpdateOptions, self).get_parser(parsed_args) + + parser.add_argument( + '--storage-apply-type', + required=True, + choices=['parallel', 'serial'], + help='Storage node apply type (parallel or serial).' + ) + + parser.add_argument( + '--compute-apply-type', + required=True, + choices=['parallel', 'serial'], + help='Compute node apply type (parallel or serial).' + ) + + parser.add_argument( + '--max-parallel-computes', + required=True, + type=int, + help='Maximum number of parallel computes.' + ) + + parser.add_argument( + '--alarm-restriction-type', + required=True, + choices=['strict', 'relaxed'], + help='Whether to allow patching if subcloud alarms are present or ' + 'not (strict, relaxed).' + ) + + parser.add_argument( + '--default-instance-action', + required=True, + choices=['stop-start', 'migrate'], + help='How instances should be handled.' + ) + + parser.add_argument( + 'subcloud', + nargs='?', + default=None, + help='Subcloud name or id, omit to set default options.' + ) + + return parser + + def _get_resources(self, parsed_args): + subcloud_ref = parsed_args.subcloud + dcmanager_client = self.app.client_manager.sw_update_options_manager + kwargs = dict() + kwargs['storage-apply-type'] = parsed_args.storage_apply_type + kwargs['compute-apply-type'] = parsed_args.compute_apply_type + kwargs['max-parallel-computes'] = parsed_args.max_parallel_computes + kwargs['alarm-restriction-type'] = parsed_args.alarm_restriction_type + kwargs['default-instance-action'] = parsed_args.default_instance_action + + try: + return dcmanager_client.sw_update_options_manager.\ + sw_update_options_update(subcloud_ref, **kwargs) + except Exception as e: + print (e) + error_msg = "Unable to update patch options for subcloud %s" % \ + (subcloud_ref) + raise exceptions.DCManagerClientException(error_msg) + + +class ListSwUpdateOptions(base.DCManagerLister): + """List patch options.""" + + def _get_format_function(self): + return options_list_format + + def get_parser(self, parsed_args): + parser = super(ListSwUpdateOptions, self).get_parser(parsed_args) + return parser + + def _get_resources(self, parsed_args): + dcmanager_client = self.app.client_manager.sw_update_options_manager + return dcmanager_client.sw_update_options_manager.\ + sw_update_options_list() + + +class ShowSwUpdateOptions(base.DCManagerShowOne): + """Show patch options, defaults or per subcloud.""" + + def _get_format_function(self): + return options_detail_format + + def get_parser(self, parsed_args): + parser = super(ShowSwUpdateOptions, self).get_parser(parsed_args) + + parser.add_argument( + 'subcloud', + nargs='?', + default=None, + help='Subcloud name or id, omit to show default options.' + ) + + return parser + + def _get_resources(self, parsed_args): + subcloud_ref = parsed_args.subcloud + dcmanager_client = self.app.client_manager.sw_update_options_manager + return dcmanager_client.sw_update_options_manager.\ + sw_update_options_detail(subcloud_ref) + + +class DeleteSwUpdateOptions(command.Command): + """Delete per subcloud patch options.""" + + def get_parser(self, prog_name): + parser = super(DeleteSwUpdateOptions, self).get_parser(prog_name) + + parser.add_argument( + 'subcloud', + help='Subcloud name or id' + ) + + return parser + + def take_action(self, parsed_args): + subcloud_ref = parsed_args.subcloud + dcmanager_client = self.app.client_manager.sw_update_options_manager + try: + return dcmanager_client.sw_update_options_manager.\ + sw_update_options_delete(subcloud_ref) + except Exception as e: + print (e) + error_msg = "Unable to delete patch options" + raise exceptions.DCManagerClientException(error_msg) diff --git a/dcmanagerclient/exceptions.py b/dcmanagerclient/exceptions.py new file mode 100644 index 0000000..f1a71d1 --- /dev/null +++ b/dcmanagerclient/exceptions.py @@ -0,0 +1,63 @@ +# Copyright 2016 Ericsson AB +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + + +class DCManagerClientException(Exception): + """Base Exception for DC Manager client + + To correctly use this class, inherit from it and define + a 'message' and 'code' properties. + """ + message = "An unknown exception occurred" + code = "UNKNOWN_EXCEPTION" + + def __str__(self): + return self.message + + def __init__(self, message=message): + self.message = message + super(DCManagerClientException, self).__init__( + '%s: %s' % (self.code, self.message)) + + +class IllegalArgumentException(DCManagerClientException): + message = "IllegalArgumentException occurred" + code = "ILLEGAL_ARGUMENT_EXCEPTION" + + def __init__(self, message=None): + if message: + self.message = message + + +class CommandError(DCManagerClientException): + message = "CommandErrorException occurred" + code = "COMMAND_ERROR_EXCEPTION" + + def __init__(self, message=None): + if message: + self.message = message + + +class APIException(Exception): + def __init__(self, error_code=None, error_message=None): + super(APIException, self).__init__(error_message) + self.error_code = error_code + self.error_message = error_message diff --git a/dcmanagerclient/openstack/__init__.py b/dcmanagerclient/openstack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dcmanagerclient/openstack/common/__init__.py b/dcmanagerclient/openstack/common/__init__.py new file mode 100644 index 0000000..75f8a3a --- /dev/null +++ b/dcmanagerclient/openstack/common/__init__.py @@ -0,0 +1,24 @@ +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import six + + +six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox')) diff --git a/dcmanagerclient/openstack/common/apiclient/__init__.py b/dcmanagerclient/openstack/common/apiclient/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dcmanagerclient/openstack/common/apiclient/auth.py b/dcmanagerclient/openstack/common/apiclient/auth.py new file mode 100644 index 0000000..d99d285 --- /dev/null +++ b/dcmanagerclient/openstack/common/apiclient/auth.py @@ -0,0 +1,231 @@ +# Copyright 2013 OpenStack Foundation +# Copyright 2013 Spanish National Research Council. +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +# E0202: An attribute inherited from %s hide this method +# pylint: disable=E0202 + +import abc +import argparse +import os + +import six +from stevedore import extension + +from dcmanagerclient.openstack.common.apiclient import exceptions + + +_discovered_plugins = {} + + +def discover_auth_systems(): + """Discover the available auth-systems. + + This won't take into account the old style auth-systems. + """ + global _discovered_plugins + _discovered_plugins = {} + + def add_plugin(ext): + _discovered_plugins[ext.name] = ext.plugin + + ep_namespace = "dcmanagerclient.openstack.common.apiclient.auth" + mgr = extension.ExtensionManager(ep_namespace) + mgr.map(add_plugin) + + +def load_auth_system_opts(parser): + """Load options needed by the available auth-systems into a parser. + + This function will try to populate the parser with options from the + available plugins. + """ + group = parser.add_argument_group("Common auth options") + BaseAuthPlugin.add_common_opts(group) + for name, auth_plugin in six.iteritems(_discovered_plugins): + group = parser.add_argument_group( + "Auth-system '%s' options" % name, + conflict_handler="resolve") + auth_plugin.add_opts(group) + + +def load_plugin(auth_system): + try: + plugin_class = _discovered_plugins[auth_system] + except KeyError: + raise exceptions.AuthSystemNotFound(auth_system) + return plugin_class(auth_system=auth_system) + + +def load_plugin_from_args(args): + """Load required plugin and populate it with options. + + Try to guess auth system if it is not specified. Systems are tried in + alphabetical order. + + :type args: argparse.Namespace + :raises: AuthPluginOptionsMissing + """ + auth_system = args.os_auth_system + if auth_system: + plugin = load_plugin(auth_system) + plugin.parse_opts(args) + plugin.sufficient_options() + return plugin + + for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)): + plugin_class = _discovered_plugins[plugin_auth_system] + plugin = plugin_class() + plugin.parse_opts(args) + try: + plugin.sufficient_options() + except exceptions.AuthPluginOptionsMissing: + continue + return plugin + raise exceptions.AuthPluginOptionsMissing(["auth_system"]) + + +@six.add_metaclass(abc.ABCMeta) +class BaseAuthPlugin(object): + """Base class for authentication plugins. + + An authentication plugin needs to override at least the authenticate + method to be a valid plugin. + """ + + auth_system = None + opt_names = [] + common_opt_names = [ + "auth_system", + "username", + "password", + "tenant_name", + "token", + "auth_url", + ] + + def __init__(self, auth_system=None, **kwargs): + self.auth_system = auth_system or self.auth_system + self.opts = dict((name, kwargs.get(name)) + for name in self.opt_names) + + @staticmethod + def _parser_add_opt(parser, opt): + """Add an option to parser in two variants. + + :param opt: option name (with underscores) + """ + dashed_opt = opt.replace("_", "-") + env_var = "OS_%s" % opt.upper() + arg_default = os.environ.get(env_var, "") + arg_help = "Defaults to env[%s]." % env_var + parser.add_argument( + "--os-%s" % dashed_opt, + metavar="<%s>" % dashed_opt, + default=arg_default, + help=arg_help) + parser.add_argument( + "--os_%s" % opt, + metavar="<%s>" % dashed_opt, + help=argparse.SUPPRESS) + + @classmethod + def add_opts(cls, parser): + """Populate the parser with the options for this plugin. + + """ + for opt in cls.opt_names: + # use `BaseAuthPlugin.common_opt_names` since it is never + # changed in child classes + if opt not in BaseAuthPlugin.common_opt_names: + cls._parser_add_opt(parser, opt) + + @classmethod + def add_common_opts(cls, parser): + """Add options that are common for several plugins. + + """ + for opt in cls.common_opt_names: + cls._parser_add_opt(parser, opt) + + @staticmethod + def get_opt(opt_name, args): + """Return option name and value. + + :param opt_name: name of the option, e.g., "username" + :param args: parsed arguments + """ + return (opt_name, getattr(args, "os_%s" % opt_name, None)) + + def parse_opts(self, args): + """Parse the actual auth-system options if any. + + This method is expected to populate the attribute `self.opts` with a + dict containing the options and values needed to make authentication. + """ + self.opts.update(dict(self.get_opt(opt_name, args) + for opt_name in self.opt_names)) + + def authenticate(self, http_client): + """Authenticate using plugin defined method. + + The method usually analyses `self.opts` and performs + a request to authentication server. + + :param http_client: client object that needs authentication + :type http_client: HTTPClient + :raises: AuthorizationFailure + """ + self.sufficient_options() + self._do_authenticate(http_client) + + @abc.abstractmethod + def _do_authenticate(self, http_client): + """Protected method for authentication. + + """ + + def sufficient_options(self): + """Check if all required options are present. + + :raises: AuthPluginOptionsMissing + """ + missing = [opt + for opt in self.opt_names + if not self.opts.get(opt)] + if missing: + raise exceptions.AuthPluginOptionsMissing(missing) + + @abc.abstractmethod + def token_and_endpoint(self, endpoint_type, service_type): + """Return token and endpoint. + + :param service_type: Service type of the endpoint + :type service_type: string + :param endpoint_type: Type of endpoint. + Possible values: public or publicURL, + internal or internalURL, + admin or adminURL + :type endpoint_type: string + :returns: tuple of token and endpoint strings + :raises: EndpointException + """ diff --git a/dcmanagerclient/openstack/common/apiclient/base.py b/dcmanagerclient/openstack/common/apiclient/base.py new file mode 100644 index 0000000..f26b583 --- /dev/null +++ b/dcmanagerclient/openstack/common/apiclient/base.py @@ -0,0 +1,515 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack Foundation +# Copyright 2012 Grid Dynamics +# Copyright 2013 OpenStack Foundation +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +""" +Base utilities to build API operation managers and objects on top of. +""" + +# E1102: %s is not callable +# pylint: disable=E1102 + +import abc +import copy + +import six +from six.moves.urllib import parse + +from dcmanagerclient.openstack.common.apiclient import exceptions +from dcmanagerclient.openstack.common.gettextutils import _ +from dcmanagerclient.openstack.common import strutils + + +def getid(obj): + """Return id if argument is a Resource. + + Abstracts the common pattern of allowing both an object or an object's ID + (UUID) as a parameter when dealing with relationships. + """ + try: + if obj.uuid: + return obj.uuid + except AttributeError: + pass + try: + return obj.id + except AttributeError: + return obj + + +# TODO(aababilov): call run_hooks() in HookableMixin's child classes +class HookableMixin(object): + """Mixin so classes can register and run hooks.""" + _hooks_map = {} + + @classmethod + def add_hook(cls, hook_type, hook_func): + """Add a new hook of specified type. + + :param cls: class that registers hooks + :param hook_type: hook type, e.g., '__pre_parse_args__' + :param hook_func: hook function + """ + if hook_type not in cls._hooks_map: + cls._hooks_map[hook_type] = [] + + cls._hooks_map[hook_type].append(hook_func) + + @classmethod + def run_hooks(cls, hook_type, *args, **kwargs): + """Run all hooks of specified type. + + :param cls: class that registers hooks + :param hook_type: hook type, e.g., '__pre_parse_args__' + :param args: args to be passed to every hook function + :param kwargs: kwargs to be passed to every hook function + """ + hook_funcs = cls._hooks_map.get(hook_type) or [] + for hook_func in hook_funcs: + hook_func(*args, **kwargs) + + +class BaseManager(HookableMixin): + """Basic manager type providing common operations. + + Managers interact with a particular type of API (servers, flavors, images, + etc.) and provide CRUD operations for them. + """ + resource_class = None + + def __init__(self, client): + """Initializes BaseManager with `client`. + + :param client: instance of BaseClient descendant for HTTP requests + """ + super(BaseManager, self).__init__() + self.client = client + + def _list(self, url, response_key, obj_class=None, json=None): + """List the collection. + + :param url: a partial URL, e.g., '/servers' + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + :param obj_class: class for constructing the returned objects + (self.resource_class will be used by default) + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + """ + if json: + body = self.client.post(url, json=json).json() + else: + body = self.client.get(url).json() + + if obj_class is None: + obj_class = self.resource_class + + data = body[response_key] + # NOTE(ja): keystone returns values as list as {'values': [ ... ]} + # unlike other services which just return the list... + try: + data = data['values'] + except (KeyError, TypeError): + pass + + return [obj_class(self, res, loaded=True) for res in data if res] + + def _get(self, url, response_key): + """Get an object from collection. + + :param url: a partial URL, e.g., '/servers' + :param response_key: the key to be looked up in response dictionary, + e.g., 'server' + """ + body = self.client.get(url).json() + return self.resource_class(self, body[response_key], loaded=True) + + def _head(self, url): + """Retrieve request headers for an object. + + :param url: a partial URL, e.g., '/servers' + """ + resp = self.client.head(url) + return resp.status_code == 204 + + def _post(self, url, json, response_key, return_raw=False): + """Create an object. + + :param url: a partial URL, e.g., '/servers' + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + :param return_raw: flag to force returning raw JSON instead of + Python object of self.resource_class + """ + body = self.client.post(url, json=json).json() + if return_raw: + return body[response_key] + return self.resource_class(self, body[response_key]) + + def _put(self, url, json=None, response_key=None): + """Update an object with PUT method. + + :param url: a partial URL, e.g., '/servers' + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + """ + resp = self.client.put(url, json=json) + # PUT requests may not return a body + if resp.content: + body = resp.json() + if response_key is not None: + return self.resource_class(self, body[response_key]) + else: + return self.resource_class(self, body) + + def _patch(self, url, json=None, response_key=None): + """Update an object with PATCH method. + + :param url: a partial URL, e.g., '/servers' + :param json: data that will be encoded as JSON and passed in POST + request (GET will be sent by default) + :param response_key: the key to be looked up in response dictionary, + e.g., 'servers' + """ + body = self.client.patch(url, json=json).json() + if response_key is not None: + return self.resource_class(self, body[response_key]) + else: + return self.resource_class(self, body) + + def _delete(self, url): + """Delete an object. + + :param url: a partial URL, e.g., '/servers/my-server' + """ + return self.client.delete(url) + + +@six.add_metaclass(abc.ABCMeta) +class ManagerWithFind(BaseManager): + """Manager with additional `find()`/`findall()` methods.""" + + @abc.abstractmethod + def list(self): + pass + + def find(self, **kwargs): + """Find a single item with attributes matching ``**kwargs``. + + This isn't very efficient: it loads the entire list then filters on + the Python side. + """ + matches = self.findall(**kwargs) + num_matches = len(matches) + if num_matches == 0: + msg = _("No %(name)s matching %(args)s.") % { + 'name': self.resource_class.__name__, + 'args': kwargs + } + raise exceptions.NotFound(msg) + elif num_matches > 1: + raise exceptions.NoUniqueMatch() + else: + return matches[0] + + def findall(self, **kwargs): + """Find all items with attributes matching ``**kwargs``. + + This isn't very efficient: it loads the entire list then filters on + the Python side. + """ + found = [] + searches = kwargs.items() + + for obj in self.list(): + try: + if all(getattr(obj, attr) == value + for (attr, value) in searches): + found.append(obj) + except AttributeError: + continue + + return found + + +class CrudManager(BaseManager): + """Base manager class for manipulating entities. + + Children of this class are expected to define a `collection_key` and `key`. + + - `collection_key`: Usually a plural noun by convention (e.g. `entities`); + used to refer collections in both URL's (e.g. `/v3/entities`) and JSON + objects containing a list of member resources (e.g. `{'entities': [{}, + {}, {}]}`). + - `key`: Usually a singular noun by convention (e.g. `entity`); used to + refer to an individual member of the collection. + + """ + collection_key = None + key = None + + def build_url(self, base_url=None, **kwargs): + """Builds a resource URL for the given kwargs. + + Given an example collection where `collection_key = 'entities'` and + `key = 'entity'`, the following URL's could be generated. + + By default, the URL will represent a collection of entities, e.g.:: + + /entities + + If kwargs contains an `entity_id`, then the URL will represent a + specific member, e.g.:: + + /entities/{entity_id} + + :param base_url: if provided, the generated URL will be appended to it + """ + url = base_url if base_url is not None else '' + + url += '/%s' % self.collection_key + + # do we have a specific entity? + entity_id = kwargs.get('%s_id' % self.key) + if entity_id is not None: + url += '/%s' % entity_id + + return url + + def _filter_kwargs(self, kwargs): + """Drop null values and handle ids.""" + for key, ref in six.iteritems(kwargs.copy()): + if ref is None: + kwargs.pop(key) + else: + if isinstance(ref, Resource): + kwargs.pop(key) + kwargs['%s_id' % key] = getid(ref) + return kwargs + + def create(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._post( + self.build_url(**kwargs), + {self.key: kwargs}, + self.key) + + def get(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._get( + self.build_url(**kwargs), + self.key) + + def head(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._head(self.build_url(**kwargs)) + + def list(self, base_url=None, **kwargs): + """List the collection. + + :param base_url: if provided, the generated URL will be appended to it + """ + kwargs = self._filter_kwargs(kwargs) + + return self._list( + '%(base_url)s%(query)s' % { + 'base_url': self.build_url(base_url=base_url, **kwargs), + 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', + }, + self.collection_key) + + def put(self, base_url=None, **kwargs): + """Update an element. + + :param base_url: if provided, the generated URL will be appended to it + """ + kwargs = self._filter_kwargs(kwargs) + + return self._put(self.build_url(base_url=base_url, **kwargs)) + + def update(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + params = kwargs.copy() + params.pop('%s_id' % self.key) + + return self._patch( + self.build_url(**kwargs), + {self.key: params}, + self.key) + + def delete(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + + return self._delete( + self.build_url(**kwargs)) + + def find(self, base_url=None, **kwargs): + """Find a single item with attributes matching ``**kwargs``. + + :param base_url: if provided, the generated URL will be appended to it + """ + kwargs = self._filter_kwargs(kwargs) + + rl = self._list( + '%(base_url)s%(query)s' % { + 'base_url': self.build_url(base_url=base_url, **kwargs), + 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', + }, + self.collection_key) + num = len(rl) + + if num == 0: + msg = _("No %(name)s matching %(args)s.") % { + 'name': self.resource_class.__name__, + 'args': kwargs + } + raise exceptions.NotFound(404, msg) + elif num > 1: + raise exceptions.NoUniqueMatch + else: + return rl[0] + + +class Extension(HookableMixin): + """Extension descriptor.""" + + SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') + manager_class = None + + def __init__(self, name, module): + super(Extension, self).__init__() + self.name = name + self.module = module + self._parse_extension_module() + + def _parse_extension_module(self): + self.manager_class = None + for attr_name, attr_value in self.module.__dict__.items(): + if attr_name in self.SUPPORTED_HOOKS: + self.add_hook(attr_name, attr_value) + else: + try: + if issubclass(attr_value, BaseManager): + self.manager_class = attr_value + except TypeError: + pass + + def __repr__(self): + return "" % self.name + + +class Resource(object): + """Base class for OpenStack resources (tenant, user, etc.). + + This is pretty much just a bag for attributes. + """ + + HUMAN_ID = False + NAME_ATTR = 'name' + + def __init__(self, manager, info, loaded=False): + """Populate and bind to a manager. + + :param manager: BaseManager object + :param info: dictionary representing resource attributes + :param loaded: prevent lazy-loading if set to True + """ + self.manager = manager + self._info = info + self._add_details(info) + self._loaded = loaded + + def __repr__(self): + reprkeys = sorted(k + for k in self.__dict__.keys() + if k[0] != '_' and k != 'manager') + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) + + @property + def human_id(self): + """Human-readable ID which can be used for bash completion. + + """ + if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: + return strutils.to_slug(getattr(self, self.NAME_ATTR)) + return None + + def _add_details(self, info): + for (k, v) in six.iteritems(info): + try: + setattr(self, k, v) + self._info[k] = v + except AttributeError: + # In this case we already defined the attribute on the class + pass + + def __getattr__(self, k): + if k not in self.__dict__: + # NOTE(bcwaldon): disallow lazy-loading if already loaded once + if not self.is_loaded(): + self.get() + return self.__getattr__(k) + + raise AttributeError(k) + else: + return self.__dict__[k] + + def get(self): + """Support for lazy loading details. + + Some clients, such as novaclient have the option to lazy load the + details, details which can be loaded with this function. + """ + # set_loaded() first ... so if we have to bail, we know we tried. + self.set_loaded(True) + if not hasattr(self.manager, 'get'): + return + + new = self.manager.get(self.id) + if new: + self._add_details(new._info) + + def __eq__(self, other): + if not isinstance(other, Resource): + return NotImplemented + # two resources of different types are not equal + if not isinstance(other, self.__class__): + return False + if hasattr(self, 'id') and hasattr(other, 'id'): + return self.id == other.id + return self._info == other._info + + def is_loaded(self): + return self._loaded + + def set_loaded(self, val): + self._loaded = val + + def to_dict(self): + return copy.deepcopy(self._info) diff --git a/dcmanagerclient/openstack/common/apiclient/client.py b/dcmanagerclient/openstack/common/apiclient/client.py new file mode 100644 index 0000000..eeb5f1d --- /dev/null +++ b/dcmanagerclient/openstack/common/apiclient/client.py @@ -0,0 +1,370 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack Foundation +# Copyright 2011 Piston Cloud Computing, Inc. +# Copyright 2013 Alessio Ababilov +# Copyright 2013 Grid Dynamics +# Copyright 2013 OpenStack Foundation +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +""" +OpenStack Client interface. Handles the REST calls and responses. +""" + +# E0202: An attribute inherited from %s hide this method +# pylint: disable=E0202 + +import logging +import time + +try: + import simplejson as json +except ImportError: + import json + +import requests + +from dcmanagerclient.openstack.common.apiclient import exceptions +from dcmanagerclient.openstack.common.gettextutils import _ +from dcmanagerclient.openstack.common import importutils + +_logger = logging.getLogger(__name__) + + +class HTTPClient(object): + """This client handles sending HTTP requests to OpenStack servers. + + Features: + + - share authentication information between several clients to different + services (e.g., for compute and image clients); + - reissue authentication request for expired tokens; + - encode/decode JSON bodies; + - raise exceptions on HTTP errors; + - pluggable authentication; + - store authentication information in a keyring; + - store time spent for requests; + - register clients for particular services, so one can use + `http_client.identity` or `http_client.compute`; + - log requests and responses in a format that is easy to copy-and-paste + into terminal and send the same request with curl. + """ + + user_agent = "dcmanagerclient.openstack.common.apiclient" + + def __init__(self, + auth_plugin, + region_name=None, + endpoint_type="publicURL", + original_ip=None, + verify=True, + cert=None, + timeout=None, + timings=False, + keyring_saver=None, + debug=False, + user_agent=None, + http=None): + self.auth_plugin = auth_plugin + + self.endpoint_type = endpoint_type + self.region_name = region_name + + self.original_ip = original_ip + self.timeout = timeout + self.verify = verify + self.cert = cert + + self.keyring_saver = keyring_saver + self.debug = debug + self.user_agent = user_agent or self.user_agent + + self.times = [] # [("item", starttime, endtime), ...] + self.timings = timings + + # requests within the same session can reuse TCP connections from pool + self.http = http or requests.Session() + + self.cached_token = None + + def _http_log_req(self, method, url, kwargs): + if not self.debug: + return + + string_parts = [ + "curl -i", + "-X '%s'" % method, + "'%s'" % url, + ] + + for element in kwargs['headers']: + header = "-H '%s: %s'" % (element, kwargs['headers'][element]) + string_parts.append(header) + + _logger.debug("REQ: %s" % " ".join(string_parts)) + if 'data' in kwargs: + _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) + + def _http_log_resp(self, resp): + if not self.debug: + return + _logger.debug( + "RESP: [%s] %s\n", + resp.status_code, + resp.headers) + if resp._content_consumed: + _logger.debug( + "RESP BODY: %s\n", + resp.text) + + def serialize(self, kwargs): + if kwargs.get('json') is not None: + kwargs['headers']['Content-Type'] = 'application/json' + kwargs['data'] = json.dumps(kwargs['json']) + try: + del kwargs['json'] + except KeyError: + pass + + def get_timings(self): + return self.times + + def reset_timings(self): + self.times = [] + + def request(self, method, url, **kwargs): + """Send an http request with the specified characteristics. + + Wrapper around `requests.Session.request` to handle tasks such as + setting headers, JSON encoding/decoding, and error handling. + + :param method: method of HTTP request + :param url: URL of HTTP request + :param kwargs: any other parameter that can be passed to + requests.Session.request (such as `headers`) or `json` + that will be encoded as JSON and used as `data` argument + """ + kwargs.setdefault("headers", kwargs.get("headers", {})) + kwargs["headers"]["User-Agent"] = self.user_agent + if self.original_ip: + kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( + self.original_ip, self.user_agent) + if self.timeout is not None: + kwargs.setdefault("timeout", self.timeout) + kwargs.setdefault("verify", self.verify) + if self.cert is not None: + kwargs.setdefault("cert", self.cert) + self.serialize(kwargs) + + self._http_log_req(method, url, kwargs) + if self.timings: + start_time = time.time() + resp = self.http.request(method, url, **kwargs) + if self.timings: + self.times.append(("%s %s" % (method, url), + start_time, time.time())) + self._http_log_resp(resp) + + if resp.status_code >= 400: + _logger.debug( + "Request returned failure status: %s", + resp.status_code) + raise exceptions.from_response(resp, method, url) + + return resp + + @staticmethod + def concat_url(endpoint, url): + """Concatenate endpoint and final URL. + + E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to + "http://keystone/v2.0/tokens". + + :param endpoint: the base URL + :param url: the final URL + """ + return "%s/%s" % (endpoint.rstrip("/"), url.strip("/")) + + def client_request(self, client, method, url, **kwargs): + """Send an http request using `client`'s endpoint and specified `url`. + + If request was rejected as unauthorized (possibly because the token is + expired), issue one authorization attempt and send the request once + again. + + :param client: instance of BaseClient descendant + :param method: method of HTTP request + :param url: URL of HTTP request + :param kwargs: any other parameter that can be passed to + `HTTPClient.request` + """ + + filter_args = { + "endpoint_type": client.endpoint_type or self.endpoint_type, + "service_type": client.service_type, + } + token, endpoint = (self.cached_token, client.cached_endpoint) + just_authenticated = False + if not (token and endpoint): + try: + token, endpoint = self.auth_plugin.token_and_endpoint( + **filter_args) + except exceptions.EndpointException: + pass + if not (token and endpoint): + self.authenticate() + just_authenticated = True + token, endpoint = self.auth_plugin.token_and_endpoint( + **filter_args) + if not (token and endpoint): + raise exceptions.AuthorizationFailure( + _("Cannot find endpoint or token for request")) + + old_token_endpoint = (token, endpoint) + kwargs.setdefault("headers", {})["X-Auth-Token"] = token + self.cached_token = token + client.cached_endpoint = endpoint + # Perform the request once. If we get Unauthorized, then it + # might be because the auth token expired, so try to + # re-authenticate and try again. If it still fails, bail. + try: + return self.request( + method, self.concat_url(endpoint, url), **kwargs) + except exceptions.Unauthorized as unauth_ex: + if just_authenticated: + raise + self.cached_token = None + client.cached_endpoint = None + self.authenticate() + try: + token, endpoint = self.auth_plugin.token_and_endpoint( + **filter_args) + except exceptions.EndpointException: + raise unauth_ex + if (not (token and endpoint) or + old_token_endpoint == (token, endpoint)): + raise unauth_ex + self.cached_token = token + client.cached_endpoint = endpoint + kwargs["headers"]["X-Auth-Token"] = token + return self.request( + method, self.concat_url(endpoint, url), **kwargs) + + def add_client(self, base_client_instance): + """Add a new instance of :class:`BaseClient` descendant. + + `self` will store a reference to `base_client_instance`. + + Example: + + >>> def test_clients(): + ... from keystoneclient.auth import keystone + ... from openstack.common.apiclient import client + ... auth = keystone.KeystoneAuthPlugin( + ... username="user", password="pass", tenant_name="tenant", + ... auth_url="http://auth:5000/v2.0") + ... openstack_client = client.HTTPClient(auth) + ... # create nova client + ... from novaclient.v1_1 import client + ... client.Client(openstack_client) + ... # create keystone client + ... from keystoneclient.v2_0 import client + ... client.Client(openstack_client) + ... # use them + ... openstack_client.identity.tenants.list() + ... openstack_client.compute.servers.list() + """ + service_type = base_client_instance.service_type + if service_type and not hasattr(self, service_type): + setattr(self, service_type, base_client_instance) + + def authenticate(self): + self.auth_plugin.authenticate(self) + # Store the authentication results in the keyring for later requests + if self.keyring_saver: + self.keyring_saver.save(self) + + +class BaseClient(object): + """Top-level object to access the OpenStack API. + + This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient` + will handle a bunch of issues such as authentication. + """ + + service_type = None + endpoint_type = None # "publicURL" will be used + cached_endpoint = None + + def __init__(self, http_client, extensions=None): + self.http_client = http_client + http_client.add_client(self) + + # Add in any extensions... + if extensions: + for extension in extensions: + if extension.manager_class: + setattr(self, extension.name, + extension.manager_class(self)) + + def client_request(self, method, url, **kwargs): + return self.http_client.client_request( + self, method, url, **kwargs) + + def head(self, url, **kwargs): + return self.client_request("HEAD", url, **kwargs) + + def get(self, url, **kwargs): + return self.client_request("GET", url, **kwargs) + + def post(self, url, **kwargs): + return self.client_request("POST", url, **kwargs) + + def put(self, url, **kwargs): + return self.client_request("PUT", url, **kwargs) + + def delete(self, url, **kwargs): + return self.client_request("DELETE", url, **kwargs) + + def patch(self, url, **kwargs): + return self.client_request("PATCH", url, **kwargs) + + @staticmethod + def get_class(api_name, version, version_map): + """Returns the client class for the requested API version + + :param api_name: the name of the API, e.g. 'compute', 'image', etc + :param version: the requested API version + :param version_map: a dict of client classes keyed by version + :rtype: a client class for the requested API version + """ + try: + client_path = version_map[str(version)] + except (KeyError, ValueError): + msg = _("Invalid %(api_name)s client version '%(version)s'. " + "Must be one of: %(version_map)s") % \ + {'api_name': api_name, + 'version': version, + 'version_map': ', '.join(version_map.keys()) + } + raise exceptions.UnsupportedVersion(msg) + + return importutils.import_class(client_path) diff --git a/dcmanagerclient/openstack/common/apiclient/exceptions.py b/dcmanagerclient/openstack/common/apiclient/exceptions.py new file mode 100644 index 0000000..40cf854 --- /dev/null +++ b/dcmanagerclient/openstack/common/apiclient/exceptions.py @@ -0,0 +1,473 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 Nebula, Inc. +# Copyright 2013 Alessio Ababilov +# Copyright 2013 OpenStack Foundation +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +""" +Exception definitions. +""" + +import inspect +import sys + +import six + +from dcmanagerclient.openstack.common.gettextutils import _ + + +class ClientException(Exception): + """The base exception class for all exceptions this library raises. + + """ + pass + + +class MissingArgs(ClientException): + """Supplied arguments are not sufficient for calling a function.""" + def __init__(self, missing): + self.missing = missing + msg = _("Missing arguments: %s") % ", ".join(missing) + super(MissingArgs, self).__init__(msg) + + +class ValidationError(ClientException): + """Error in validation on API client side.""" + pass + + +class UnsupportedVersion(ClientException): + """User is trying to use an unsupported version of the API.""" + pass + + +class CommandError(ClientException): + """Error in CLI tool.""" + pass + + +class AuthorizationFailure(ClientException): + """Cannot authorize API client.""" + pass + + +class ConnectionRefused(ClientException): + """Cannot connect to API service.""" + pass + + +class AuthPluginOptionsMissing(AuthorizationFailure): + """Auth plugin misses some options.""" + def __init__(self, opt_names): + super(AuthPluginOptionsMissing, self).__init__( + _("Authentication failed. Missing options: %s") % + ", ".join(opt_names)) + self.opt_names = opt_names + + +class AuthSystemNotFound(AuthorizationFailure): + """User has specified a AuthSystem that is not installed.""" + def __init__(self, auth_system): + super(AuthSystemNotFound, self).__init__( + _("AuthSystemNotFound: %s") % repr(auth_system)) + self.auth_system = auth_system + + +class NoUniqueMatch(ClientException): + """Multiple entities found instead of one.""" + pass + + +class EndpointException(ClientException): + """Something is rotten in Service Catalog.""" + pass + + +class EndpointNotFound(EndpointException): + """Could not find requested endpoint in Service Catalog.""" + pass + + +class AmbiguousEndpoints(EndpointException): + """Found more than one matching endpoint in Service Catalog.""" + def __init__(self, endpoints=None): + super(AmbiguousEndpoints, self).__init__( + _("AmbiguousEndpoints: %s") % repr(endpoints)) + self.endpoints = endpoints + + +class HttpError(ClientException): + """The base exception class for all HTTP exceptions.""" + http_status = 0 + message = _("HTTP Error") + + def __init__(self, message=None, details=None, + response=None, request_id=None, + url=None, method=None, http_status=None): + self.http_status = http_status or self.http_status + self.message = message or self.message + self.details = details + self.request_id = request_id + self.response = response + self.url = url + self.method = method + formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) + if request_id: + formatted_string += " (Request-ID: %s)" % request_id + super(HttpError, self).__init__(formatted_string) + + +class HTTPRedirection(HttpError): + """HTTP Redirection.""" + message = _("HTTP Redirection") + + +class HTTPClientError(HttpError): + """Client-side HTTP error. + + Exception for cases in which the client seems to have erred. + """ + message = _("HTTP Client Error") + + +class HttpServerError(HttpError): + """Server-side HTTP error. + + Exception for cases in which the server is aware that it has + erred or is incapable of performing the request. + """ + message = _("HTTP Server Error") + + +class MultipleChoices(HTTPRedirection): + """HTTP 300 - Multiple Choices. + + Indicates multiple options for the resource that the client may follow. + """ + + http_status = 300 + message = _("Multiple Choices") + + +class BadRequest(HTTPClientError): + """HTTP 400 - Bad Request. + + The request cannot be fulfilled due to bad syntax. + """ + http_status = 400 + message = _("Bad Request") + + +class Unauthorized(HTTPClientError): + """HTTP 401 - Unauthorized. + + Similar to 403 Forbidden, but specifically for use when authentication + is required and has failed or has not yet been provided. + """ + http_status = 401 + message = _("Unauthorized") + + +class PaymentRequired(HTTPClientError): + """HTTP 402 - Payment Required. + + Reserved for future use. + """ + http_status = 402 + message = _("Payment Required") + + +class Forbidden(HTTPClientError): + """HTTP 403 - Forbidden. + + The request was a valid request, but the server is refusing to respond + to it. + """ + http_status = 403 + message = _("Forbidden") + + +class NotFound(HTTPClientError): + """HTTP 404 - Not Found. + + The requested resource could not be found but may be available again + in the future. + """ + http_status = 404 + message = _("Not Found") + + +class MethodNotAllowed(HTTPClientError): + """HTTP 405 - Method Not Allowed. + + A request was made of a resource using a request method not supported + by that resource. + """ + http_status = 405 + message = _("Method Not Allowed") + + +class NotAcceptable(HTTPClientError): + """HTTP 406 - Not Acceptable. + + The requested resource is only capable of generating content not + acceptable according to the Accept headers sent in the request. + """ + http_status = 406 + message = _("Not Acceptable") + + +class ProxyAuthenticationRequired(HTTPClientError): + """HTTP 407 - Proxy Authentication Required. + + The client must first authenticate itself with the proxy. + """ + http_status = 407 + message = _("Proxy Authentication Required") + + +class RequestTimeout(HTTPClientError): + """HTTP 408 - Request Timeout. + + The server timed out waiting for the request. + """ + http_status = 408 + message = _("Request Timeout") + + +class Conflict(HTTPClientError): + """HTTP 409 - Conflict. + + Indicates that the request could not be processed because of conflict + in the request, such as an edit conflict. + """ + http_status = 409 + message = _("Conflict") + + +class Gone(HTTPClientError): + """HTTP 410 - Gone. + + Indicates that the resource requested is no longer available and will + not be available again. + """ + http_status = 410 + message = _("Gone") + + +class LengthRequired(HTTPClientError): + """HTTP 411 - Length Required. + + The request did not specify the length of its content, which is + required by the requested resource. + """ + http_status = 411 + message = _("Length Required") + + +class PreconditionFailed(HTTPClientError): + """HTTP 412 - Precondition Failed. + + The server does not meet one of the preconditions that the requester + put on the request. + """ + http_status = 412 + message = _("Precondition Failed") + + +class RequestEntityTooLarge(HTTPClientError): + """HTTP 413 - Request Entity Too Large. + + The request is larger than the server is willing or able to process. + """ + http_status = 413 + message = _("Request Entity Too Large") + + def __init__(self, *args, **kwargs): + try: + self.retry_after = int(kwargs.pop('retry_after')) + except (KeyError, ValueError): + self.retry_after = 0 + + super(RequestEntityTooLarge, self).__init__(*args, **kwargs) + + +class RequestUriTooLong(HTTPClientError): + """HTTP 414 - Request-URI Too Long. + + The URI provided was too long for the server to process. + """ + http_status = 414 + message = _("Request-URI Too Long") + + +class UnsupportedMediaType(HTTPClientError): + """HTTP 415 - Unsupported Media Type. + + The request entity has a media type which the server or resource does + not support. + """ + http_status = 415 + message = _("Unsupported Media Type") + + +class RequestedRangeNotSatisfiable(HTTPClientError): + """HTTP 416 - Requested Range Not Satisfiable. + + The client has asked for a portion of the file, but the server cannot + supply that portion. + """ + http_status = 416 + message = _("Requested Range Not Satisfiable") + + +class ExpectationFailed(HTTPClientError): + """HTTP 417 - Expectation Failed. + + The server cannot meet the requirements of the Expect request-header field. + """ + http_status = 417 + message = _("Expectation Failed") + + +class UnprocessableEntity(HTTPClientError): + """HTTP 422 - Unprocessable Entity. + + The request was well-formed but was unable to be followed due to semantic + errors. + """ + http_status = 422 + message = _("Unprocessable Entity") + + +class InternalServerError(HttpServerError): + """HTTP 500 - Internal Server Error. + + A generic error message, given when no more specific message is suitable. + """ + http_status = 500 + message = _("Internal Server Error") + + +# NotImplemented is a python keyword. +class HttpNotImplemented(HttpServerError): + """HTTP 501 - Not Implemented. + + The server either does not recognize the request method, or it lacks + the ability to fulfill the request. + """ + http_status = 501 + message = _("Not Implemented") + + +class BadGateway(HttpServerError): + """HTTP 502 - Bad Gateway. + + The server was acting as a gateway or proxy and received an invalid + response from the upstream server. + """ + http_status = 502 + message = _("Bad Gateway") + + +class ServiceUnavailable(HttpServerError): + """HTTP 503 - Service Unavailable. + + The server is currently unavailable. + """ + http_status = 503 + message = _("Service Unavailable") + + +class GatewayTimeout(HttpServerError): + """HTTP 504 - Gateway Timeout. + + The server was acting as a gateway or proxy and did not receive a timely + response from the upstream server. + """ + http_status = 504 + message = _("Gateway Timeout") + + +class HttpVersionNotSupported(HttpServerError): + """HTTP 505 - HttpVersion Not Supported. + + The server does not support the HTTP protocol version used in the request. + """ + http_status = 505 + message = _("HTTP Version Not Supported") + + +# _code_map contains all the classes that have http_status attribute. +_code_map = dict( + (getattr(obj, 'http_status', None), obj) + for name, obj in six.iteritems(vars(sys.modules[__name__])) + if inspect.isclass(obj) and getattr(obj, 'http_status', False) +) + + +def from_response(response, method, url): + """Returns an instance of :class:`HttpError` or subclass based on response. + + :param response: instance of `requests.Response` class + :param method: HTTP method used for request + :param url: URL used for request + """ + + req_id = response.headers.get("x-openstack-request-id") + # NOTE(hdd) true for older versions of nova and cinder + if not req_id: + req_id = response.headers.get("x-compute-request-id") + kwargs = { + "http_status": response.status_code, + "response": response, + "method": method, + "url": url, + "request_id": req_id, + } + if "retry-after" in response.headers: + kwargs["retry_after"] = response.headers["retry-after"] + + content_type = response.headers.get("Content-Type", "") + if content_type.startswith("application/json"): + try: + body = response.json() + except ValueError: + pass + else: + if isinstance(body, dict): + error = list(body.values())[0] + kwargs["message"] = error.get("message") + kwargs["details"] = error.get("details") + elif content_type.startswith("text/"): + kwargs["details"] = response.text + + try: + cls = _code_map[response.status_code] + except KeyError: + if 500 <= response.status_code < 600: + cls = HttpServerError + elif 400 <= response.status_code < 500: + cls = HTTPClientError + else: + cls = HttpError + return cls(**kwargs) diff --git a/dcmanagerclient/openstack/common/apiclient/fake_client.py b/dcmanagerclient/openstack/common/apiclient/fake_client.py new file mode 100644 index 0000000..a4eff2c --- /dev/null +++ b/dcmanagerclient/openstack/common/apiclient/fake_client.py @@ -0,0 +1,184 @@ +# Copyright 2013 OpenStack Foundation +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +""" +A fake server that "responds" to API methods with pre-canned responses. + +All of these responses come from the spec, so if for some reason the spec's +wrong the tests might raise AssertionError. I've indicated in comments the +places where actual behavior differs from the spec. +""" + +# W0102: Dangerous default value %s as argument +# pylint: disable=W0102 + +import json + +import requests +import six +from six.moves.urllib import parse + +from dcmanagerclient.openstack.common.apiclient import client + + +def assert_has_keys(dct, required=None, optional=None): + required = required or [] + optional = optional or [] + for k in required: + try: + assert k in dct + except AssertionError: + extra_keys = set(dct.keys()).difference(set(required + optional)) + raise AssertionError("found unexpected keys: %s" % + list(extra_keys)) + + +class TestResponse(requests.Response): + """Wrap requests.Response and provide a convenient initialization. + + """ + + def __init__(self, data): + super(TestResponse, self).__init__() + self._content_consumed = True + if isinstance(data, dict): + self.status_code = data.get('status_code', 200) + # Fake the text attribute to streamline Response creation + text = data.get('text', "") + if isinstance(text, (dict, list)): + self._content = json.dumps(text) + default_headers = { + "Content-Type": "application/json", + } + else: + self._content = text + default_headers = {} + if six.PY3 and isinstance(self._content, six.string_types): + self._content = self._content.encode('utf-8', 'strict') + self.headers = data.get('headers') or default_headers + else: + self.status_code = data + + def __eq__(self, other): + return (self.status_code == other.status_code and + self.headers == other.headers and + self._content == other._content) + + +class FakeHTTPClient(client.HTTPClient): + def __init__(self, *args, **kwargs): + self.callstack = [] + self.fixtures = kwargs.pop("fixtures", None) or {} + if not args and not ("auth_plugin" in kwargs): + args = (None,) + super(FakeHTTPClient, self).__init__(*args, **kwargs) + + def assert_called(self, method, url, body=None, pos=-1): + """Assert than an API method was just called. + + """ + expected = (method, url) + called = self.callstack[pos][0:2] + assert self.callstack, \ + "Expected %s %s but no calls were made." % expected + + assert expected == called, 'Expected %s %s; got %s %s' % \ + (expected + called) + + if body is not None: + if self.callstack[pos][3] != body: + raise AssertionError('%r != %r' % + (self.callstack[pos][3], body)) + + def assert_called_anytime(self, method, url, body=None): + """Assert than an API method was called anytime in the test. + + """ + expected = (method, url) + + assert self.callstack, \ + "Expected %s %s but no calls were made." % expected + + found = False + entry = None + for entry in self.callstack: + if expected == entry[0:2]: + found = True + break + + assert found, 'Expected %s %s; got %s' % \ + (method, url, self.callstack) + if body is not None: + assert entry[3] == body, "%s != %s" % (entry[3], body) + + self.callstack = [] + + def clear_callstack(self): + self.callstack = [] + + def authenticate(self): + pass + + def client_request(self, client, method, url, **kwargs): + # Check that certain things are called correctly + if method in ["GET", "DELETE"]: + assert "json" not in kwargs + + # Note the call + self.callstack.append( + (method, + url, + kwargs.get("headers") or {}, + kwargs.get("json") or kwargs.get("data"))) + try: + fixture = self.fixtures[url][method] + except KeyError: + pass + else: + return TestResponse({"headers": fixture[0], + "text": fixture[1]}) + + # Call the method + args = parse.parse_qsl(parse.urlparse(url)[4]) + kwargs.update(args) + munged_url = url.rsplit('?', 1)[0] + munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') + munged_url = munged_url.replace('-', '_') + + callback = "%s_%s" % (method.lower(), munged_url) + + if not hasattr(self, callback): + raise AssertionError('Called unknown API method: %s %s, ' + 'expected fakes method name: %s' % + (method, url, callback)) + + resp = getattr(self, callback)(**kwargs) + if len(resp) == 3: + status, headers, body = resp + else: + status, body = resp + headers = {} + return TestResponse({ + "status_code": status, + "text": body, + "headers": headers, + }) diff --git a/dcmanagerclient/openstack/common/cliutils.py b/dcmanagerclient/openstack/common/cliutils.py new file mode 100644 index 0000000..d40d6ee --- /dev/null +++ b/dcmanagerclient/openstack/common/cliutils.py @@ -0,0 +1,319 @@ +# Copyright 2012 Red Hat, Inc. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +# W0603: Using the global statement +# W0621: Redefining name %s from outer scope +# pylint: disable=W0603,W0621 + +from __future__ import print_function + +import getpass +import inspect +import os +import sys +import textwrap + +import prettytable +import six +from six import moves + +from dcmanagerclient.openstack.common.apiclient import exceptions +from dcmanagerclient.openstack.common.gettextutils import _ +from dcmanagerclient.openstack.common import strutils +from dcmanagerclient.openstack.common import uuidutils + + +def validate_args(fn, *args, **kwargs): + """Check that the supplied args are sufficient for calling a function. + + >>> validate_args(lambda a: None) + Traceback (most recent call last): + ... + MissingArgs: Missing argument(s): a + >>> validate_args(lambda a, b, c, d: None, 0, c=1) + Traceback (most recent call last): + ... + MissingArgs: Missing argument(s): b, d + + :param fn: the function to check + :param arg: the positional arguments supplied + :param kwargs: the keyword arguments supplied + """ + argspec = inspect.getargspec(fn) + + num_defaults = len(argspec.defaults or []) + required_args = argspec.args[:len(argspec.args) - num_defaults] + + def isbound(method): + return getattr(method, '__self__', None) is not None + + if isbound(fn): + required_args.pop(0) + + missing = [arg for arg in required_args if arg not in kwargs] + missing = missing[len(args):] + if missing: + raise exceptions.MissingArgs(missing) + + +def arg(*args, **kwargs): + """Decorator for CLI args. + + Example: + + >>> @arg("name", help="Name of the new entity") + ... def entity_create(args): + ... pass + """ + def _decorator(func): + add_arg(func, *args, **kwargs) + return func + return _decorator + + +def env(*args, **kwargs): + """Returns the first environment variable set. + + If all are empty, defaults to '' or keyword arg `default`. + """ + for arg in args: + value = os.environ.get(arg) + if value: + return value + return kwargs.get('default', '') + + +def add_arg(func, *args, **kwargs): + """Bind CLI arguments to a shell.py `do_foo` function.""" + + if not hasattr(func, 'arguments'): + func.arguments = [] + + # NOTE(sirp): avoid dups that can occur when the module is shared across + # tests. + if (args, kwargs) not in func.arguments: + # Because of the semantics of decorator composition if we just append + # to the options list positional options will appear to be backwards. + func.arguments.insert(0, (args, kwargs)) + + +def unauthenticated(func): + """Adds 'unauthenticated' attribute to decorated function. + + Usage: + + >>> @unauthenticated + ... def mymethod(f): + ... pass + """ + func.unauthenticated = True + return func + + +def isunauthenticated(func): + """Checks if the function does not require authentication. + + Mark such functions with the `@unauthenticated` decorator. + + :returns: bool + """ + return getattr(func, 'unauthenticated', False) + + +def print_list(objs, fields, formatters=None, sortby_index=0, + mixed_case_fields=None): + """Print a list or objects as a table, one row per object. + + :param objs: iterable of :class:`Resource` + :param fields: attributes that correspond to columns, in order + :param formatters: `dict` of callables for field formatting + :param sortby_index: index of the field for sorting table rows + :param mixed_case_fields: fields corresponding to object attributes that + have mixed case names (e.g., 'serverId') + """ + formatters = formatters or {} + mixed_case_fields = mixed_case_fields or [] + if sortby_index is None: + kwargs = {} + else: + kwargs = {'sortby': fields[sortby_index]} + pt = prettytable.PrettyTable(fields, caching=False) + pt.align = 'l' + + for o in objs: + row = [] + for field in fields: + if field in formatters: + row.append(formatters[field](o)) + else: + if field in mixed_case_fields: + field_name = field.replace(' ', '_') + else: + field_name = field.lower().replace(' ', '_') + data = getattr(o, field_name, '') + row.append(data) + pt.add_row(row) + + print(strutils.safe_encode(pt.get_string(**kwargs))) + + +def print_dict(dct, dict_property="Property", wrap=0): + """Print a `dict` as a table of two columns. + + :param dct: `dict` to print + :param dict_property: name of the first column + :param wrap: wrapping for the second column + """ + pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False) + pt.align = 'l' + for k, v in six.iteritems(dct): + # convert dict to str to check length + if isinstance(v, dict): + v = six.text_type(v) + if wrap > 0: + v = textwrap.fill(six.text_type(v), wrap) + # if value has a newline, add in multiple rows + # e.g. fault with stacktrace + if v and isinstance(v, six.string_types) and r'\n' in v: + lines = v.strip().split(r'\n') + col1 = k + for line in lines: + pt.add_row([col1, line]) + col1 = '' + else: + pt.add_row([k, v]) + print(strutils.safe_encode(pt.get_string())) + + +def get_password(max_password_prompts=3): + """Read password from TTY.""" + verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD")) + pw = None + if hasattr(sys.stdin, "isatty") and sys.stdin.isatty(): + # Check for Ctrl-D + try: + for __ in moves.range(max_password_prompts): + pw1 = getpass.getpass("OS Password: ") + if verify: + pw2 = getpass.getpass("Please verify: ") + else: + pw2 = pw1 + if pw1 == pw2 and pw1: + pw = pw1 + break + except EOFError: + pass + return pw + + +def find_resource(manager, name_or_id, **find_args): + """Look for resource in a given manager. + + Used as a helper for the _find_* methods. + Example: + + def _find_hypervisor(cs, hypervisor): + #Get a hypervisor by name or ID. + return cliutils.find_resource(cs.hypervisors, hypervisor) + """ + # first try to get entity as integer id + try: + return manager.get(int(name_or_id)) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # now try to get entity as uuid + try: + if six.PY2: + tmp_id = strutils.safe_encode(name_or_id) + else: + tmp_id = strutils.safe_decode(name_or_id) + + if uuidutils.is_uuid_like(tmp_id): + return manager.get(tmp_id) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # for str id which is not uuid + if getattr(manager, 'is_alphanum_id_allowed', False): + try: + return manager.get(name_or_id) + except exceptions.NotFound: + pass + + try: + try: + return manager.find(human_id=name_or_id, **find_args) + except exceptions.NotFound: + pass + + # finally try to find entity by name + try: + resource = getattr(manager, 'resource_class', None) + name_attr = resource.NAME_ATTR if resource else 'name' + kwargs = {name_attr: name_or_id} + kwargs.update(find_args) + return manager.find(**kwargs) + except exceptions.NotFound: + msg = _("No %(name)s with a name or " + "ID of '%(name_or_id)s' exists.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) + except exceptions.NoUniqueMatch: + msg = _("Multiple %(name)s matches found for " + "'%(name_or_id)s', use an ID to be more specific.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) + + +def service_type(stype): + """Adds 'service_type' attribute to decorated function. + + Usage: + @service_type('volume') + def mymethod(f): + ... + """ + def inner(f): + f.service_type = stype + return f + return inner + + +def get_service_type(f): + """Retrieves service type from function.""" + return getattr(f, 'service_type', None) + + +def pretty_choice_list(l): + return ', '.join("'%s'" % i for i in l) + + +def exit(msg=''): + if msg: + print (msg, file=sys.stderr) + sys.exit(1) diff --git a/dcmanagerclient/openstack/common/gettextutils.py b/dcmanagerclient/openstack/common/gettextutils.py new file mode 100644 index 0000000..61fc891 --- /dev/null +++ b/dcmanagerclient/openstack/common/gettextutils.py @@ -0,0 +1,506 @@ +# Copyright 2012 Red Hat, Inc. +# Copyright 2013 IBM Corp. +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +""" +gettext for openstack-common modules. + +Usual usage in an openstack.common module: + + from dcmanagerclient.openstack.common.gettextutils import _ +""" + +import copy +import functools +import gettext +import locale +from logging import handlers +import os + +from babel import localedata +import six + +_AVAILABLE_LANGUAGES = {} + +# FIXME(dhellmann): Remove this when moving to oslo.i18n. +USE_LAZY = False + + +class TranslatorFactory(object): + """Create translator functions + + """ + + def __init__(self, domain, lazy=False, localedir=None): + """Establish a set of translation functions for the domain. + + :param domain: Name of translation domain, + specifying a message catalog. + :type domain: str + :param lazy: Delays translation until a message is emitted. + Defaults to False. + :type lazy: Boolean + :param localedir: Directory with translation catalogs. + :type localedir: str + """ + self.domain = domain + self.lazy = lazy + if localedir is None: + localedir = os.environ.get(domain.upper() + '_LOCALEDIR') + self.localedir = localedir + + def _make_translation_func(self, domain=None): + """Return a new translation function ready for use. + + Takes into account whether or not lazy translation is being + done. + + The domain can be specified to override the default from the + factory, but the localedir from the factory is always used + because we assume the log-level translation catalogs are + installed in the same directory as the main application + catalog. + + """ + if domain is None: + domain = self.domain + if self.lazy: + return functools.partial(Message, domain=domain) + t = gettext.translation( + domain, + localedir=self.localedir, + fallback=True, + ) + if six.PY3: + return t.gettext + return t.ugettext + + @property + def primary(self): + "The default translation function." + return self._make_translation_func() + + def _make_log_translation_func(self, level): + return self._make_translation_func(self.domain + '-log-' + level) + + @property + def log_info(self): + "Translate info-level log messages." + return self._make_log_translation_func('info') + + @property + def log_warning(self): + "Translate warning-level log messages." + return self._make_log_translation_func('warning') + + @property + def log_error(self): + "Translate error-level log messages." + return self._make_log_translation_func('error') + + @property + def log_critical(self): + "Translate critical-level log messages." + return self._make_log_translation_func('critical') + + +# NOTE(dhellmann): When this module moves out of the incubator into +# oslo.i18n, these global variables can be moved to an integration +# module within each application. + +# Create the global translation functions. +_translators = TranslatorFactory('dcmanagerclient') + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical + +# NOTE(dhellmann): End of globals that will move to the application's +# integration module. + + +def enable_lazy(): + """Convenience function for configuring _() to use lazy gettext + + Call this at the start of execution to enable the gettextutils._ + function to use lazy gettext functionality. This is useful if + your project is importing _ directly instead of using the + gettextutils.install() way of importing the _ function. + """ + # FIXME(dhellmann): This function will be removed in oslo.i18n, + # because the TranslatorFactory makes it superfluous. + global _, _LI, _LW, _LE, _LC, USE_LAZY + tf = TranslatorFactory('dcmanagerclient', lazy=True) + _ = tf.primary + _LI = tf.log_info + _LW = tf.log_warning + _LE = tf.log_error + _LC = tf.log_critical + USE_LAZY = True + + +def install(domain, lazy=False): + """Install a _() function using the given translation domain. + + Given a translation domain, install a _() function using gettext's + install() function. + + The main difference from gettext.install() is that we allow + overriding the default localedir (e.g. /usr/share/locale) using + a translation-domain-specific environment variable (e.g. + NOVA_LOCALEDIR). + + :param domain: the translation domain + :param lazy: indicates whether or not to install the lazy _() function. + The lazy _() introduces a way to do deferred translation + of messages by installing a _ that builds Message objects, + instead of strings, which can then be lazily translated into + any available locale. + """ + if lazy: + from six import moves + tf = TranslatorFactory(domain, lazy=True) + moves.builtins.__dict__['_'] = tf.primary + else: + localedir = '%s_LOCALEDIR' % domain.upper() + if six.PY3: + gettext.install(domain, + localedir=os.environ.get(localedir)) + else: + gettext.install(domain, + localedir=os.environ.get(localedir), + unicode=True) + + +class Message(six.text_type): + """A Message object is a unicode object that can be translated. + + Translation of Message is done explicitly using the translate() method. + For all non-translation intents and purposes, a Message is simply unicode, + and can be treated as such. + """ + + def __new__(cls, msgid, msgtext=None, params=None, + domain='dcmanagerclient', *args): + """Create a new Message object. + + In order for translation to work gettext requires a message ID, this + msgid will be used as the base unicode text. It is also possible + for the msgid and the base unicode text to be different by passing + the msgtext parameter. + """ + # If the base msgtext is not given, we use the default translation + # of the msgid (which is in English) just in case the system locale is + # not English, so that the base text will be in that locale by default. + if not msgtext: + msgtext = Message._translate_msgid(msgid, domain) + # We want to initialize the parent unicode with the actual object that + # would have been plain unicode if 'Message' was not enabled. + msg = super(Message, cls).__new__(cls, msgtext) + msg.msgid = msgid + msg.domain = domain + msg.params = params + return msg + + def translate(self, desired_locale=None): + """Translate this message to the desired locale. + + :param desired_locale: The desired locale to translate the message to, + if no locale is provided the message will be + translated to the system's default locale. + + :returns: the translated message in unicode + """ + + translated_message = Message._translate_msgid(self.msgid, + self.domain, + desired_locale) + if self.params is None: + # No need for more translation + return translated_message + + # This Message object may have been formatted with one or more + # Message objects as substitution arguments, given either as a single + # argument, part of a tuple, or as one or more values in a dictionary. + # When translating this Message we need to translate those Messages too + translated_params = _translate_args(self.params, desired_locale) + + translated_message = translated_message % translated_params + + return translated_message + + @staticmethod + def _translate_msgid(msgid, domain, desired_locale=None): + if not desired_locale: + system_locale = locale.getdefaultlocale() + # If the system locale is not available to the runtime use English + if not system_locale[0]: + desired_locale = 'en_US' + else: + desired_locale = system_locale[0] + + locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR') + lang = gettext.translation(domain, + localedir=locale_dir, + languages=[desired_locale], + fallback=True) + if six.PY3: + translator = lang.gettext + else: + translator = lang.ugettext + + translated_message = translator(msgid) + return translated_message + + def __mod__(self, other): + # When we mod a Message we want the actual operation to be performed + # by the parent class (i.e. unicode()), the only thing we do here is + # save the original msgid and the parameters in case of a translation + params = self._sanitize_mod_params(other) + unicode_mod = super(Message, self).__mod__(params) + modded = Message(self.msgid, + msgtext=unicode_mod, + params=params, + domain=self.domain) + return modded + + def _sanitize_mod_params(self, other): + """Sanitize the object being modded with this Message. + + - Add support for modding 'None' so translation supports it + - Trim the modded object, which can be a large dictionary, to only + those keys that would actually be used in a translation + - Snapshot the object being modded, in case the message is + translated, it will be used as it was when the Message was created + """ + if other is None: + params = (other,) + elif isinstance(other, dict): + # Merge the dictionaries + # Copy each item in case one does not support deep copy. + params = {} + if isinstance(self.params, dict): + for key, val in self.params.items(): + params[key] = self._copy_param(val) + for key, val in other.items(): + params[key] = self._copy_param(val) + else: + params = self._copy_param(other) + return params + + def _copy_param(self, param): + try: + return copy.deepcopy(param) + except Exception: + # Fallback to casting to unicode this will handle the + # python code-like objects that can't be deep-copied + return six.text_type(param) + + def __add__(self, other): + msg = _('Message objects do not support addition.') + raise TypeError(msg) + + def __radd__(self, other): + return self.__add__(other) + + if six.PY2: + def __str__(self): + # NOTE(luisg): Logging in python 2.6 tries to str() log records, + # and it expects specifically a UnicodeError in order to proceed. + msg = _('Message objects do not support str() because they may ' + 'contain non-ascii characters. ' + 'Please use unicode() or translate() instead.') + raise UnicodeError(msg) + + +def get_available_languages(domain): + """Lists the available languages for the given translation domain. + + :param domain: the domain to get languages for + """ + if domain in _AVAILABLE_LANGUAGES: + return copy.copy(_AVAILABLE_LANGUAGES[domain]) + + localedir = '%s_LOCALEDIR' % domain.upper() + find = lambda x: gettext.find(domain, + localedir=os.environ.get(localedir), + languages=[x]) + + # NOTE(mrodden): en_US should always be available (and first in case + # order matters) since our in-line message strings are en_US + language_list = ['en_US'] + # NOTE(luisg): Babel <1.0 used a function called list(), which was + # renamed to locale_identifiers() in >=1.0, the requirements master list + # requires >=0.9.6, uncapped, so defensively work with both. We can remove + # this check when the master list updates to >=1.0, and update all projects + list_identifiers = (getattr(localedata, 'list', None) or + getattr(localedata, 'locale_identifiers')) + locale_identifiers = list_identifiers() + + for i in locale_identifiers: + if find(i) is not None: + language_list.append(i) + + # NOTE(luisg): Babel>=1.0,<1.3 has a bug where some OpenStack supported + # locales (e.g. 'zh_CN', and 'zh_TW') aren't supported even though they + # are perfectly legitimate locales: + # https://github.com/mitsuhiko/babel/issues/37 + # In Babel 1.3 they fixed the bug and they support these locales, but + # they are still not explicitly "listed" by locale_identifiers(). + # That is why we add the locales here explicitly if necessary so that + # they are listed as supported. + aliases = {'zh': 'zh_CN', + 'zh_Hant_HK': 'zh_HK', + 'zh_Hant': 'zh_TW', + 'fil': 'tl_PH'} + for (loc, alias) in six.iteritems(aliases): + if loc in language_list and alias not in language_list: + language_list.append(alias) + + _AVAILABLE_LANGUAGES[domain] = language_list + return copy.copy(language_list) + + +def translate(obj, desired_locale=None): + """Gets the translated unicode representation of the given object. + + If the object is not translatable it is returned as-is. + If the locale is None the object is translated to the system locale. + + :param obj: the object to translate + :param desired_locale: the locale to translate the message to, if None the + default system locale will be used + :returns: the translated object in unicode, or the original object if + it could not be translated + """ + message = obj + if not isinstance(message, Message): + # If the object to translate is not already translatable, + # let's first get its unicode representation + message = six.text_type(obj) + if isinstance(message, Message): + # Even after unicoding() we still need to check if we are + # running with translatable unicode before translating + return message.translate(desired_locale) + return obj + + +def _translate_args(args, desired_locale=None): + """Translates all the translatable elements of the given arguments object. + + This method is used for translating the translatable values in method + arguments which include values of tuples or dictionaries. + If the object is not a tuple or a dictionary the object itself is + translated if it is translatable. + + If the locale is None the object is translated to the system locale. + + :param args: the args to translate + :param desired_locale: the locale to translate the args to, if None the + default system locale will be used + :returns: a new args object with the translated contents of the original + """ + if isinstance(args, tuple): + return tuple(translate(v, desired_locale) for v in args) + if isinstance(args, dict): + translated_dict = {} + for (k, v) in six.iteritems(args): + translated_v = translate(v, desired_locale) + translated_dict[k] = translated_v + return translated_dict + return translate(args, desired_locale) + + +class TranslationHandler(handlers.MemoryHandler): + """Handler that translates records before logging them. + + The TranslationHandler takes a locale and a target logging.Handler object + to forward LogRecord objects to after translating them. This handler + depends on Message objects being logged, instead of regular strings. + + The handler can be configured declaratively in the logging.conf as follows: + + [handlers] + keys = translatedlog, translator + + [handler_translatedlog] + class = handlers.WatchedFileHandler + args = ('/var/log/api-localized.log',) + formatter = context + + [handler_translator] + class = openstack.common.log.TranslationHandler + target = translatedlog + args = ('zh_CN',) + + If the specified locale is not available in the system, the handler will + log in the default locale. + """ + + def __init__(self, locale=None, target=None): + """Initialize a TranslationHandler + + :param locale: locale to use for translating messages + :param target: logging.Handler object to forward + LogRecord objects to after translation + """ + # NOTE(luisg): In order to allow this handler to be a wrapper for + # other handlers, such as a FileHandler, and still be able to + # configure it using logging.conf, this handler has to extend + # MemoryHandler because only the MemoryHandlers' logging.conf + # parsing is implemented such that it accepts a target handler. + handlers.MemoryHandler.__init__(self, capacity=0, target=target) + self.locale = locale + + def setFormatter(self, fmt): + self.target.setFormatter(fmt) + + def emit(self, record): + # We save the message from the original record to restore it + # after translation, so other handlers are not affected by this + original_msg = record.msg + original_args = record.args + + try: + self._translate_and_log_record(record) + finally: + record.msg = original_msg + record.args = original_args + + def _translate_and_log_record(self, record): + record.msg = translate(record.msg, self.locale) + + # In addition to translating the message, we also need to translate + # arguments that were passed to the log method that were not part + # of the main message e.g., log.info(_('Some message %s'), this_one)) + record.args = _translate_args(record.args, self.locale) + + self.target.emit(record) diff --git a/dcmanagerclient/openstack/common/importutils.py b/dcmanagerclient/openstack/common/importutils.py new file mode 100644 index 0000000..a429a97 --- /dev/null +++ b/dcmanagerclient/openstack/common/importutils.py @@ -0,0 +1,80 @@ +# Copyright 2011 OpenStack Foundation. +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +""" +Import related utilities and helper functions. +""" + +import sys +import traceback + + +def import_class(import_str): + """Returns a class from a string including module and class.""" + mod_str, _sep, class_str = import_str.rpartition('.') + __import__(mod_str) + try: + return getattr(sys.modules[mod_str], class_str) + except AttributeError: + raise ImportError('Class %s cannot be found (%s)' % + (class_str, + traceback.format_exception(*sys.exc_info()))) + + +def import_object(import_str, *args, **kwargs): + """Import a class and return an instance of it.""" + return import_class(import_str)(*args, **kwargs) + + +def import_object_ns(name_space, import_str, *args, **kwargs): + """Tries to import object from default namespace. + + Imports a class and return an instance of it, first by trying + to find the class in a default namespace, then failing back to + a full path if not found in the default namespace. + """ + import_value = "%s.%s" % (name_space, import_str) + try: + return import_class(import_value)(*args, **kwargs) + except ImportError: + return import_class(import_str)(*args, **kwargs) + + +def import_module(import_str): + """Import a module.""" + __import__(import_str) + return sys.modules[import_str] + + +def import_versioned_module(version, submodule=None): + module = 'dcmanagerclient.v%s' % version + if submodule: + module = '.'.join((module, submodule)) + return import_module(module) + + +def try_import(import_str, default=None): + """Try to import a module and if it fails return default.""" + try: + return import_module(import_str) + except ImportError: + return default diff --git a/dcmanagerclient/openstack/common/strutils.py b/dcmanagerclient/openstack/common/strutils.py new file mode 100644 index 0000000..72682a6 --- /dev/null +++ b/dcmanagerclient/openstack/common/strutils.py @@ -0,0 +1,247 @@ +# Copyright 2011 OpenStack Foundation. +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +""" +System-level utilities and helper functions. +""" + +import math +import re +import sys +import unicodedata + +import six + +from dcmanagerclient.openstack.common.gettextutils import _ + + +UNIT_PREFIX_EXPONENT = { + 'k': 1, + 'K': 1, + 'Ki': 1, + 'M': 2, + 'Mi': 2, + 'G': 3, + 'Gi': 3, + 'T': 4, + 'Ti': 4, +} +UNIT_SYSTEM_INFO = { + 'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')), + 'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')), +} + +TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') +FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') + +SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]") +SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") + + +def int_from_bool_as_string(subject): + """Interpret a string as a boolean and return either 1 or 0. + + Any string value in: + + ('True', 'true', 'On', 'on', '1') + + is interpreted as a boolean True. + + Useful for JSON-decoded stuff and config file parsing + """ + return bool_from_string(subject) and 1 or 0 + + +def bool_from_string(subject, strict=False, default=False): + """Interpret a string as a boolean. + + A case-insensitive match is performed such that strings matching 't', + 'true', 'on', 'y', 'yes', or '1' are considered True and, when + `strict=False`, anything else returns the value specified by 'default'. + + Useful for JSON-decoded stuff and config file parsing. + + If `strict=True`, unrecognized values, including None, will raise a + ValueError which is useful when parsing values passed in from an API call. + Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. + """ + if not isinstance(subject, six.string_types): + subject = six.text_type(subject) + + lowered = subject.strip().lower() + + if lowered in TRUE_STRINGS: + return True + elif lowered in FALSE_STRINGS: + return False + elif strict: + acceptable = ', '.join( + "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS)) + msg = _("Unrecognized value '%(val)s', acceptable values are:" + " %(acceptable)s") % {'val': subject, + 'acceptable': acceptable} + raise ValueError(msg) + else: + return default + + +def safe_decode(text, incoming=None, errors='strict'): + """Decodes incoming text/bytes string using `incoming` + + if they're not already unicode. + + :param incoming: Text's current encoding + :param errors: Errors handling policy. See here for valid + values http://docs.python.org/2/library/codecs.html + :returns: text or a unicode `incoming` encoded + representation of it. + :raises TypeError: If text is not an instance of str + """ + if not isinstance(text, (six.string_types, six.binary_type)): + raise TypeError("%s can't be decoded" % type(text)) + + if isinstance(text, six.text_type): + return text + + if not incoming: + incoming = (sys.stdin.encoding or + sys.getdefaultencoding()) + + try: + return text.decode(incoming, errors) + except UnicodeDecodeError: + # Note(flaper87) If we get here, it means that + # sys.stdin.encoding / sys.getdefaultencoding + # didn't return a suitable encoding to decode + # text. This happens mostly when global LANG + # var is not set correctly and there's no + # default encoding. In this case, most likely + # python will use ASCII or ANSI encoders as + # default encodings but they won't be capable + # of decoding non-ASCII characters. + # + # Also, UTF-8 is being used since it's an ASCII + # extension. + return text.decode('utf-8', errors) + + +def safe_encode(text, incoming=None, + encoding='utf-8', errors='strict'): + """Encodes incoming text/bytes string using `encoding`. + + If incoming is not specified, text is expected to be encoded with + current python's default encoding. (`sys.getdefaultencoding`) + + :param incoming: Text's current encoding + :param encoding: Expected encoding for text (Default UTF-8) + :param errors: Errors handling policy. See here for valid + values http://docs.python.org/2/library/codecs.html + :returns: text or a bytestring `encoding` encoded + representation of it. + :raises TypeError: If text is not an instance of str + """ + if not isinstance(text, (six.string_types, six.binary_type)): + raise TypeError("%s can't be encoded" % type(text)) + + if not incoming: + incoming = (sys.stdin.encoding or + sys.getdefaultencoding()) + + if isinstance(text, six.text_type): + return text.encode(encoding, errors) + elif text and encoding != incoming: + # Decode text before encoding it with `encoding` + text = safe_decode(text, incoming, errors) + return text.encode(encoding, errors) + else: + return text + + +def string_to_bytes(text, unit_system='IEC', return_int=False): + """Converts a string into an float representation of bytes. + + The units supported for IEC :: + + Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it) + KB, KiB, MB, MiB, GB, GiB, TB, TiB + + The units supported for SI :: + + kb(it), Mb(it), Gb(it), Tb(it) + kB, MB, GB, TB + + Note that the SI unit system does not support capital letter 'K' + + :param text: String input for bytes size conversion. + :param unit_system: Unit system for byte size conversion. + :param return_int: If True, returns integer representation of text + in bytes. (default: decimal) + :returns: Numerical representation of text in bytes. + :raises ValueError: If text has an invalid value. + + """ + try: + base, reg_ex = UNIT_SYSTEM_INFO[unit_system] + except KeyError: + msg = _('Invalid unit system: "%s"') % unit_system + raise ValueError(msg) + match = reg_ex.match(text) + if match: + magnitude = float(match.group(1)) + unit_prefix = match.group(2) + if match.group(3) in ['b', 'bit']: + magnitude /= 8 + else: + msg = _('Invalid string format: %s') % text + raise ValueError(msg) + if not unit_prefix: + res = magnitude + else: + res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix]) + if return_int: + return int(math.ceil(res)) + return res + + +def to_slug(value, incoming=None, errors="strict"): + """Normalize string. + + Convert to lowercase, remove non-word characters, and convert spaces + to hyphens. + + Inspired by Django's `slugify` filter. + + :param value: Text to slugify + :param incoming: Text's current encoding + :param errors: Errors handling policy. See here for valid + values http://docs.python.org/2/library/codecs.html + :returns: slugified unicode representation of `value` + :raises TypeError: If text is not an instance of str + """ + value = safe_decode(value, incoming, errors) + # NOTE(aababilov): no need to use safe_(encode|decode) here: + # encodings are always "ascii", error handling is always "ignore" + # and types are always known (first: unicode; second: str) + value = unicodedata.normalize("NFKD", value).encode( + "ascii", "ignore").decode("ascii") + value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() + return SLUGIFY_HYPHENATE_RE.sub("-", value) diff --git a/dcmanagerclient/openstack/common/uuidutils.py b/dcmanagerclient/openstack/common/uuidutils.py new file mode 100644 index 0000000..f999694 --- /dev/null +++ b/dcmanagerclient/openstack/common/uuidutils.py @@ -0,0 +1,44 @@ +# Copyright (c) 2012 Intel Corporation. +# All 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +""" +UUID related utilities and helper functions. +""" + +import uuid + + +def generate_uuid(): + return str(uuid.uuid4()) + + +def is_uuid_like(val): + """Returns validation of a value as a UUID. + + For our purposes, a UUID is a canonical form string: + aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa + + """ + try: + return str(uuid.UUID(val)) == val + except (TypeError, ValueError, AttributeError): + return False diff --git a/dcmanagerclient/osc/plugin.py b/dcmanagerclient/osc/plugin.py new file mode 100644 index 0000000..a0d1879 --- /dev/null +++ b/dcmanagerclient/osc/plugin.py @@ -0,0 +1,70 @@ +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +"""OpenStackClient plugin for DC Manager.""" + +import logging + +from osc_lib import utils + +LOG = logging.getLogger(__name__) + +DEFAULT_DCMANAGER_API_VERSION = '1' +API_VERSION_OPTION = 'os_dcmanager_api_version' +API_NAME = 'dcmanager' +API_VERSIONS = { + '1': 'dcmanagerclient.api.v1.client.Client', +} + + +def make_client(instance): + """Return a dcmanager client.""" + version = instance._api_version[API_NAME] + dcmanager_client = utils.get_client_class( + API_NAME, + version, + API_VERSIONS) + + LOG.debug('Instantiating dcmanager client: %s', dcmanager_client) + + dcmanager_url = instance.get_endpoint_for_service_type( + 'dcmanager', + interface='publicURL' + ) + + client = dcmanager_client(dcmanager_url=dcmanager_url, + session=instance.session) + + return client + + +def build_option_parser(parser): + """Hook to add global options.""" + parser.add_argument( + '--os-dcmanager-api-version', + metavar='', + default=utils.env( + 'OS_DCMANAGER_API_VERSION', + default=DEFAULT_DCMANAGER_API_VERSION), + help='DCMANAGER API version, default=' + + DEFAULT_DCMANAGER_API_VERSION + + ' (Env: OS_DCMANAGER_API_VERSION)') + + return parser diff --git a/dcmanagerclient/shell.py b/dcmanagerclient/shell.py new file mode 100644 index 0000000..2ac0300 --- /dev/null +++ b/dcmanagerclient/shell.py @@ -0,0 +1,504 @@ +# Copyright 2015 - Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +""" +Command-line interface to the DC Manager APIs +""" + +import logging +import sys + +from dcmanagerclient import __version__ as dcmanager_version +from dcmanagerclient.api import client +from dcmanagerclient import exceptions +from dcmanagerclient.openstack.common import cliutils as c + +from cliff import app +from cliff import commandmanager +from osc_lib.command import command + +import argparse +from dcmanagerclient.commands.v1 import alarm_manager as am +from dcmanagerclient.commands.v1 import subcloud_manager as sm +from dcmanagerclient.commands.v1 import sw_update_manager as sum +from dcmanagerclient.commands.v1 import sw_update_options_manager as suom + +LOG = logging.getLogger(__name__) + + +class OpenStackHelpFormatter(argparse.HelpFormatter): + def __init__(self, prog, indent_increment=2, max_help_position=32, + width=None): + super(OpenStackHelpFormatter, self).__init__( + prog, + indent_increment, + max_help_position, + width + ) + + def start_section(self, heading): + # Title-case the headings. + heading = '%s%s' % (heading[0].upper(), heading[1:]) + super(OpenStackHelpFormatter, self).start_section(heading) + + +class HelpAction(argparse.Action): + """Custom help action. + + Provide a custom action so the -h and --help options + to the main app will print a list of the commands. + + The commands are determined by checking the CommandManager + instance, passed in as the "default" value for the action. + + """ + + def __call__(self, parser, namespace, values, option_string=None): + outputs = [] + max_len = 0 + app = self.default + parser.print_help(app.stdout) + app.stdout.write('\nCommands for API v1 :\n') + + for name, ep in sorted(app.command_manager): + factory = ep.load() + cmd = factory(self, None) + one_liner = cmd.get_description().split('\n')[0] + outputs.append((name, one_liner)) + max_len = max(len(name), max_len) + + for (name, one_liner) in outputs: + app.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner)) + + sys.exit(0) + + +class BashCompletionCommand(command.Command): + """Prints all of the commands and options for bash-completion.""" + + def take_action(self, parsed_args): + commands = set() + options = set() + + for option, _action in self.app.parser._option_string_actions.items(): + options.add(option) + + for command_name, _cmd in self.app.command_manager: + commands.add(command_name) + + print(' '.join(commands | options)) + + +class DCManagerShell(app.App): + def __init__(self): + super(DCManagerShell, self).__init__( + description=__doc__.strip(), + version=dcmanager_version, + command_manager=commandmanager.CommandManager('dcmanager.cli'), + ) + + # Set v1 commands by default + self._set_shell_commands(self._get_commands(version=1)) + + def configure_logging(self): + log_lvl = logging.DEBUG if self.options.debug else logging.WARNING + logging.basicConfig( + format="%(levelname)s (%(module)s) %(message)s", + level=log_lvl + ) + logging.getLogger('iso8601').setLevel(logging.WARNING) + + if self.options.verbose_level <= 1: + logging.getLogger('requests').setLevel(logging.WARNING) + + def build_option_parser(self, description, version, + argparse_kwargs=None): + """Return an argparse option parser for this application. + + Subclasses may override this method to extend + the parser with more global options. + + :param description: full description of the application + :paramtype description: str + :param version: version number for the application + :paramtype version: str + :param argparse_kwargs: extra keyword argument passed to the + ArgumentParser constructor + :paramtype extra_kwargs: dict + """ + argparse_kwargs = argparse_kwargs or {} + + parser = argparse.ArgumentParser( + description=description, + add_help=False, + formatter_class=OpenStackHelpFormatter, + **argparse_kwargs + ) + + parser.add_argument( + '--version', + action='version', + version='%(prog)s {0}'.format(version), + help='Show program\'s version number and exit.' + ) + + parser.add_argument( + '-v', '--verbose', + action='count', + dest='verbose_level', + default=self.DEFAULT_VERBOSE_LEVEL, + help='Increase verbosity of output. Can be repeated.', + ) + + parser.add_argument( + '--log-file', + action='store', + default=None, + help='Specify a file to log output. Disabled by default.', + ) + + parser.add_argument( + '-q', '--quiet', + action='store_const', + dest='verbose_level', + const=0, + help='Suppress output except warnings and errors.', + ) + + parser.add_argument( + '-h', '--help', + action=HelpAction, + nargs=0, + default=self, # tricky + help="Show this help message and exit.", + ) + + parser.add_argument( + '--debug', + default=False, + action='store_true', + help='Show tracebacks on errors.', + ) + + parser.add_argument( + '--dcmanager-url', + action='store', + dest='dcmanager_url', + default=c.env('DCMANAGER_URL'), + help='DC Manager API host (Env: DCMANAGER_URL)' + ) + + parser.add_argument( + '--dcmanager-api-version', + action='store', + dest='dcmanager_version', + default=c.env('DCMANAGER_API_VERSION', default='v1.0'), + help='DC Manager API version (default = v1.0) (Env: ' + 'DCMANAGER_API_VERSION)' + ) + + parser.add_argument( + '--dcmanager-service-type', + action='store', + dest='service_type', + default=c.env('DCMANAGER_SERVICE_TYPE', + default='dcmanager'), + help='DC Manager service-type (should be the same name as in ' + 'keystone-endpoint) (default = dcmanager) (Env: ' + 'DCMANAGER_SERVICE_TYPE)' + ) + + parser.add_argument( + '--os-endpoint-type', + action='store', + dest='endpoint_type', + default=c.env('OS_ENDPOINT_TYPE', + default='internalURL'), + help='DC Manager endpoint-type (should be the same name as in ' + 'keystone-endpoint) (default = OS_ENDPOINT_TYPE)' + ) + + parser.add_argument( + '--os-username', + action='store', + dest='username', + default=c.env('OS_USERNAME', default='admin'), + help='Authentication username (Env: OS_USERNAME)' + ) + + parser.add_argument( + '--os-password', + action='store', + dest='password', + default=c.env('OS_PASSWORD'), + help='Authentication password (Env: OS_PASSWORD)' + ) + + parser.add_argument( + '--os-tenant-id', + action='store', + dest='tenant_id', + default=c.env('OS_TENANT_ID', 'OS_PROJECT_ID'), + help='Authentication tenant identifier (Env: OS_TENANT_ID)' + ) + + parser.add_argument( + '--os-project-id', + action='store', + dest='project_id', + default=c.env('OS_TENANT_ID', 'OS_PROJECT_ID'), + help='Authentication project identifier (Env: OS_TENANT_ID' + ' or OS_PROJECT_ID), will use tenant_id if both tenant_id' + ' and project_id are set' + ) + + parser.add_argument( + '--os-tenant-name', + action='store', + dest='tenant_name', + default=c.env('OS_TENANT_NAME', 'OS_PROJECT_NAME'), + help='Authentication tenant name (Env: OS_TENANT_NAME)' + ) + + parser.add_argument( + '--os-project-name', + action='store', + dest='project_name', + default=c.env('OS_TENANT_NAME', 'OS_PROJECT_NAME'), + help='Authentication project name (Env: OS_TENANT_NAME' + ' or OS_PROJECT_NAME), will use tenant_name if both' + ' tenant_name and project_name are set' + ) + + parser.add_argument( + '--os-auth-token', + action='store', + dest='token', + default=c.env('OS_AUTH_TOKEN'), + help='Authentication token (Env: OS_AUTH_TOKEN)' + ) + + parser.add_argument( + '--os-project-domain-name', + action='store', + dest='project_domain_name', + default=c.env('OS_PROJECT_DOMAIN_NAME'), + help='Authentication project domain name or ID' + ' (Env: OS_PROJECT_DOMAIN_NAME)' + ) + + parser.add_argument( + '--os-project-domain-id', + action='store', + dest='project_domain_id', + default=c.env('OS_PROJECT_DOMAIN_ID'), + help='Authentication project domain ID' + ' (Env: OS_PROJECT_DOMAIN_ID)' + ) + + parser.add_argument( + '--os-user-domain-name', + action='store', + dest='user_domain_name', + default=c.env('OS_USER_DOMAIN_NAME'), + help='Authentication user domain name' + ' (Env: OS_USER_DOMAIN_NAME)' + ) + + parser.add_argument( + '--os-user-domain-id', + action='store', + dest='user_domain_id', + default=c.env('OS_USER_DOMAIN_ID'), + help='Authentication user domain name' + ' (Env: OS_USER_DOMAIN_ID)' + ) + + parser.add_argument( + '--os-auth-url', + action='store', + dest='auth_url', + default=c.env('OS_AUTH_URL'), + help='Authentication URL (Env: OS_AUTH_URL)' + ) + + parser.add_argument( + '--os-cacert', + action='store', + dest='cacert', + default=c.env('OS_CACERT'), + help='Authentication CA Certificate (Env: OS_CACERT)' + ) + + parser.add_argument( + '--insecure', + action='store_true', + dest='insecure', + default=c.env('DCMANAGERCLIENT_INSECURE', default=False), + help='Disables SSL/TLS certificate verification ' + '(Env: DCMANAGERCLIENT_INSECURE)' + ) + + parser.add_argument( + '--profile', + dest='profile', + metavar='HMAC_KEY', + help='HMAC key to use for encrypting context data for performance ' + 'profiling of operation. This key should be one of the ' + 'values configured for the osprofiler middleware in ' + 'dcmanager, it is specified in the profiler section of the ' + 'dcmanager configuration ' + '(i.e. /etc/dcmanager/dcmanager.conf). ' + 'Without the key, profiling will not be triggered even if ' + 'osprofiler is enabled on the server side.' + ) + + return parser + + def initialize_app(self, argv): + self._clear_shell_commands() + + ver = client.determine_client_version(self.options.dcmanager_version) + + self._set_shell_commands(self._get_commands(ver)) + + do_help = ['help', '-h', 'bash-completion', 'complete'] + + # bash-completion should not require authentication. + skip_auth = ''.join(argv) in do_help + + if skip_auth: + self.options.auth_url = None + + if self.options.auth_url and not self.options.token \ + and not skip_auth: + if not self.options.tenant_name: + raise exceptions.CommandError( + ("You must provide a tenant_name " + "via --os-tenantname env[OS_TENANT_NAME]") + ) + if not self.options.username: + raise exceptions.CommandError( + ("You must provide a username " + "via --os-username env[OS_USERNAME]") + ) + + if not self.options.password: + raise exceptions.CommandError( + ("You must provide a password " + "via --os-password env[OS_PASSWORD]") + ) + + kwargs = { + 'user_domain_name': self.options.user_domain_name, + 'user_domain_id': self.options.user_domain_id, + 'project_domain_name': self.options.project_domain_name, + 'project_domain_id': self.options.project_domain_id + } + + self.client = client.client( + dcmanager_url=self.options.dcmanager_url, + username=self.options.username, + api_key=self.options.password, + project_name=self.options.tenant_name or self.options.project_name, + auth_url=self.options.auth_url, + project_id=self.options.tenant_id, + endpoint_type=self.options.endpoint_type, + service_type=self.options.service_type, + auth_token=self.options.token, + cacert=self.options.cacert, + insecure=self.options.insecure, + profile=self.options.profile, + **kwargs + ) + + if not self.options.auth_url and not skip_auth: + raise exceptions.CommandError( + ("You must provide an auth url via either " + "--os-auth-url or env[OS_AUTH_URL] or " + "specify an auth_system which defines a" + " default url with --os-auth-system or env[OS_AUTH_SYSTEM]") + ) + + # Adding client_manager variable to make dcmanager client work with + # unified OpenStack client. + ClientManager = type( + 'ClientManager', + (object,), + dict(subcloud_manager=self.client, + alarm_manager=self.client, + sw_update_manager=self.client, + strategy_step_manager=self.client, + sw_update_options_manager=self.client) + ) + self.client_manager = ClientManager() + + def _set_shell_commands(self, cmds_dict): + for k, v in cmds_dict.items(): + self.command_manager.add_command(k, v) + + def _clear_shell_commands(self): + exclude_cmds = ['help', 'complete'] + + cmds = self.command_manager.commands.copy() + for k, v in cmds.items(): + if k not in exclude_cmds: + self.command_manager.commands.pop(k) + + def _get_commands(self, version): + if version == 1: + return self._get_commands_v1() + + return {} + + @staticmethod + def _get_commands_v1(): + return { + 'bash-completion': BashCompletionCommand, + 'subcloud add': sm.AddSubcloud, + 'subcloud delete': sm.DeleteSubcloud, + 'subcloud list': sm.ListSubcloud, + 'subcloud show': sm.ShowSubcloud, + 'subcloud unmanage': sm.UnmanageSubcloud, + 'subcloud manage': sm.ManageSubcloud, + 'subcloud update': sm.UpdateSubcloud, + 'subcloud generate-config': sm.GenerateConfigSubcloud, + 'alarm summary': am.ListAlarmSummary, + 'patch-strategy create': sum.CreatePatchStrategy, + 'patch-strategy delete': sum.DeletePatchStrategy, + 'patch-strategy apply': sum.ApplyPatchStrategy, + 'patch-strategy abort': sum.AbortPatchStrategy, + 'patch-strategy show': sum.ShowPatchStrategy, + 'strategy-step list': sum.ListStrategyStep, + 'strategy-step show': sum.ShowStrategyStep, + 'patch-strategy-config update': suom.UpdateSwUpdateOptions, + 'patch-strategy-config list': suom.ListSwUpdateOptions, + 'patch-strategy-config show': suom.ShowSwUpdateOptions, + 'patch-strategy-config delete': suom.DeleteSwUpdateOptions, + } + + +def main(argv=sys.argv[1:]): + return DCManagerShell().run(argv) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/dcmanagerclient/tests/__init__.py b/dcmanagerclient/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dcmanagerclient/tests/base.py b/dcmanagerclient/tests/base.py new file mode 100644 index 0000000..c2f9866 --- /dev/null +++ b/dcmanagerclient/tests/base.py @@ -0,0 +1,88 @@ +# Copyright 2013 - Mirantis, Inc. +# Copyright 2016 - Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import json + +import mock +import unittest2 + + +class FakeResponse(object): + """Fake response for testing DC Manager Client.""" + + def __init__(self, status_code, content=None): + self.status_code = status_code + self.content = content + self.headers = {} + + def json(self): + return json.loads(self.content) + + +class BaseClientTest(unittest2.TestCase): + _client = None + + def mock_http_get(self, content, status_code=200): + if isinstance(content, dict): + content = json.dumps(content) + + self._client.http_client.get = mock.MagicMock( + return_value=FakeResponse(status_code, content)) + + return self._client.http_client.get + + def mock_http_post(self, content, status_code=201): + if isinstance(content, dict): + content = json.dumps(content) + + self._client.http_client.post = mock.MagicMock( + return_value=FakeResponse(status_code, content)) + + return self._client.http_client.post + + def mock_http_put(self, content, status_code=200): + if isinstance(content, dict): + content = json.dumps(content) + + self._client.http_client.put = mock.MagicMock( + return_value=FakeResponse(status_code, content)) + + return self._client.http_client.put + + def mock_http_delete(self, status_code=204): + self._client.http_client.delete = mock.MagicMock( + return_value=FakeResponse(status_code)) + + return self._client.http_client.delete + + +class BaseCommandTest(unittest2.TestCase): + def setUp(self): + self.app = mock.Mock() + self.client = self.app.client_manager.subcloud_manager + + def call(self, command, app_args=[], prog_name=''): + cmd = command(self.app, app_args) + + parsed_args = cmd.get_parser(prog_name).parse_args(app_args) + + return cmd.take_action(parsed_args) diff --git a/dcmanagerclient/tests/base_shell_test.py b/dcmanagerclient/tests/base_shell_test.py new file mode 100644 index 0000000..aa8195b --- /dev/null +++ b/dcmanagerclient/tests/base_shell_test.py @@ -0,0 +1,55 @@ +# Copyright 2015 Huawei Technologies Co., Ltd. +# Copyright 2016 Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import os +import sys + +import six +import testtools + +from dcmanagerclient import shell + + +class BaseShellTests(testtools.TestCase): + + def shell(self, argstr): + orig = (sys.stdout, sys.stderr) + clean_env = {} + _old_env, os.environ = os.environ, clean_env.copy() + + try: + sys.stdout = six.moves.cStringIO() + sys.stderr = six.moves.cStringIO() + _shell = shell.DCManagerShell() + _shell.run(argstr.split()) + except SystemExit: + exc_type, exc_value, exc_traceback = sys.exc_info() + self.assertEqual(0, exc_value.code) + finally: + stdout = sys.stdout.getvalue() + stderr = sys.stderr.getvalue() + sys.stdout.close() + sys.stderr.close() + sys.stdout, sys.stderr = orig + os.environ = _old_env + + return stdout, stderr diff --git a/dcmanagerclient/tests/test_client.py b/dcmanagerclient/tests/test_client.py new file mode 100644 index 0000000..1ea1b7f --- /dev/null +++ b/dcmanagerclient/tests/test_client.py @@ -0,0 +1,233 @@ +# Copyright 2015 - Huawei Technologies Co., Ltd. +# Copyright 2016 - StackStorm, Inc. +# Copyright 2016 - Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import os +import tempfile +import uuid + +import mock +import testtools + +import osprofiler.profiler + +from dcmanagerclient.api import client + +AUTH_HTTP_URL = 'http://localhost:35357/v3' +AUTH_HTTPS_URL = AUTH_HTTP_URL.replace('http', 'https') +DCMANAGER_HTTP_URL = 'http://localhost:8119/v1.0' +DCMANAGER_HTTPS_URL = DCMANAGER_HTTP_URL.replace('http', 'https') +PROFILER_HMAC_KEY = 'SECRET_HMAC_KEY' +FAKE_KWARGS = {'user_domain_name': 'fake_user_domain_name', + 'user_domain_id': 'fake_user_domain_id', + 'project_domain_name': 'fake_project_domain_name', + 'project_domain_id': 'fake_project_domain_id'} + + +class BaseClientTests(testtools.TestCase): + @mock.patch('keystoneauth1.session.Session') + @mock.patch('dcmanagerclient.api.httpclient.HTTPClient') + def test_dcmanager_url_default(self, mock, mock_keystone_auth_session): + keystone_session_instance = mock_keystone_auth_session.return_value + token = keystone_session_instance.get_token.return_value = \ + str(uuid.uuid4()) + project_id = keystone_session_instance.get_project_id.return_value = \ + str(uuid.uuid4()) + user_id = keystone_session_instance.get_user_id.return_value = \ + str(uuid.uuid4()) + keystone_session_instance.get_endpoint.return_value = \ + DCMANAGER_HTTP_URL + + expected_args = ( + DCMANAGER_HTTP_URL, token, project_id, user_id) + + expected_kwargs = { + 'cacert': None, + 'insecure': False + } + + client.client(username='dcmanager', project_name='dcmanager', + auth_url=AUTH_HTTP_URL, api_key='password', + **FAKE_KWARGS) + self.assertTrue(mock.called) + self.assertEqual(mock.call_args[0], expected_args) + self.assertDictEqual(mock.call_args[1], expected_kwargs) + + @mock.patch('keystoneauth1.session.Session') + @mock.patch('dcmanagerclient.api.httpclient.HTTPClient') + def test_dcmanager_url_https_insecure(self, mock, + mock_keystone_auth_session): + keystone_session_instance = mock_keystone_auth_session.return_value + token = keystone_session_instance.get_token.return_value = \ + str(uuid.uuid4()) + project_id = keystone_session_instance.get_project_id.return_value = \ + str(uuid.uuid4()) + user_id = keystone_session_instance.get_user_id.return_value = \ + str(uuid.uuid4()) + keystone_session_instance.get_endpoint.return_value = \ + DCMANAGER_HTTP_URL + + expected_args = (DCMANAGER_HTTPS_URL, token, project_id, user_id) + + expected_kwargs = { + 'cacert': None, + 'insecure': True + } + + client.client(dcmanager_url=DCMANAGER_HTTPS_URL, username='dcmanager', + project_name='dcmanager', auth_url=AUTH_HTTP_URL, + api_key='password', cacert=None, insecure=True, + **FAKE_KWARGS) + + self.assertTrue(mock.called) + self.assertEqual(mock.call_args[0], expected_args) + self.assertDictEqual(mock.call_args[1], expected_kwargs) + + @mock.patch('keystoneauth1.session.Session') + @mock.patch('dcmanagerclient.api.httpclient.HTTPClient') + def test_dcmanager_url_https_secure(self, mock, + mock_keystone_auth_session): + fd, path = tempfile.mkstemp(suffix='.pem') + keystone_session_instance = mock_keystone_auth_session.return_value + token = keystone_session_instance.get_token.return_value = \ + str(uuid.uuid4()) + project_id = keystone_session_instance.get_project_id.return_value = \ + str(uuid.uuid4()) + user_id = keystone_session_instance.get_user_id.return_value = \ + str(uuid.uuid4()) + keystone_session_instance.get_endpoint.return_value = \ + DCMANAGER_HTTPS_URL + + expected_args = (DCMANAGER_HTTPS_URL, token, project_id, user_id) + + expected_kwargs = { + 'cacert': path, + 'insecure': False + } + + try: + client.client( + dcmanager_url=DCMANAGER_HTTPS_URL, + username='dcmanager', + project_name='dcmanager', + auth_url=AUTH_HTTP_URL, + api_key='password', + cacert=path, + insecure=False, **FAKE_KWARGS) + finally: + os.close(fd) + os.unlink(path) + + self.assertTrue(mock.called) + self.assertEqual(mock.call_args[0], expected_args) + self.assertDictEqual(mock.call_args[1], expected_kwargs) + + @mock.patch('keystoneauth1.session.Session') + def test_dcmanager_url_https_bad_cacert(self, mock_keystone_auth_session): + self.assertRaises( + ValueError, + client.client, + dcmanager_url=DCMANAGER_HTTPS_URL, + username='dcmanager', + project_name='dcmanager', + api_key='password', + auth_url=AUTH_HTTP_URL, + cacert='/path/to/foobar', + insecure=False, **FAKE_KWARGS) + + @mock.patch('logging.Logger.warning') + @mock.patch('keystoneauth1.session.Session') + def test_dcmanager_url_https_bad_insecure(self, mock_keystone_auth_session, + log_warning_mock): + fd, path = tempfile.mkstemp(suffix='.pem') + + try: + client.client( + dcmanager_url=DCMANAGER_HTTPS_URL, + username='dcmanager', + project_name='dcmanager', + api_key='password', + auth_url=AUTH_HTTP_URL, + cacert=path, + insecure=True, + **FAKE_KWARGS) + finally: + os.close(fd) + os.unlink(path) + + self.assertTrue(log_warning_mock.called) + + @mock.patch('keystoneauth1.session.Session') + @mock.patch('dcmanagerclient.api.httpclient.HTTPClient') + def test_dcmanager_profile_enabled(self, mock, mock_keystone_auth_session): + keystone_session_instance = mock_keystone_auth_session.return_value + token = keystone_session_instance.get_token.return_value = \ + str(uuid.uuid4()) + project_id = keystone_session_instance.get_project_id.return_value = \ + str(uuid.uuid4()) + user_id = keystone_session_instance.get_user_id.return_value = \ + str(uuid.uuid4()) + keystone_session_instance.get_endpoint.return_value = \ + DCMANAGER_HTTP_URL + + expected_args = (DCMANAGER_HTTP_URL, token, project_id, user_id) + + expected_kwargs = { + 'cacert': None, + 'insecure': False + } + + client.client( + username='dcmanager', + project_name='dcmanager', + auth_url=AUTH_HTTP_URL, + api_key='password', + profile=PROFILER_HMAC_KEY, + **FAKE_KWARGS) + + self.assertTrue(mock.called) + self.assertEqual(mock.call_args[0], expected_args) + self.assertDictEqual(mock.call_args[1], expected_kwargs) + + profiler = osprofiler.profiler.get() + + self.assertEqual(profiler.hmac_key, PROFILER_HMAC_KEY) + + def test_no_api_key(self): + self.assertRaises(RuntimeError, client.client, + dcmanager_url=DCMANAGER_HTTP_URL, + username='dcmanager', project_name='dcmanager', + auth_url=AUTH_HTTP_URL, **FAKE_KWARGS) + + def test_project_name_and_project_id(self): + self.assertRaises(RuntimeError, client.client, + dcmanager_url=DCMANAGER_HTTP_URL, + username='dcmanager', project_name='dcmanager', + project_id=str(uuid.uuid4()), + auth_url=AUTH_HTTP_URL, **FAKE_KWARGS) + + def test_user_name_and_user_id(self): + self.assertRaises(RuntimeError, client.client, + dcmanager_url=DCMANAGER_HTTP_URL, + username='dcmanager', project_name='dcmanager', + user_id=str(uuid.uuid4()), + auth_url=AUTH_HTTP_URL, **FAKE_KWARGS) diff --git a/dcmanagerclient/tests/test_help_and_bash_completion.py b/dcmanagerclient/tests/test_help_and_bash_completion.py new file mode 100644 index 0000000..c577f04 --- /dev/null +++ b/dcmanagerclient/tests/test_help_and_bash_completion.py @@ -0,0 +1,45 @@ +# Copyright 2016 Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import re + +from testtools import matchers + +from dcmanagerclient.tests import base_shell_test as base + + +class TestCLIBashCompletionV1(base.BaseShellTests): + def test_bash_completion(self): + bash_completion, stderr = self.shell('bash-completion') + self.assertIn('bash-completion', bash_completion) + self.assertFalse(stderr) + + +class TestCLIHelp(base.BaseShellTests): + def test_help(self): + required = [ + '.*?^usage: ', + '.*?^\s+help\s+print detailed help for another command' + ] + kb_help, stderr = self.shell('help') + for r in required: + self.assertThat((kb_help + stderr), + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) diff --git a/dcmanagerclient/tests/test_httpclient.py b/dcmanagerclient/tests/test_httpclient.py new file mode 100644 index 0000000..66b40cb --- /dev/null +++ b/dcmanagerclient/tests/test_httpclient.py @@ -0,0 +1,303 @@ +# Copyright 2016 - StackStorm, Inc. +# Copyright 2016 - Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import copy +import uuid + +import mock +import requests +import testtools + +from osprofiler import _utils as osprofiler_utils +import osprofiler.profiler + +from dcmanagerclient.api import httpclient + +API_BASE_URL = 'http://localhost:8119/v1.0' +API_URL = '/os-quota-sets' + +EXPECTED_URL = API_BASE_URL + API_URL + +AUTH_TOKEN = str(uuid.uuid4()) +PROJECT_ID = str(uuid.uuid4()) +USER_ID = str(uuid.uuid4()) +PROFILER_HMAC_KEY = 'SECRET_HMAC_KEY' +PROFILER_TRACE_ID = str(uuid.uuid4()) + +EXPECTED_AUTH_HEADERS = { + 'x-auth-token': AUTH_TOKEN, + 'X-Project-Id': PROJECT_ID, + 'X-User-Id': USER_ID +} + +EXPECTED_REQ_OPTIONS = { + 'headers': EXPECTED_AUTH_HEADERS +} + +EXPECTED_BODY = { + 'k1': 'abc', + 'k2': 123, + 'k3': True +} + + +class FakeRequest(object): + def __init__(self, method): + self.method = method + + +class FakeResponse(object): + def __init__(self, method, url, status_code): + self.request = FakeRequest(method) + self.url = url + self.status_code = status_code + + +class HTTPClientTest(testtools.TestCase): + def setUp(self): + super(HTTPClientTest, self).setUp() + osprofiler.profiler.init(None) + self.client = httpclient.HTTPClient( + API_BASE_URL, + AUTH_TOKEN, + PROJECT_ID, + USER_ID + ) + + @mock.patch.object( + requests, + 'get', + mock.MagicMock(return_value=FakeResponse('get', EXPECTED_URL, 200)) + ) + def test_get_request_options(self): + self.client.get(API_URL) + + requests.get.assert_called_with( + EXPECTED_URL, + **EXPECTED_REQ_OPTIONS + ) + + @mock.patch.object( + requests, + 'get', + mock.MagicMock(return_value=FakeResponse('get', EXPECTED_URL, 200)) + ) + def test_get_request_options_with_headers_for_get(self): + headers = {'foo': 'bar'} + + self.client.get(API_URL, headers=headers) + + expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS) + expected_options['headers'].update(headers) + + requests.get.assert_called_with( + EXPECTED_URL, + **expected_options + ) + + @mock.patch.object( + osprofiler.profiler._Profiler, + 'get_base_id', + mock.MagicMock(return_value=PROFILER_TRACE_ID) + ) + @mock.patch.object( + osprofiler.profiler._Profiler, + 'get_id', + mock.MagicMock(return_value=PROFILER_TRACE_ID) + ) + @mock.patch.object( + requests, + 'get', + mock.MagicMock(return_value=FakeResponse('get', EXPECTED_URL, 200)) + ) + def test_get_request_options_with_profile_enabled(self): + osprofiler.profiler.init(PROFILER_HMAC_KEY) + + data = {'base_id': PROFILER_TRACE_ID, 'parent_id': PROFILER_TRACE_ID} + signed_data = osprofiler_utils.signed_pack(data, PROFILER_HMAC_KEY) + + headers = { + 'X-Trace-Info': signed_data[0], + 'X-Trace-HMAC': signed_data[1] + } + + self.client.get(API_URL) + + expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS) + expected_options['headers'].update(headers) + + requests.get.assert_called_with( + EXPECTED_URL, + **expected_options + ) + + @mock.patch.object( + requests, + 'post', + mock.MagicMock(return_value=FakeResponse('post', EXPECTED_URL, 201)) + ) + def test_get_request_options_with_headers_for_post(self): + headers = {'foo': 'bar'} + + self.client.post(API_URL, EXPECTED_BODY, headers=headers) + + expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS) + expected_options['headers'].update(headers) + expected_options['headers']['content-type'] = 'application/json' + + requests.post.assert_called_with( + EXPECTED_URL, + EXPECTED_BODY, + **expected_options + ) + + @mock.patch.object( + requests, + 'put', + mock.MagicMock(return_value=FakeResponse('put', EXPECTED_URL, 200)) + ) + def test_get_request_options_with_headers_for_put(self): + headers = {'foo': 'bar'} + + self.client.put(API_URL, EXPECTED_BODY, headers=headers) + + expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS) + expected_options['headers'].update(headers) + expected_options['headers']['content-type'] = 'application/json' + + requests.put.assert_called_with( + EXPECTED_URL, + EXPECTED_BODY, + **expected_options + ) + + @mock.patch.object( + requests, + 'delete', + mock.MagicMock(return_value=FakeResponse('delete', EXPECTED_URL, 200)) + ) + def test_get_request_options_with_headers_for_delete(self): + headers = {'foo': 'bar'} + + self.client.delete(API_URL, headers=headers) + + expected_options = copy.deepcopy(EXPECTED_REQ_OPTIONS) + expected_options['headers'].update(headers) + + requests.delete.assert_called_with( + EXPECTED_URL, + **expected_options + ) + + @mock.patch.object( + httpclient.HTTPClient, + '_get_request_options', + mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS)) + ) + @mock.patch.object( + requests, + 'get', + mock.MagicMock(return_value=FakeResponse('get', EXPECTED_URL, 200)) + ) + def test_http_get(self): + self.client.get(API_URL) + + httpclient.HTTPClient._get_request_options.assert_called_with( + 'get', + None + ) + + requests.get.assert_called_with( + EXPECTED_URL, + **EXPECTED_REQ_OPTIONS + ) + + @mock.patch.object( + httpclient.HTTPClient, + '_get_request_options', + mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS)) + ) + @mock.patch.object( + requests, + 'post', + mock.MagicMock(return_value=FakeResponse('post', EXPECTED_URL, 201)) + ) + def test_http_post(self): + self.client.post(API_URL, EXPECTED_BODY) + + httpclient.HTTPClient._get_request_options.assert_called_with( + 'post', + None + ) + + requests.post.assert_called_with( + EXPECTED_URL, + EXPECTED_BODY, + **EXPECTED_REQ_OPTIONS + ) + + @mock.patch.object( + httpclient.HTTPClient, + '_get_request_options', + mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS)) + ) + @mock.patch.object( + requests, + 'put', + mock.MagicMock(return_value=FakeResponse('put', EXPECTED_URL, 200)) + ) + def test_http_put(self): + self.client.put(API_URL, EXPECTED_BODY) + + httpclient.HTTPClient._get_request_options.assert_called_with( + 'put', + None + ) + + requests.put.assert_called_with( + EXPECTED_URL, + EXPECTED_BODY, + **EXPECTED_REQ_OPTIONS + ) + + @mock.patch.object( + httpclient.HTTPClient, + '_get_request_options', + mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS)) + ) + @mock.patch.object( + requests, + 'delete', + mock.MagicMock(return_value=FakeResponse('delete', EXPECTED_URL, 200)) + ) + def test_http_delete(self): + self.client.delete(API_URL) + + httpclient.HTTPClient._get_request_options.assert_called_with( + 'delete', + None + ) + + requests.delete.assert_called_with( + EXPECTED_URL, + **EXPECTED_REQ_OPTIONS + ) diff --git a/dcmanagerclient/tests/test_shell.py b/dcmanagerclient/tests/test_shell.py new file mode 100644 index 0000000..a9bf5eb --- /dev/null +++ b/dcmanagerclient/tests/test_shell.py @@ -0,0 +1,196 @@ +# Copyright 2016 EricssonAB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import mock + +from dcmanagerclient.tests import base_shell_test as base + + +class TestShell(base.BaseShellTests): + + @mock.patch('dcmanagerclient.api.client.determine_client_version') + def test_dcmanager_version(self, mock): + self.shell( + '--os-dcmanager-version=v1 quota-defaults' + ) + self.assertTrue(mock.called) + dcmanager_version = mock.call_args + self.assertEqual('v1', dcmanager_version[0][0]) + + @mock.patch('dcmanagerclient.api.client.determine_client_version') + def test_default_dcmanager_version(self, mock): + default_version = 'v1.0' + self.shell('quota defaults') + self.assertTrue(mock.called) + dcmanager_version = mock.call_args + self.assertEqual(default_version, dcmanager_version[0][0]) + + @mock.patch('dcmanagerclient.api.client.client') + def test_env_variables(self, mock): + self.shell( + '--os-auth-url=https://127.0.0.1:35357/v3 ' + '--os-username=admin ' + '--os-password=1234 ' + '--os-tenant-name=admin ' + 'quota defaults' + ) + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('https://127.0.0.1:35357/v3', params[1]['auth_url']) + self.assertEqual('admin', params[1]['username']) + self.assertEqual('admin', params[1]['project_name']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_env_without_auth_url(self, mock): + self.shell( + '--os-username=admin ' + '--os-password=1234 ' + '--os-tenant-name=admin ' + 'quota defaults' + ) + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('', params[1]['auth_url']) + self.assertEqual('admin', params[1]['username']) + self.assertEqual('admin', params[1]['project_name']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_kb_service_type(self, mock): + self.shell('--os-service-type=dcmanager') + self.assertTrue(mock.called) + parameters = mock.call_args + self.assertEqual('dcmanager', parameters[1]['service_type']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_kb_default_service_type(self, mock): + self.shell('quota defaults') + self.assertTrue(mock.called) + params = mock.call_args + # Default service type is dcmanager + self.assertEqual('dcmanager', params[1]['service_type']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_kb_endpoint_type(self, mock): + self.shell('--os-dcmanager-endpoint-type=adminURL quota-defaults') + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('adminURL', params[1]['endpoint_type']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_kb_default_endpoint_type(self, mock): + self.shell('quota defaults') + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('internalURL', params[1]['endpoint_type']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_os_auth_token(self, mock): + self.shell( + '--os-auth-token=abcd1234 ' + 'quota defaults' + ) + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('abcd1234', params[1]['auth_token']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_command_without_dcmanager_url(self, mock): + self.shell( + 'quota defaults' + ) + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('', params[1]['dcmanager_url']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_command_with_dcmanager_url(self, mock): + self.shell( + '--os-dcmanager-url=http://localhost:8118/v1 quota-defaults' + ) + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('http://localhost:8118/v1', + params[1]['dcmanager_url']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_command_without_project_name(self, mock): + self.shell( + 'quota defaults' + ) + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('', params[1]['project_name']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_dcmanager_profile(self, mock): + self.shell('--profile=SECRET_HMAC_KEY quota defaults') + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('SECRET_HMAC_KEY', params[1]['profile']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_dcmanager_without_profile(self, mock): + self.shell('quota defaults') + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual(None, params[1]['profile']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_dcmanager_project_name(self, mock): + self.shell('--os-project-name default quota defaults') + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('default', params[1]['project_name']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_dcmanager_tenant_name(self, mock): + self.shell('--os-tenant-name default quota defaults') + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('default', params[1]['project_name']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_dcmanager_project_domain_name(self, mock): + self.shell('--os-project-domain-name default quota defaults') + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('default', params[1]['project_domain_name']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_dcmanager_project_domain_id(self, mock): + self.shell('--os-project-domain-id default quota defaults') + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('default', params[1]['project_domain_id']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_dcmanager_user_domain_name(self, mock): + self.shell('--os-user-domain-name default quota defaults') + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('default', params[1]['user_domain_name']) + + @mock.patch('dcmanagerclient.api.client.client') + def test_dcmanager_user_domain_id(self, mock): + self.shell('--os-user-domain-id default quota defaults') + self.assertTrue(mock.called) + params = mock.call_args + self.assertEqual('default', params[1]['user_domain_id']) diff --git a/dcmanagerclient/tests/test_utils.py b/dcmanagerclient/tests/test_utils.py new file mode 100644 index 0000000..ef21b34 --- /dev/null +++ b/dcmanagerclient/tests/test_utils.py @@ -0,0 +1,64 @@ +# Copyright 2015 - StackStorm, Inc. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import json +import os.path +import tempfile +import testtools +import yaml + +from dcmanagerclient import utils + + +ENV_DICT = {'k1': 'abc', 'k2': 123, 'k3': True} +ENV_STR = json.dumps(ENV_DICT) +ENV_YAML = yaml.safe_dump(ENV_DICT, default_flow_style=False) + + +class UtilityTest(testtools.TestCase): + + def test_load_empty(self): + self.assertDictEqual(dict(), utils.load_content(None)) + self.assertDictEqual(dict(), utils.load_content('')) + self.assertDictEqual(dict(), utils.load_content('{}')) + self.assertListEqual(list(), utils.load_content('[]')) + + def test_load_json_content(self): + self.assertDictEqual(ENV_DICT, utils.load_content(ENV_STR)) + + def test_load_json_file(self): + with tempfile.NamedTemporaryFile() as f: + f.write(ENV_STR.encode('utf-8')) + f.flush() + file_path = os.path.abspath(f.name) + + self.assertDictEqual(ENV_DICT, utils.load_file(file_path)) + + def test_load_yaml_content(self): + self.assertDictEqual(ENV_DICT, utils.load_content(ENV_YAML)) + + def test_load_yaml_file(self): + with tempfile.NamedTemporaryFile() as f: + f.write(ENV_YAML.encode('utf-8')) + f.flush() + file_path = os.path.abspath(f.name) + + self.assertDictEqual(ENV_DICT, utils.load_file(file_path)) diff --git a/dcmanagerclient/tests/v1/__init__.py b/dcmanagerclient/tests/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dcmanagerclient/tests/v1/test_subcloud_manager.py b/dcmanagerclient/tests/v1/test_subcloud_manager.py new file mode 100644 index 0000000..26a3dbf --- /dev/null +++ b/dcmanagerclient/tests/v1/test_subcloud_manager.py @@ -0,0 +1,250 @@ +# Copyright (c) 2017 Ericsson AB. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import copy +import mock + +from oslo_utils import timeutils + +from dcmanagerclient.api.v1 import subcloud_manager as sm +from dcmanagerclient.commands.v1 import subcloud_manager as subcloud_cmd +from dcmanagerclient.tests import base + +TIME_NOW = timeutils.utcnow().isoformat() +ID = '1' +ID_1 = '2' +NAME = 'subcloud1' +DESCRIPTION = 'subcloud1 description' +LOCATION = 'subcloud1 location' +SOFTWARE_VERSION = '12.34' +MANAGEMENT_STATE = 'unmanaged' +AVAILABILITY_STATUS = 'offline' +MANAGEMENT_SUBNET = '192.168.101.0/24' +MANAGEMENT_START_IP = '192.168.101.2' +MANAGEMENT_END_IP = '192.168.101.50' +MANAGEMENT_GATEWAY_IP = '192.168.101.1' +SYSTEMCONTROLLER_GATEWAY_IP = '192.168.204.101' + +SUBCLOUD_DICT = { + 'SUBCLOUD_ID': ID, + 'NAME': NAME, + 'DESCRIPTION': DESCRIPTION, + 'LOCATION': LOCATION, + 'SOFTWARE_VERSION': SOFTWARE_VERSION, + 'MANAGEMENT_STATE': MANAGEMENT_STATE, + 'AVAILABILITY_STATUS': AVAILABILITY_STATUS, + 'MANAGEMENT_SUBNET': MANAGEMENT_SUBNET, + 'MANAGEMENT_START_IP': MANAGEMENT_START_IP, + 'MANAGEMENT_END_IP': MANAGEMENT_END_IP, + 'MANAGEMENT_GATEWAY_IP': MANAGEMENT_GATEWAY_IP, + 'SYSTEMCONTROLLER_GATEWAY_IP': SYSTEMCONTROLLER_GATEWAY_IP, + 'CREATED_AT': TIME_NOW, + 'UPDATED_AT': TIME_NOW +} + +SUBCLOUD = sm.Subcloud( + mock, + subcloud_id=SUBCLOUD_DICT['SUBCLOUD_ID'], + name=SUBCLOUD_DICT['NAME'], + description=SUBCLOUD_DICT['DESCRIPTION'], + location=SUBCLOUD_DICT['LOCATION'], + software_version=SUBCLOUD_DICT['SOFTWARE_VERSION'], + management_state=SUBCLOUD_DICT['MANAGEMENT_STATE'], + availability_status=SUBCLOUD_DICT['AVAILABILITY_STATUS'], + management_subnet=SUBCLOUD_DICT['MANAGEMENT_SUBNET'], + management_start_ip=SUBCLOUD_DICT['MANAGEMENT_START_IP'], + management_end_ip=SUBCLOUD_DICT['MANAGEMENT_END_IP'], + management_gateway_ip=SUBCLOUD_DICT['MANAGEMENT_GATEWAY_IP'], + systemcontroller_gateway_ip=SUBCLOUD_DICT['SYSTEMCONTROLLER_GATEWAY_IP'], + created_at=SUBCLOUD_DICT['CREATED_AT'], + updated_at=SUBCLOUD_DICT['UPDATED_AT']) + + +class TestCLISubcloudManagerV1(base.BaseCommandTest): + + def test_list_subclouds(self): + self.client.subcloud_manager.list_subclouds.return_value = [SUBCLOUD] + actual_call = self.call(subcloud_cmd.ListSubcloud) + self.assertEqual([(ID, NAME, MANAGEMENT_STATE, AVAILABILITY_STATUS, + "unknown")], + actual_call[1]) + + def test_negative_list_subclouds(self): + self.client.subcloud_manager.list_subclouds.return_value = [] + actual_call = self.call(subcloud_cmd.ListSubcloud) + self.assertEqual((('', '', '', '', + ''),), + actual_call[1]) + + def test_delete_subcloud_with_subcloud_id(self): + self.call(subcloud_cmd.DeleteSubcloud, app_args=[ID]) + self.client.subcloud_manager.delete_subcloud.\ + assert_called_once_with(ID) + + def test_delete_subcloud_without_subcloud_id(self): + self.assertRaises(SystemExit, self.call, + subcloud_cmd.DeleteSubcloud, app_args=[]) + + def test_show_subcloud_with_subcloud_id(self): + self.client.subcloud_manager.subcloud_detail.\ + return_value = [SUBCLOUD] + actual_call = self.call(subcloud_cmd.ShowSubcloud, app_args=[ID]) + self.assertEqual((ID, NAME, + DESCRIPTION, + LOCATION, + SOFTWARE_VERSION, + MANAGEMENT_STATE, + AVAILABILITY_STATUS, + MANAGEMENT_SUBNET, + MANAGEMENT_START_IP, + MANAGEMENT_END_IP, + MANAGEMENT_GATEWAY_IP, + SYSTEMCONTROLLER_GATEWAY_IP, + TIME_NOW, TIME_NOW), + actual_call[1]) + + def test_show_subcloud_negative(self): + self.client.subcloud_manager.subcloud_detail.return_value = [] + actual_call = self.call(subcloud_cmd.ShowSubcloud, app_args=[ID]) + self.assertEqual((('', '', '', '', + '', '', '', '', + '', '', '', '', + '', ''),), + actual_call[1]) + + def test_add_subcloud(self): + self.client.subcloud_manager.add_subcloud.\ + return_value = [SUBCLOUD] + actual_call = self.call( + subcloud_cmd.AddSubcloud, app_args=[ + '--name', NAME, + '--description', DESCRIPTION, + '--location', LOCATION, + '--management-subnet', MANAGEMENT_SUBNET, + '--management-start-ip', MANAGEMENT_START_IP, + '--management-end-ip', MANAGEMENT_END_IP, + '--management-gateway-ip', MANAGEMENT_GATEWAY_IP, + '--systemcontroller-gateway-ip', SYSTEMCONTROLLER_GATEWAY_IP]) + self.assertEqual((ID, NAME, DESCRIPTION, LOCATION, SOFTWARE_VERSION, + MANAGEMENT_STATE, AVAILABILITY_STATUS, + MANAGEMENT_SUBNET, MANAGEMENT_START_IP, + MANAGEMENT_END_IP, MANAGEMENT_GATEWAY_IP, + SYSTEMCONTROLLER_GATEWAY_IP, + TIME_NOW, TIME_NOW), actual_call[1]) + + def test_add_subcloud_no_optional_parameters(self): + subcloud = copy.copy(SUBCLOUD) + subcloud.description = '' + subcloud.location = '' + self.client.subcloud_manager.add_subcloud.\ + return_value = [subcloud] + actual_call = self.call( + subcloud_cmd.AddSubcloud, app_args=[ + '--name', NAME, + '--management-subnet', MANAGEMENT_SUBNET, + '--management-start-ip', MANAGEMENT_START_IP, + '--management-end-ip', MANAGEMENT_END_IP, + '--management-gateway-ip', MANAGEMENT_GATEWAY_IP, + '--systemcontroller-gateway-ip', SYSTEMCONTROLLER_GATEWAY_IP]) + self.assertEqual((ID, NAME, '', '', SOFTWARE_VERSION, + MANAGEMENT_STATE, AVAILABILITY_STATUS, + MANAGEMENT_SUBNET, MANAGEMENT_START_IP, + MANAGEMENT_END_IP, MANAGEMENT_GATEWAY_IP, + SYSTEMCONTROLLER_GATEWAY_IP, + TIME_NOW, TIME_NOW), actual_call[1]) + + def test_add_subcloud_without_name(self): + self.client.subcloud_manager.add_subcloud.\ + return_value = [SUBCLOUD] + self.assertRaises( + SystemExit, self.call, subcloud_cmd.AddSubcloud, app_args=[ + '--description', DESCRIPTION, + '--location', LOCATION, + '--management-subnet', MANAGEMENT_SUBNET, + '--management-start-ip', MANAGEMENT_START_IP, + '--management-end-ip', MANAGEMENT_END_IP, + '--management-gateway-ip', MANAGEMENT_GATEWAY_IP, + '--systemcontroller-gateway-ip', SYSTEMCONTROLLER_GATEWAY_IP]) + + def test_unmanage_subcloud(self): + self.client.subcloud_manager.update_subcloud.\ + return_value = [SUBCLOUD] + actual_call = self.call( + subcloud_cmd.UnmanageSubcloud, app_args=[ID]) + self.assertEqual((ID, NAME, + DESCRIPTION, LOCATION, + SOFTWARE_VERSION, MANAGEMENT_STATE, + AVAILABILITY_STATUS, + MANAGEMENT_SUBNET, MANAGEMENT_START_IP, + MANAGEMENT_END_IP, MANAGEMENT_GATEWAY_IP, + SYSTEMCONTROLLER_GATEWAY_IP, + TIME_NOW, TIME_NOW), actual_call[1]) + + def test_unmanage_subcloud_without_subcloud_id(self): + self.assertRaises(SystemExit, self.call, + subcloud_cmd.UnmanageSubcloud, app_args=[]) + + def test_manage_subcloud(self): + self.client.subcloud_manager.update_subcloud.\ + return_value = [SUBCLOUD] + actual_call = self.call( + subcloud_cmd.ManageSubcloud, app_args=[ID]) + self.assertEqual((ID, NAME, + DESCRIPTION, LOCATION, + SOFTWARE_VERSION, MANAGEMENT_STATE, + AVAILABILITY_STATUS, + MANAGEMENT_SUBNET, MANAGEMENT_START_IP, + MANAGEMENT_END_IP, MANAGEMENT_GATEWAY_IP, + SYSTEMCONTROLLER_GATEWAY_IP, + TIME_NOW, TIME_NOW), actual_call[1]) + + def test_manage_subcloud_without_subcloud_id(self): + self.assertRaises(SystemExit, self.call, + subcloud_cmd.ManageSubcloud, app_args=[]) + + def test_update_subcloud(self): + self.client.subcloud_manager.update_subcloud.\ + return_value = [SUBCLOUD] + actual_call = self.call( + subcloud_cmd.UpdateSubcloud, + app_args=[ID, + '--description', 'subcloud description', + '--location', 'subcloud location']) + self.assertEqual((ID, NAME, + DESCRIPTION, LOCATION, + SOFTWARE_VERSION, MANAGEMENT_STATE, + AVAILABILITY_STATUS, + MANAGEMENT_SUBNET, MANAGEMENT_START_IP, + MANAGEMENT_END_IP, MANAGEMENT_GATEWAY_IP, + SYSTEMCONTROLLER_GATEWAY_IP, + TIME_NOW, TIME_NOW), actual_call[1]) + + def test_generate_config_subcloud(self): + FAKE_CONFIG = "This is a fake config file." + self.client.subcloud_manager.generate_config_subcloud.\ + return_value = FAKE_CONFIG + actual_call = self.call( + subcloud_cmd.GenerateConfigSubcloud, app_args=[ID]) + self.assertEqual(FAKE_CONFIG, actual_call) + + def test_generate_config_subcloud_without_subcloud_id(self): + self.assertRaises(SystemExit, self.call, + subcloud_cmd.GenerateConfigSubcloud, app_args=[]) diff --git a/dcmanagerclient/utils.py b/dcmanagerclient/utils.py new file mode 100644 index 0000000..72d761c --- /dev/null +++ b/dcmanagerclient/utils.py @@ -0,0 +1,89 @@ +# Copyright 2016 - Ericsson AB +# Copyright 2015 - Huawei Technologies Co. Ltd +# Copyright 2015 - StackStorm, Inc. +# +# 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. +# +# Copyright (c) 2017 Wind River Systems, Inc. +# +# The right to copy, distribute, modify, or otherwise make use +# of this software may be licensed only pursuant to the terms +# of an applicable Wind River license agreement. +# + +import json +import os +import yaml + +from six.moves.urllib import parse +from six.moves.urllib import request + +from dcmanagerclient import exceptions + + +def do_action_on_many(action, resources, success_msg, error_msg): + """Helper to run an action on many resources.""" + failure_flag = False + + for resource in resources: + try: + action(resource) + print(success_msg % resource) + except Exception as e: + failure_flag = True + print(e) + + if failure_flag: + raise exceptions.DCManagerClientException(error_msg) + + +def load_content(content): + if content is None or content == '': + return dict() + + try: + data = yaml.safe_load(content) + except Exception: + data = json.loads(content) + + return data + + +def load_file(path): + with open(path, 'r') as f: + return load_content(f.read()) + + +def get_contents_if_file(contents_or_file_name): + """Get the contents of a file. + + If the value passed in is a file name or file URI, return the + contents. If not, or there is an error reading the file contents, + return the value passed in as the contents. + + For example, a workflow definition will be returned if either the + workflow definition file name, or file URI are passed in, or the + actual workflow definition itself is passed in. + """ + try: + if parse.urlparse(contents_or_file_name).scheme: + definition_url = contents_or_file_name + else: + path = os.path.abspath(contents_or_file_name) + definition_url = parse.urljoin( + 'file:', + request.pathname2url(path) + ) + return request.urlopen(definition_url).read().decode('utf8') + except Exception: + return contents_or_file_name diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b6e9014 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. +cliff>=2.3.0 # Apache-2.0 +osc-lib>=1.2.0 # Apache-2.0 +osprofiler>=1.4.0 # Apache-2.0 +pbr>=2.0.0 # Apache-2.0 +python-keystoneclient>=3.8.0 # Apache-2.0 +PyYAML>=3.10.0 # MIT +requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0 +six>=1.9.0 # MIT +beautifulsoup4 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..76da2bb --- /dev/null +++ b/setup.cfg @@ -0,0 +1,33 @@ +[metadata] +name = distributedcloud-client +summary = Python client for Distributed Cloud +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 + +[files] +packages = + dcmanagerclient + +[entry_points] +console_scripts = + dcmanager = dcmanagerclient.shell:main + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = dcmanagerclient/locale/dcmanagerclient.pot diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5f9bfbd --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +# Danger - pbr requirement >= 2.0.0 not satisfied... +setuptools.setup( + setup_requires=['pbr>=1.8.0'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..017d476 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,13 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. +hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 +pylint==1.4.5 # GPLv2 +python-openstackclient>=3.3.0 # Apache-2.0 +sphinx>=1.5.1 # BSD +unittest2 # BSD +fixtures>=3.0.0 # Apache-2.0/BSD +mock>=2.0 # BSD +nose # LGPL +tempest>=14.0.0 # Apache-2.0 +testtools>=1.4.0 # MIT diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..49f1d5f --- /dev/null +++ b/tox.ini @@ -0,0 +1,35 @@ +[tox] +minversion = 2.0 +envlist = py27,pep8 +# Tox does not work if the path to the workdir is too long, so move it to /tmp +toxworkdir = /tmp/{env:USER}_distributedcloud-client +skipsdist = True + +[testenv] +usedevelop = True +install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} + DISCOVER_DIRECTORY=dcmanagerclient/tests +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = python setup.py testr --slowest --testr-args='{posargs}' + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = python setup.py test --coverage --testr-args='{posargs}' + +[testenv:debug] +commands = oslo_debug_helper {posargs} + +[flake8] +# E123, E125 skipped as they are invalid PEP-8. +show-source = True +ignore = E123,E125 +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*lib/python*,*openstack/common*,*egg,build