From 493d6933a6269d6ca9d8deabdaf3be1f6180b10d Mon Sep 17 00:00:00 2001 From: liusheng Date: Fri, 10 Feb 2017 17:00:44 +0800 Subject: [PATCH] Initial commit Change-Id: I95ea5eb503e5a5a7e59fe317a62e54cebc83bd55 --- .coveragerc | 6 + .gitreview | 4 + .testr.conf | 7 + CONTRIBUTING.rst | 17 + HACKING.rst | 4 + LICENSE | 176 +++++++ MANIFEST.in | 6 + README.rst | 15 + babel.cfg | 2 + doc/source/api.rst | 27 ++ doc/source/conf.py | 116 +++++ doc/source/contributing.rst | 4 + doc/source/index.rst | 46 ++ doc/source/installation.rst | 8 + doc/source/shell.rst | 87 ++++ pankoclient/__init__.py | 21 + pankoclient/common/__init__.py | 0 pankoclient/common/base.py | 374 +++++++++++++++ pankoclient/common/exceptions.py | 481 ++++++++++++++++++++ pankoclient/common/http.py | 348 ++++++++++++++ pankoclient/common/i18n.py | 31 ++ pankoclient/common/utils.py | 50 ++ pankoclient/osc/__init__.py | 0 pankoclient/osc/plugin.py | 73 +++ pankoclient/osc/v2/__init__.py | 0 pankoclient/osc/v2/capabilities.py | 27 ++ pankoclient/osc/v2/events.py | 109 +++++ pankoclient/tests/__init__.py | 0 pankoclient/v2/__init__.py | 0 pankoclient/v2/capabilities.py | 23 + pankoclient/v2/client.py | 27 ++ pankoclient/v2/events.py | 49 ++ releasenotes/notes/.placeholder | 0 releasenotes/source/_static/.placeholder | 0 releasenotes/source/_templates/.placeholder | 0 releasenotes/source/conf.py | 281 ++++++++++++ releasenotes/source/index.rst | 8 + releasenotes/source/unreleased.rst | 5 + requirements.txt | 12 + setup.cfg | 60 +++ setup.py | 29 ++ test-requirements.txt | 16 + tox.ini | 41 ++ 43 files changed, 2590 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitreview create mode 100644 .testr.conf create mode 100644 CONTRIBUTING.rst create mode 100644 HACKING.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 babel.cfg create mode 100644 doc/source/api.rst create mode 100644 doc/source/conf.py create mode 100644 doc/source/contributing.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/installation.rst create mode 100644 doc/source/shell.rst create mode 100644 pankoclient/__init__.py create mode 100644 pankoclient/common/__init__.py create mode 100644 pankoclient/common/base.py create mode 100644 pankoclient/common/exceptions.py create mode 100644 pankoclient/common/http.py create mode 100644 pankoclient/common/i18n.py create mode 100644 pankoclient/common/utils.py create mode 100644 pankoclient/osc/__init__.py create mode 100644 pankoclient/osc/plugin.py create mode 100644 pankoclient/osc/v2/__init__.py create mode 100644 pankoclient/osc/v2/capabilities.py create mode 100644 pankoclient/osc/v2/events.py create mode 100644 pankoclient/tests/__init__.py create mode 100644 pankoclient/v2/__init__.py create mode 100644 pankoclient/v2/capabilities.py create mode 100644 pankoclient/v2/client.py create mode 100644 pankoclient/v2/events.py create mode 100644 releasenotes/notes/.placeholder create mode 100644 releasenotes/source/_static/.placeholder create mode 100644 releasenotes/source/_templates/.placeholder create mode 100644 releasenotes/source/conf.py create mode 100644 releasenotes/source/index.rst create mode 100644 releasenotes/source/unreleased.rst create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test-requirements.txt create mode 100644 tox.ini 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