commit 493d6933a6269d6ca9d8deabdaf3be1f6180b10d Author: liusheng Date: Fri Feb 10 17:00:44 2017 +0800 Initial commit Change-Id: I95ea5eb503e5a5a7e59fe317a62e54cebc83bd55 diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..16f936f --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[run] +branch = True +source = pankoclient + +[report] +ignore_errors = True diff --git a/.gitreview b/.gitreview new file mode 100644 index 0000000..03057b7 --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=openstack/python-pankoclient.git diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..6d83b3c --- /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 -t ./ . $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..2191f58 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +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. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/python-pankoclient diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..2ae89aa --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +python-pankoclient 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..5ed2c36 --- /dev/null +++ b/README.rst @@ -0,0 +1,15 @@ +================== +python-pankoclient +================== + +Python client library for OpenStack Panko project. + +This is a client for OpenStack Panko API. There's :doc:`a Python API +` (the :mod:`pankoclient` module), and a :doc:`command-line script +` (installed as :program:`panko`). Each implements the entire +OpenStack Panko API. + +* Free software: Apache license +* Documentation: http://docs.openstack.org/developer/python-pankoclient +* Source: http://git.openstack.org/cgit/openstack/python-pankoclient +* Bugs: http://bugs.launchpad.net/python-pankoclient 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/doc/source/api.rst b/doc/source/api.rst new file mode 100644 index 0000000..1e83ed9 --- /dev/null +++ b/doc/source/api.rst @@ -0,0 +1,27 @@ +The :mod:`pankoclient` Python API +================================= + +.. module:: pankoclient + :synopsis: A client for the Panko API. + +.. currentmodule:: pankoclient + +Usage +----- + +To use pankoclient in a project:: + + >>> from pankoclient.v2 import client + >>> panko = client.Client(...) + >>> panko.event.list() + +Reference +--------- + +For more information, see the reference: + +.. toctree:: + :maxdepth: 2 + + ref/v2/index + diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..ec7303f --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) + +sys.path.insert(0, ROOT) +sys.path.insert(0, BASE_DIR) + + +def gen_ref(ver, title, names): + refdir = os.path.join(BASE_DIR, "ref") + pkg = "pankoclient" + if ver: + pkg = "%s.%s" % (pkg, ver) + refdir = os.path.join(refdir, ver) + if not os.path.exists(refdir): + os.makedirs(refdir) + idxpath = os.path.join(refdir, "index.rst") + with open(idxpath, "w") as idx: + idx.write(("%(title)s\n" + "%(signs)s\n" + "\n" + ".. toctree::\n" + " :maxdepth: 1\n" + "\n") % {"title": title, "signs": "=" * len(title)}) + for name in names: + idx.write(" %s\n" % name) + rstpath = os.path.join(refdir, "%s.rst" % name) + with open(rstpath, "w") as rst: + rst.write(("%(title)s\n" + "%(signs)s\n" + "\n" + ".. automodule:: %(pkg)s.%(name)s\n" + " :members:\n" + " :undoc-members:\n" + " :show-inheritance:\n" + " :noindex:\n") + % {"title": " ".join([n.capitalize() + for n in name.split("_")]), + "signs": "=" * len(name), + "pkg": pkg, "name": name}) + +gen_ref("v2", "Version 2 API", ["client"]) + +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + #'sphinx.ext.intersphinx', + 'oslosphinx' +] + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'pankoclient' +copyright = u'2015, OpenStack Foundation' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst new file mode 100644 index 0000000..1728a61 --- /dev/null +++ b/doc/source/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../CONTRIBUTING.rst diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..4573a43 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,46 @@ +.. pankoclient documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Python bindings to the Panko API +=============================== + +This is a client for Panko API. There's :doc:`a Python API +` (the :mod:`pankoclient` module), and a :doc:`command-line script +` (installed as :program:`panko`). Each implements the entire +Panko API. + +.. warning:: + + This is a new client to interact with Panko API. There may be differences + in functionality, syntax, and command line output when compared with the + alarm functionality provided by ceilometerclient. + + +.. seealso:: + + You may want to read the `Panko Developer Guide`__ -- the overview, at + least -- to get an idea of the concepts. By understanding the concepts + this library should make more sense. + + __ http://docs.openstack.org/developer/panko/ + + +Contents: + +.. toctree:: + :maxdepth: 2 + + installation + shell + api + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 0000000..201af51 --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,8 @@ +============ +Installation +============ + +At the command line:: + + $ pip install python-pankoclient + diff --git a/doc/source/shell.rst b/doc/source/shell.rst new file mode 100644 index 0000000..6913f88 --- /dev/null +++ b/doc/source/shell.rst @@ -0,0 +1,87 @@ +The :program:`panko` shell utility +========================================= + +.. program:: panko +.. highlight:: bash + +The :program:`panko` shell utility interacts with Panko API +from the command line. It supports the entirety of the Panko API excluding +deprecated combination alarms. + +You'll need to provide :program:`panko` with your OpenStack credentials. +You can do this with the :option:`--os-username`, :option:`--os-password`, +:option:`--os-tenant-id` and :option:`--os-auth-url` options, but it's easier to +just set them as environment variables: + +.. envvar:: OS_USERNAME + + Your OpenStack username. + +.. envvar:: OS_PASSWORD + + Your password. + +.. envvar:: OS_TENANT_NAME + + Project to work on. + +.. envvar:: OS_AUTH_URL + + The OpenStack auth server URL (keystone). + +For example, in Bash you would use:: + + export OS_USERNAME=user + export OS_PASSWORD=pass + export OS_TENANT_NAME=myproject + export OS_AUTH_URL=http://auth.example.com:5000/v2.0 + +The command line tool will attempt to reauthenticate using your provided credentials +for every request. You can override this behavior by manually supplying an auth +token using :option:`--panko-endpoint` and :option:`--os-auth-token`. You can alternatively +set these environment variables:: + + export PANKO_ENDPOINT=http://panko.example.org:8041 + export OS_AUTH_PLUGIN=token + export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 + +Also, if the server doesn't support authentication, you can provide +:option:`--os-auth-plugon` panko-noauth, :option:`--panko-endpoint`, :option:`--user-id` +and :option:`--project-id`. You can alternatively set these environment variables:: + + export OS_AUTH_PLUGIN=panko-noauth + export PANKO_ENDPOINT=http://panko.example.org:8041 + export PANKO_USER_ID=99aae-4dc2-4fbc-b5b8-9688c470d9cc + export PANKO_PROJECT_ID=c8d27445-48af-457c-8e0d-1de7103eae1f + +From there, all shell commands take the form:: + + panko [arguments...] + +Run :program:`panko help` to get a full list of all possible commands, +and run :program:`panko help ` to get detailed help for that +command. + +Examples +-------- + +Create an alarm:: + + panko alarm create -t threshold --name alarm1 -m cpu_util --threshold 5 + +List alarms:: + + panko alarm list + +List alarm with query parameters:: + + panko alarm list --query "state=alarm and type=threshold" + +Show an alarm's history:: + + panko alarm-history show + +Search alarm history data:: + + panko --debug alarm-history search --query 'timestamp>"2016-03-09T01:22:35"' + diff --git a/pankoclient/__init__.py b/pankoclient/__init__.py new file mode 100644 index 0000000..a5c0c49 --- /dev/null +++ b/pankoclient/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +# Copyright 2017 Huawei, Inc. 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. + +import pbr.version + + +__version__ = pbr.version.VersionInfo( + 'python-pankoclient').version_string() diff --git a/pankoclient/common/__init__.py b/pankoclient/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pankoclient/common/base.py b/pankoclient/common/base.py new file mode 100644 index 0000000..fb575d3 --- /dev/null +++ b/pankoclient/common/base.py @@ -0,0 +1,374 @@ +# Copyright 2017 Huawei, Inc. 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. +# + +""" +Base utilities to build API operation managers and objects on top of. +""" + +import abc +import copy + +from requests import Response +import six + +from pankoclient.common import exceptions + + +def getid(obj): + """Get obj's uuid or object itself if no uuid + + Abstracts the common pattern of allowing both an object or + an object's ID (UUID) as a parameter when dealing with relationships. + """ + try: + return obj.uuid + except AttributeError: + return obj + + +class Manager(object): + """Interacts with type of API + + Managers interact with a particular type of API (instances, types, etc.) + and provide CRUD operations for them. + """ + resource_class = None + + def __init__(self, api): + self.api = api + + def _list(self, url, response_key=None, obj_class=None, + data=None, headers=None): + + if headers is None: + headers = {} + resp, body = self.api.get(url, headers=headers) + + if obj_class is None: + obj_class = self.resource_class + + if response_key: + if response_key not in body: + body[response_key] = [] + data = body[response_key] + else: + data = body + if all([isinstance(res, six.string_types) for res in data]): + items = data + else: + items = [obj_class(self, res, loaded=True) for res in data if res] + + return ListWithMeta(items, resp) + + def _delete(self, url, headers=None): + if headers is None: + headers = {} + resp, body = self.api.delete(url, headers=headers) + + return self.convert_into_with_meta(body, resp) + + def _update(self, url, data, response_key=None, return_raw=False, + headers=None): + if headers is None: + headers = {} + resp, body = self.api.patch(url, data=data, headers=headers) + if return_raw: + if response_key: + body = body[response_key] + return self.convert_into_with_meta(body, resp) + # PATCH requests may not return a body + if body: + if response_key: + return self.resource_class(self, body[response_key], resp=resp) + return self.resource_class(self, body, resp=resp) + else: + return StrWithMeta(body, resp) + + def _update_all(self, url, data, response_key=None, return_raw=False, + headers=None): + if headers is None: + headers = {} + resp, body = self.api.put(url, data=data, headers=headers) + if return_raw: + if response_key: + body = body[response_key] + return self.convert_into_with_meta(body, resp) + # PUT requests may not return a body + if body: + if response_key: + return self.resource_class(self, body[response_key], resp=resp) + return self.resource_class(self, body, resp=resp) + else: + return StrWithMeta(body, resp) + + def _create(self, url, data=None, response_key=None, return_raw=False, + headers=None): + if headers is None: + headers = {} + if data: + resp, body = self.api.post(url, data=data, headers=headers) + else: + resp, body = self.api.post(url, headers=headers) + if return_raw: + if response_key: + body = body[response_key] + return self.convert_into_with_meta(body, resp) + + if response_key: + return self.resource_class(self, body[response_key], resp=resp) + return self.resource_class(self, body, resp=resp) + + def _get(self, url, response_key=None, return_raw=False, headers=None): + if headers is None: + headers = {} + resp, body = self.api.get(url, headers=headers) + if return_raw: + if response_key: + body = body[response_key] + return self.convert_into_with_meta(body, resp) + + if response_key: + return self.resource_class(self, body[response_key], loaded=True, + resp=resp) + return self.resource_class(self, body, loaded=True, resp=resp) + + def convert_into_with_meta(self, item, resp): + if isinstance(item, six.string_types): + if six.PY2 and isinstance(item, six.text_type): + return UnicodeWithMeta(item, resp) + else: + return StrWithMeta(item, resp) + elif isinstance(item, six.binary_type): + return BytesWithMeta(item, resp) + elif isinstance(item, list): + return ListWithMeta(item, resp) + elif isinstance(item, tuple): + return TupleWithMeta(item, resp) + elif item is None: + return TupleWithMeta((), resp) + else: + return DictWithMeta(item, resp) + + +@six.add_metaclass(abc.ABCMeta) +class ManagerWithFind(Manager): + """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 = len(matches) + + if num == 0: + msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + raise exceptions.NotFound(msg) + elif num > 1: + raise exceptions.NoUniqueMatch + else: + return self.get(matches[0].uuid) + + 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 RequestIdMixin(object): + """Wrapper class to expose x-openstack-request-id to the caller.""" + def request_ids_setup(self): + self.x_openstack_request_ids = [] + + @property + def request_ids(self): + return self.x_openstack_request_ids + + def append_request_ids(self, resp): + """Add request_ids as an attribute to the object + + :param resp: Response object or list of Response objects + """ + if isinstance(resp, list): + # Add list of request_ids if response is of type list. + for resp_obj in resp: + self._append_request_id(resp_obj) + elif resp is not None: + # Add request_ids if response contains single object. + self._append_request_id(resp) + + def _append_request_id(self, resp): + if isinstance(resp, Response): + # Extract 'X-Openstack-Request-Id' from headers if + # response is a Response object. + request_id = (resp.headers.get('Openstack-Request-Id') or + resp.headers.get('x-openstack-request-id') or + resp.headers.get('x-compute-request-id')) + else: + # If resp is of type string or None. + request_id = resp + if request_id not in self.x_openstack_request_ids: + self.x_openstack_request_ids.append(request_id) + + +class Resource(RequestIdMixin): + """Represents an instance of an object + + A resource represents a particular instance of an object (instance, type, + etc). This is pretty much just a bag for attributes. + + :param manager: BaseManager object + :param info: dictionary representing resource attributes + :param loaded: prevent lazy-loading if set to True + :param resp: Response or list of Response objects + """ + + def __init__(self, manager, info, loaded=False, resp=None): + self.manager = manager + self._info = info + self._add_details(info) + self._loaded = loaded + self.request_ids_setup() + self.append_request_ids(resp) + + 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 __setstate__(self, d): + for k, v in d.items(): + setattr(self, k, v) + + def __getattr__(self, k): + if k not in self.__dict__: + # NOTE(RuiChen): 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 __repr__(self): + reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and + k not in ('manager', 'x_openstack_request_ids')) + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) + + def get(self): + # 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.uuid) + if new: + self._add_details(new._info) + # The 'request_ids' attribute has been added, + # so store the request id to it instead of _info + self.append_request_ids(new.request_ids) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + return self._info == other._info + + def __ne__(self, other): + return not self.__eq__(other) + + def is_loaded(self): + return self._loaded + + def set_loaded(self, val): + self._loaded = val + + def to_dict(self): + return copy.deepcopy(self._info) + + +class ListWithMeta(list, RequestIdMixin): + def __init__(self, values, resp): + super(ListWithMeta, self).__init__(values) + self.request_ids_setup() + self.append_request_ids(resp) + + +class DictWithMeta(dict, RequestIdMixin): + def __init__(self, values, resp): + super(DictWithMeta, self).__init__(values) + self.request_ids_setup() + self.append_request_ids(resp) + + +class TupleWithMeta(tuple, RequestIdMixin): + def __new__(cls, values, resp): + return super(TupleWithMeta, cls).__new__(cls, values) + + def __init__(self, values, resp): + self.request_ids_setup() + self.append_request_ids(resp) + + +class StrWithMeta(str, RequestIdMixin): + def __new__(cls, value, resp): + return super(StrWithMeta, cls).__new__(cls, value) + + def __init__(self, values, resp): + self.request_ids_setup() + self.append_request_ids(resp) + + +class BytesWithMeta(six.binary_type, RequestIdMixin): + def __new__(cls, value, resp): + return super(BytesWithMeta, cls).__new__(cls, value) + + def __init__(self, values, resp): + self.request_ids_setup() + self.append_request_ids(resp) + + +if six.PY2: + class UnicodeWithMeta(six.text_type, RequestIdMixin): + def __new__(cls, value, resp): + return super(UnicodeWithMeta, cls).__new__(cls, value) + + def __init__(self, values, resp): + self.request_ids_setup() + self.append_request_ids(resp) diff --git a/pankoclient/common/exceptions.py b/pankoclient/common/exceptions.py new file mode 100644 index 0000000..d9fa28d --- /dev/null +++ b/pankoclient/common/exceptions.py @@ -0,0 +1,481 @@ +# Copyright 2017 Huawei, Inc. 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. +# + +import inspect +import sys + +from oslo_serialization import jsonutils +import six + +from pankoclient.common.i18n import _ + + +class ClientException(Exception): + """The base exception class for all exceptions this library raises.""" + def __init__(self, message=None): + self.message = message + + def __str__(self): + return self.message or self.__class__.__doc__ + + +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 ConnectionError(ClientException): + """Cannot connect to API service.""" + pass + + +class ConnectionRefused(ConnectionError): + """Connection refused while trying to 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 an AuthSystem that is not installed.""" + def __init__(self, auth_system): + super(AuthSystemNotFound, self).__init__( + _("AuthSystemNotFound: %r") % 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: %r") % endpoints) + self.endpoints = endpoints + + +class HttpError(ClientException): + """The base exception class for all HTTP exceptions.""" + status_code = 0 + message = _("HTTP Error") + + def __init__(self, message=None, details=None, + response=None, request_id=None, + url=None, method=None, status_code=None): + self.status_code = status_code or self.status_code + 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.status_code) + 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. + """ + + status_code = 300 + message = _("Multiple Choices") + + +class BadRequest(HTTPClientError): + """HTTP 400 - Bad Request. + + The request cannot be fulfilled due to bad syntax. + """ + status_code = 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. + """ + status_code = 401 + message = _("Unauthorized") + + +class PaymentRequired(HTTPClientError): + """HTTP 402 - Payment Required. + + Reserved for future use. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 406 + message = _("Not Acceptable") + + +class ProxyAuthenticationRequired(HTTPClientError): + """HTTP 407 - Proxy Authentication Required. + + The client must first authenticate itself with the proxy. + """ + status_code = 407 + message = _("Proxy Authentication Required") + + +class RequestTimeout(HTTPClientError): + """HTTP 408 - Request Timeout. + + The server timed out waiting for the request. + """ + status_code = 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. + """ + status_code = 409 + message = _("Conflict") + + +class Gone(HTTPClientError): + """HTTP 410 - Gone. + + Indicates that the resource requested is no longer available and will + not be available again. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 422 + message = _("Unprocessable Entity") + + +class InternalServerError(HttpServerError): + """HTTP 500 - Internal Server Error. + + A generic error message, given when no more specific message is suitable. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 502 + message = _("Bad Gateway") + + +class ServiceUnavailable(HttpServerError): + """HTTP 503 - Service Unavailable. + + The server is currently unavailable. + """ + status_code = 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. + """ + status_code = 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. + """ + status_code = 505 + message = _("HTTP Version Not Supported") + + +# _code_map contains all the classes that have status_code attribute. +_code_map = dict( + (getattr(obj, 'status_code', None), obj) + for name, obj in six.iteritems(vars(sys.modules[__name__])) + if inspect.isclass(obj) and getattr(obj, 'status_code', 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 + """ + + # NOTE(liusheng): for pecan's response, the request_id is + # "Openstack-Request-Id" + req_id = (response.headers.get("x-openstack-request-id") or + response.headers.get("Openstack-Request-Id")) + kwargs = { + "status_code": 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 hasattr(body, 'keys'): + # NOTE(RuiChen): WebOb<1.6.0 will return a nested dict + # structure where the error keys to the message/details/code. + # WebOb>=1.6.0 returns just a response body as a single dict, + # not nested, so we have to handle both cases (since we can't + # trust what we're given with content_type: application/json + # either way. + if 'message' in body: + # WebOb>=1.6.0 case + error = body + else: + # WebOb<1.6.0 where we assume there is a single error + # message key to the body that has the message and details. + error = body.get(list(body)[0]) + # NOTE(liusheng): the response.json() may like this: + # {u'error_message': u'{"debuginfo": null, "faultcode": + # "Client", "faultstring": "error message"}'}, the + # "error_message" in the body is also a json string. + if isinstance(error, six.string_types): + error = jsonutils.loads(error) + + if hasattr(error, 'keys'): + kwargs['message'] = (error.get('message') or + error.get('faultstring')) + kwargs['details'] = (error.get('details') or + six.text_type(body)) + elif content_type.startswith("text/"): + kwargs["details"] = getattr(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/pankoclient/common/http.py b/pankoclient/common/http.py new file mode 100644 index 0000000..826edeb --- /dev/null +++ b/pankoclient/common/http.py @@ -0,0 +1,348 @@ +# Copyright 2017 Huawei, Inc. 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. +# + +import copy +import hashlib +import logging +import os +import socket + +from keystoneauth1 import adapter +from oslo_serialization import jsonutils +from oslo_utils import encodeutils +from oslo_utils import importutils +import requests +import six +from six.moves.urllib import parse + +from pankoclient.common import exceptions as exc +from pankoclient.common.i18n import _ +from pankoclient.common.i18n import _LW +from pankoclient.common import utils + +LOG = logging.getLogger(__name__) + +USER_AGENT = 'python-pankoclient' +CHUNKSIZE = 1024 * 64 # 64kB +SENSITIVE_HEADERS = ('X-Auth-Token',) +osprofiler_web = importutils.try_import('osprofiler.web') + + +def get_system_ca_file(): + """Return path to system default CA file.""" + # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, + # Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca + ca_path = ['/etc/ssl/certs/ca-certificates.crt', + '/etc/pki/tls/certs/ca-bundle.crt', + '/etc/ssl/ca-bundle.pem', + '/etc/ssl/cert.pem', + '/System/Library/OpenSSL/certs/cacert.pem', + requests.certs.where()] + for ca in ca_path: + LOG.debug("Looking for ca file %s", ca) + if os.path.exists(ca): + LOG.debug("Using ca file %s", ca) + return ca + LOG.warning(_LW("System ca file could not be found.")) + + +class HTTPClient(object): + + def __init__(self, endpoint, **kwargs): + self.endpoint = endpoint + self.auth_url = kwargs.get('auth_url') + self.auth_token = kwargs.get('token') + self.username = kwargs.get('username') + self.password = kwargs.get('password') + self.region_name = kwargs.get('region_name') + self.include_pass = kwargs.get('include_pass') + self.endpoint_url = endpoint + + self.cert_file = kwargs.get('cert_file') + self.key_file = kwargs.get('key_file') + self.timeout = kwargs.get('timeout') + + self.ssl_connection_params = { + 'ca_file': kwargs.get('ca_file'), + 'cert_file': kwargs.get('cert_file'), + 'key_file': kwargs.get('key_file'), + 'insecure': kwargs.get('insecure'), + } + + self.verify_cert = None + if parse.urlparse(endpoint).scheme == "https": + if kwargs.get('insecure'): + self.verify_cert = False + else: + self.verify_cert = kwargs.get('ca_file', get_system_ca_file()) + + # FIXME(RuiChen): We need this for compatibility with the oslo + # apiclient we should move to inheriting this class from the oslo + # HTTPClient + self.last_request_id = None + + def safe_header(self, name, value): + if name in SENSITIVE_HEADERS: + # because in python3 byte string handling is ... ug + v = value.encode('utf-8') + h = hashlib.sha1(v) + d = h.hexdigest() + return encodeutils.safe_decode(name), "{SHA1}%s" % d + else: + return (encodeutils.safe_decode(name), + encodeutils.safe_decode(value)) + + def log_curl_request(self, method, url, kwargs): + curl = ['curl -g -i -X %s' % method] + + for (key, value) in kwargs['headers'].items(): + header = '-H \'%s: %s\'' % self.safe_header(key, value) + curl.append(header) + + conn_params_fmt = [ + ('key_file', '--key %s'), + ('cert_file', '--cert %s'), + ('ca_file', '--cacert %s'), + ] + for (key, fmt) in conn_params_fmt: + value = self.ssl_connection_params.get(key) + if value: + curl.append(fmt % value) + + if self.ssl_connection_params.get('insecure'): + curl.append('-k') + + if 'data' in kwargs: + curl.append('-d \'%s\'' % kwargs['data']) + + curl.append('%s%s' % (self.endpoint, url)) + LOG.debug(' '.join(curl)) + + @staticmethod + def log_http_response(resp): + status = (resp.raw.version / 10.0, resp.status_code, resp.reason) + dump = ['\nHTTP/%.1f %s %s' % status] + dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()]) + dump.append('') + if resp.content: + content = resp.content + if isinstance(content, six.binary_type): + content = content.decode() + dump.extend([content, '']) + LOG.debug('\n'.join(dump)) + + def _http_request(self, url, method, **kwargs): + """Send an http request with the specified characteristics. + + Wrapper around requests.request to handle tasks such as + setting headers and error handling. + """ + # Copy the kwargs so we can reuse the original in case of redirects + kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) + kwargs['headers'].setdefault('User-Agent', USER_AGENT) + if self.auth_token: + kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) + else: + kwargs['headers'].update(self.credentials_headers()) + if self.auth_url: + kwargs['headers'].setdefault('X-Auth-Url', self.auth_url) + if self.region_name: + kwargs['headers'].setdefault('X-Region-Name', self.region_name) + if self.include_pass and 'X-Auth-Key' not in kwargs['headers']: + kwargs['headers'].update(self.credentials_headers()) + if osprofiler_web: + kwargs['headers'].update(osprofiler_web.get_trace_id_headers()) + + self.log_curl_request(method, url, kwargs) + + if self.cert_file and self.key_file: + kwargs['cert'] = (self.cert_file, self.key_file) + + if self.verify_cert is not None: + kwargs['verify'] = self.verify_cert + + if self.timeout is not None: + kwargs['timeout'] = float(self.timeout) + + # Allow caller to specify not to follow redirects, in which case we + # just return the redirect response. Useful for using stacks:lookup. + redirect = kwargs.pop('redirect', True) + + # Since requests does not follow the RFC when doing redirection to sent + # back the same method on a redirect we are simply bypassing it. For + # example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says + # that we should follow that URL with the same method as before, + # requests doesn't follow that and send a GET instead for the method. + # Hopefully this could be fixed as they say in a comment in a future + # point version i.e.: 3.x + # See issue: https://github.com/kennethreitz/requests/issues/1704 + allow_redirects = False + + # Use fully qualified URL from response header for redirects + if not parse.urlparse(url).netloc: + url = self.endpoint_url + url + + try: + resp = requests.request( + method, + url, + allow_redirects=allow_redirects, + **kwargs) + except socket.gaierror as e: + message = (_("Error finding address for %(url)s: %(e)s") % + {'url': self.endpoint_url + url, 'e': e}) + raise exc.EndpointNotFound(message=message) + except (socket.error, socket.timeout) as e: + endpoint = self.endpoint + message = (_("Error communicating with %(endpoint)s %(e)s") % + {'endpoint': endpoint, 'e': e}) + raise exc.ConnectionError(message=message) + + self.log_http_response(resp) + + if not ('X-Auth-Key' in kwargs['headers']) and ( + resp.status_code == 401 or + (resp.status_code == 500 and "(HTTP 401)" in resp.content)): + raise exc.AuthorizationFailure(_("Authentication failed: %s") + % resp.content) + elif 400 <= resp.status_code < 600: + raise exc.from_response(resp, method, url) + elif resp.status_code in (301, 302, 305): + # Redirected. Reissue the request to the new location, + # unless caller specified redirect=False + if redirect: + location = resp.headers.get('location') + location = self.strip_endpoint(location) + resp = self._http_request(location, method, **kwargs) + elif resp.status_code == 300: + raise exc.from_response(resp, method, url) + + return resp + + def strip_endpoint(self, location): + if location is None: + message = _("Location not returned with redirect") + raise exc.EndpointException(message=message) + if location.lower().startswith(self.endpoint): + return location[len(self.endpoint):] + else: + return location + + def credentials_headers(self): + creds = {} + # NOTE(RuiChen): When deferred_auth_method=password, Heat + # encrypts and stores username/password. For Keystone v3, the + # intent is to use trusts since SHARDY is working towards + # deferred_auth_method=trusts as the default. + if self.username: + creds['X-Auth-User'] = self.username + if self.password: + creds['X-Auth-Key'] = self.password + return creds + + def json_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', 'application/json') + kwargs['headers'].setdefault('Accept', 'application/json') + + if 'data' in kwargs: + kwargs['data'] = jsonutils.dumps(kwargs['data']) + + resp = self._http_request(url, method, **kwargs) + body = utils.get_response_body(resp) + return resp, body + + def raw_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', + 'application/octet-stream') + resp = self._http_request(url, method, **kwargs) + body = utils.get_response_body(resp) + return resp, body + + def head(self, url, **kwargs): + return self.json_request("HEAD", url, **kwargs) + + def get(self, url, **kwargs): + return self.json_request("GET", url, **kwargs) + + def post(self, url, **kwargs): + return self.json_request("POST", url, **kwargs) + + def put(self, url, **kwargs): + return self.json_request("PUT", url, **kwargs) + + def delete(self, url, **kwargs): + return self.raw_request("DELETE", url, **kwargs) + + def patch(self, url, **kwargs): + return self.json_request("PATCH", url, **kwargs) + + +class SessionClient(adapter.LegacyJsonAdapter): + """HTTP client based on Keystone client session.""" + + def request(self, url, method, **kwargs): + redirect = kwargs.get('redirect') + kwargs.setdefault('user_agent', USER_AGENT) + + if 'data' in kwargs: + kwargs['json'] = kwargs.pop('data') + + resp, body = super(SessionClient, self).request( + url, method, + raise_exc=False, + **kwargs) + + if 400 <= resp.status_code < 600: + raise exc.from_response(resp, method, url) + elif resp.status_code in (301, 302, 305): + if redirect: + location = resp.headers.get('location') + path = self.strip_endpoint(location) + resp, body = self.request(path, method, **kwargs) + elif resp.status_code == 300: + raise exc.from_response(resp, method, url) + + return resp, body + + def credentials_headers(self): + return {} + + def strip_endpoint(self, location): + if location is None: + message = _("Location not returned with redirect") + raise exc.EndpointException(message=message) + if (self.endpoint_override is not None and + location.lower().startswith(self.endpoint_override.lower())): + return location[len(self.endpoint_override):] + else: + return location + + +def _construct_http_client(endpoint=None, username=None, password=None, + include_pass=None, endpoint_type=None, + auth_url=None, **kwargs): + session = kwargs.pop('session', None) + auth = kwargs.pop('auth', None) + + if session: + kwargs['endpoint_override'] = endpoint + return SessionClient(session, auth=auth, **kwargs) + else: + return HTTPClient(endpoint=endpoint, username=username, + password=password, include_pass=include_pass, + endpoint_type=endpoint_type, auth_url=auth_url, + **kwargs) diff --git a/pankoclient/common/i18n.py b/pankoclient/common/i18n.py new file mode 100644 index 0000000..582d056 --- /dev/null +++ b/pankoclient/common/i18n.py @@ -0,0 +1,31 @@ +# Copyright 2017 Huawei, Inc. 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. +# + +import oslo_i18n + +_translators = oslo_i18n.TranslatorFactory(domain='pankoclient') + +# 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 diff --git a/pankoclient/common/utils.py b/pankoclient/common/utils.py new file mode 100644 index 0000000..fbad573 --- /dev/null +++ b/pankoclient/common/utils.py @@ -0,0 +1,50 @@ +# Copyright 2017 Huawei, Inc. 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. +# +from six.moves.urllib import parse as urllib_parse + +import logging + +from pankoclient.common.i18n import _LE + +LOG = logging.getLogger(__name__) + + +def get_response_body(resp): + body = resp.content + content_type = resp.headers.get('Content-Type', '') + if 'application/json' in content_type: + try: + body = resp.json() + except ValueError: + LOG.error(_LE('Could not decode response body as JSON')) + elif 'application/octet-stream' in content_type: + try: + body = resp.body() + except ValueError: + LOG.error(_LE('Could not decode response body as raw')) + else: + body = None + return body + + +def get_pagination_options(limit=None, marker=None, sorts=None): + options = [] + if limit: + options.append("limit=%d" % limit) + if marker: + options.append("marker=%s" % urllib_parse.quote(marker)) + for sort in sorts or []: + options.append("sort=%s" % urllib_parse.quote(sort)) + return "&".join(options) diff --git a/pankoclient/osc/__init__.py b/pankoclient/osc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pankoclient/osc/plugin.py b/pankoclient/osc/plugin.py new file mode 100644 index 0000000..cb20824 --- /dev/null +++ b/pankoclient/osc/plugin.py @@ -0,0 +1,73 @@ +# Copyright 2017 Huawei, Inc. 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. +# + +import logging + +from osc_lib import utils + +from pankoclient.common.i18n import _ + +LOG = logging.getLogger(__name__) + +DEFAULT_EVENT_API_VERSION = '2' +API_VERSION_OPTION = 'os_event_api_version' +API_NAME = 'event' +API_VERSIONS = { + '2': 'pankoclient.v2.client.Client', +} + + +def make_client(instance): + """Returns an event service client""" + panko_client = utils.get_client_class( + API_NAME, + instance._api_version[API_NAME], + API_VERSIONS) + LOG.debug('Instantiating event client: %s', panko_client) + + endpoint = instance.get_endpoint_for_service_type( + API_NAME, + region_name=instance.region_name, + interface=instance.interface, + ) + + kwargs = {'endpoint': endpoint, + 'auth_url': instance.auth.auth_url, + 'region_name': instance.region_name, + 'username': instance.auth_ref.username} + + if instance.session: + kwargs.update(session=instance.session) + else: + kwargs.update(token=instance.auth_ref.auth_token) + + client = panko_client(**kwargs) + + return client + + +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-event-api-version', + metavar='', + default=utils.env( + 'OS_EVENT_API_VERSION', + default=DEFAULT_EVENT_API_VERSION), + help=(_('Event API version, default=%s ' + '(Env: OS_EVENT_API_VERSION)') % + DEFAULT_EVENT_API_VERSION) + ) + return parser diff --git a/pankoclient/osc/v2/__init__.py b/pankoclient/osc/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pankoclient/osc/v2/capabilities.py b/pankoclient/osc/v2/capabilities.py new file mode 100644 index 0000000..1d3f1bc --- /dev/null +++ b/pankoclient/osc/v2/capabilities.py @@ -0,0 +1,27 @@ +# Copyright 2016 Huawei, Inc. 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. +# + +"""Panko v2 Capabilities action implementations""" + +from cliff import show + + +class CliCapabilitiesList(show.ShowOne): + """List capabilities for event service""" + + def take_action(self, parsed_args): + ac = self.app.client_manager.alarming + caps = ac.capabilities.list() + return self.dict2columns(caps) diff --git a/pankoclient/osc/v2/events.py b/pankoclient/osc/v2/events.py new file mode 100644 index 0000000..a1faa68 --- /dev/null +++ b/pankoclient/osc/v2/events.py @@ -0,0 +1,109 @@ +# Copyright 2016 Huawei, Inc. 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. +# + + +"""Panko v2 event action implementations""" + +import logging + +from osc_lib.cli import parseractions +from osc_lib.command import command +from osc_lib import exceptions +from osc_lib import utils +import six + +from pankoclient.common.i18n import _ + +LOG = logging.getLogger(__name__) + + +class ListEvent(command.Lister): + """List all baremetal servers""" + + def get_parser(self, prog_name): + parser = super(ListEvent, self).get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=_("List the baremetal servers of all projects, " + "only available for admin users.") + ) + return parser + + @staticmethod + def _networks_formatter(network_info): + return_info = [] + for port_uuid in network_info: + port_ips = [] + for fixed_ip in network_info[port_uuid]['fixed_ips']: + port_ips.append(fixed_ip['ip_address']) + return_info.append(', '.join(port_ips)) + return '; '.join(return_info) + + def take_action(self, parsed_args): + bc_client = self.app.client_manager.baremetal_compute + + if parsed_args.long: + data = bc_client.server.list(detailed=True, + all_projects=parsed_args.all_projects) + formatters = {'network_info': self._networks_formatter} + # This is the easiest way to change column headers + column_headers = ( + "UUID", + "Name", + "Flavor", + "Status", + "Power State", + "Image", + "Description", + "Availability Zone", + "Networks" + ) + columns = ( + "uuid", + "name", + "instance_type_uuid", + "status", + "power_state", + "image_uuid", + "description", + "availability_zone", + "network_info" + ) + else: + data = bc_client.server.list(all_projects=parsed_args.all_projects) + formatters = None + column_headers = ( + "UUID", + "Name", + "Status", + ) + columns = ( + "uuid", + "name", + "status", + ) + + return (column_headers, + (utils.get_item_properties( + s, columns, formatters=formatters + ) for s in data)) diff --git a/pankoclient/tests/__init__.py b/pankoclient/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pankoclient/v2/__init__.py b/pankoclient/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pankoclient/v2/capabilities.py b/pankoclient/v2/capabilities.py new file mode 100644 index 0000000..7472bdf --- /dev/null +++ b/pankoclient/v2/capabilities.py @@ -0,0 +1,23 @@ +# +# 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. + +from pankoclient.common import base + + +class CapabilitiesManager(base.ManagerWithFind): + resource_class = None + + def list(self): + """List capabilities""" + cap_url = "v2/capabilities/" + return self._get(cap_url).json() diff --git a/pankoclient/v2/client.py b/pankoclient/v2/client.py new file mode 100644 index 0000000..c35bcbf --- /dev/null +++ b/pankoclient/v2/client.py @@ -0,0 +1,27 @@ +# Copyright 2017 Huawei, Inc. 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. +# + +from pankoclient.common import http +from pankoclient.v2 import events + + +class Client(object): + """Client for the Panko v2 API.""" + + def __init__(self, *args, **kwargs): + """Initialize a new client for the Panko v1 API.""" + self.http_client = http._construct_http_client(*args, **kwargs) + self.event = events.EventManager( + self.http_client) diff --git a/pankoclient/v2/events.py b/pankoclient/v2/events.py new file mode 100644 index 0000000..bb17cd1 --- /dev/null +++ b/pankoclient/v2/events.py @@ -0,0 +1,49 @@ +# Copyright 2017 Huawei, Inc. 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. +# + +from pankoclient.common import base +from pankoclient.common import utils + +class Event(base.Resource): + pass + + +class EventManager(base.ManagerWithFind): + resource_class = Event + + def list(self, query=None, limit=None, marker=None, sorts=None): + """List Events + :param query: Filter arguments for which Events to return + :type query: list + :param limit: maximum number of resources to return + :type limit: int + :param marker: the last item of the previous page; we return the next + results after this value. + :type marker: str + :param sorts: list of resource attributes to order by. + :type sorts: list of str + """ + pagination = utils.get_pagination_options(limit, marker, sorts) + #simple_query_string = EventManager.build_simple_query_string(query) + + url = self.url + options = [] + if pagination: + options.append(pagination) + #if simple_query_string: + # options.append(simple_query_string) + if options: + url += "?" + "&".join(options) + return self._get(url).json() diff --git a/releasenotes/notes/.placeholder b/releasenotes/notes/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 0000000..b935088 --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- + +# Copyright 2017 Huawei, Inc. 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. + +# Panko Client Release Notes documentation build configuration file, created +# by sphinx-quickstart on Tue Nov 3 17:40:50 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'oslosphinx', + 'reno.sphinxext', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'pankoclient Release Notes' +copyright = u'2016, OpenStack Foundation' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +# The full version, including alpha/beta/rc tags. +release = '' +# The short X.Y version. +version = '' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PankoClientReleaseNotesdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'PankoClientReleaseNotes.tex', + u'Panko Client Release Notes Documentation', + u'Panko Client Developers', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'pankoclientreleasenotes', + u'Panko Client Release Notes Documentation', + [u'Panko Client Developers'], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'PankoClientReleaseNotes', + u'Panko Client Release Notes Documentation', + u'Panko Client Developers', 'PankoClientReleaseNotes', + 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + +# -- Options for Internationalization output ------------------------------ +locale_dirs = ['locale/'] diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 0000000..3fe657f --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,8 @@ +================================ +python-pankoclient Release Notes +================================ + +.. toctree:: + :maxdepth: 1 + + unreleased diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 0000000..875030f --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +============================ +Current Series Release Notes +============================ + +.. release-notes:: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..50abc10 --- /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. + +keystoneauth1>=2.18.0 # Apache-2.0 +osc-lib>=1.2.0 # Apache-2.0 +oslo.i18n>=2.1.0 # Apache-2.0 +oslo.serialization>=1.10.0 # Apache-2.0 +oslo.utils>=3.18.0 # Apache-2.0 +pbr>=1.8 # Apache-2.0 +requests!=2.12.2,>=2.10.0 # Apache-2.0 +six>=1.9.0 # MIT diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..795bc1a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,60 @@ +[metadata] +name = python-pankoclient +summary = Python client library for OpenStack Panko project. +description-file = + README.rst +license = Apache License, Version 2.0 +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Development Status :: 5 - Production/Stable + Environment :: Console + 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.5 + +[files] +packages = + pankoclient + +[entry_points] +openstack.cli.extension = + event = pankoclient.osc.plugin + +openstack.event.v2 = + alarming capabilities list = pankoclient.v2.capabilities_cli:CliCapabilitiesList + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html + +[compile_catalog] +directory = pankoclient/locale +domain = pankoclient + +[update_catalog] +domain = pankoclient +output_dir = pankoclient/locale +input_file = pankoclient/locale/pankoclient.pot + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = pankoclient/locale/pankoclient.pot + +[build_releasenotes] +all_files = 1 +build-dir = releasenotes/build +source-dir = releasenotes/source diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..782bb21 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +# 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 + +setuptools.setup( + setup_requires=['pbr>=1.8'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..3593d3f --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,16 @@ +# 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. + +coverage>=4.0 # Apache-2.0 +hacking<0.12,>=0.11.0 # Apache-2.0 +mock>=2.0 # BSD +python-openstackclient>=3.3.0 # Apache-2.0 +python-subunit>=0.0.18 # Apache-2.0/BSD +oslosphinx>=4.7.0 # Apache-2.0 +oslotest>=1.10.0 # Apache-2.0 +reno>=1.8.0 # Apache-2.0 +requests-mock>=1.1 # Apache-2.0 +sphinx!=1.3b1,<1.4,>=1.2.1 # BSD +testrepository>=0.0.18 # Apache-2.0/BSD +testscenarios>=0.4 # Apache-2.0/BSD diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..7f375de --- /dev/null +++ b/tox.ini @@ -0,0 +1,41 @@ +[tox] +minversion = 2.0 +envlist = py35,py27,pep8 +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} +deps = -r{toxinidir}/test-requirements.txt +commands = python setup.py test --slowest --testr-args='{posargs}' + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = + python setup.py test --coverage --testr-args='{posargs}' + coverage report + +[testenv:docs] +commands = python setup.py build_sphinx + +[testenv:releasenotes] +commands = + sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + +[testenv:debug] +commands = oslo_debug_helper -t pankoclient/tests {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*,*egg,build