Initial commit
Change-Id: I95ea5eb503e5a5a7e59fe317a62e54cebc83bd55
This commit is contained in:
commit
493d6933a6
|
@ -0,0 +1,6 @@
|
||||||
|
[run]
|
||||||
|
branch = True
|
||||||
|
source = pankoclient
|
||||||
|
|
||||||
|
[report]
|
||||||
|
ignore_errors = True
|
|
@ -0,0 +1,4 @@
|
||||||
|
[gerrit]
|
||||||
|
host=review.openstack.org
|
||||||
|
port=29418
|
||||||
|
project=openstack/python-pankoclient.git
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
||||||
|
python-pankoclient Style Commandments
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/
|
|
@ -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.
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
include AUTHORS
|
||||||
|
include ChangeLog
|
||||||
|
exclude .gitignore
|
||||||
|
exclude .gitreview
|
||||||
|
|
||||||
|
global-exclude *.pyc
|
|
@ -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
|
||||||
|
<api>` (the :mod:`pankoclient` module), and a :doc:`command-line script
|
||||||
|
<shell>` (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
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
|
@ -0,0 +1,4 @@
|
||||||
|
============
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
.. include:: ../../CONTRIBUTING.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
|
||||||
|
<api>` (the :mod:`pankoclient` module), and a :doc:`command-line script
|
||||||
|
<shell>` (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`
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
============
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
At the command line::
|
||||||
|
|
||||||
|
$ pip install python-pankoclient
|
||||||
|
|
|
@ -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 <command> [arguments...]
|
||||||
|
|
||||||
|
Run :program:`panko help` to get a full list of all possible commands,
|
||||||
|
and run :program:`panko help <command>` 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 <ALARM_ID>
|
||||||
|
|
||||||
|
Search alarm history data::
|
||||||
|
|
||||||
|
panko --debug alarm-history search --query 'timestamp>"2016-03-09T01:22:35"'
|
||||||
|
|
|
@ -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()
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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='<event-api-version>',
|
||||||
|
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
|
|
@ -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)
|
|
@ -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))
|
|
@ -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()
|
|
@ -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)
|
|
@ -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()
|
|
@ -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
|
||||||
|
# "<project> v<release> 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 <link> 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/']
|
|
@ -0,0 +1,8 @@
|
||||||
|
================================
|
||||||
|
python-pankoclient Release Notes
|
||||||
|
================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
unreleased
|
|
@ -0,0 +1,5 @@
|
||||||
|
============================
|
||||||
|
Current Series Release Notes
|
||||||
|
============================
|
||||||
|
|
||||||
|
.. release-notes::
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue