From 54c39ba69cf70b4da7ff410b084a2bab7bdc1c83 Mon Sep 17 00:00:00 2001 From: jiasirui Date: Fri, 25 Dec 2020 09:26:45 +0800 Subject: [PATCH] Initialize the python_venusclient project Change-Id: Ifc832dfec84a7b8cbcc7de6f795bf279767ba42f --- .coveragerc | 6 + .gitignore | 61 ++ .mailmap | 3 + .stestr.conf | 3 + CONTRIBUTING.rst | 17 + HACKING.rst | 4 + LICENSE | 176 +++++ README.rst | 9 + doc/source/admin/index.rst | 5 + doc/source/cli/index.rst | 5 + doc/source/conf.py | 80 +++ doc/source/contributor/contributing.rst | 4 + doc/source/contributor/index.rst | 9 + doc/source/index.rst | 29 + doc/source/install/index.rst | 12 + doc/source/library/index.rst | 7 + doc/source/readme.rst | 1 + doc/source/reference/index.rst | 5 + doc/source/user/index.rst | 5 + releasenotes/notes/.placeholder | 0 .../notes/drop-py-2-7-d47826d4387f40fe.yaml | 6 + releasenotes/source/_static/.placeholder | 0 releasenotes/source/_templates/.placeholder | 0 releasenotes/source/conf.py | 267 ++++++++ releasenotes/source/index.rst | 12 + releasenotes/source/rocky.rst | 6 + releasenotes/source/stein.rst | 6 + releasenotes/source/train.rst | 6 + releasenotes/source/unreleased.rst | 5 + releasenotes/source/ussuri.rst | 6 + requirements.txt | 19 + setup.cfg | 31 + setup.py | 20 + test-requirements.txt | 16 + tox.ini | 69 ++ venusclient/__init__.py | 19 + venusclient/common/__init__.py | 0 venusclient/common/apiclient/__init__.py | 0 venusclient/common/apiclient/base.py | 108 +++ venusclient/common/apiclient/exceptions.py | 460 +++++++++++++ venusclient/common/base.py | 149 ++++ venusclient/common/cliutils.py | 508 ++++++++++++++ venusclient/common/http.py | 347 ++++++++++ venusclient/common/httpclient.py | 435 ++++++++++++ venusclient/common/utils.py | 159 +++++ venusclient/exceptions.py | 515 ++++++++++++++ venusclient/i18n.py | 19 + venusclient/osc/__init__.py | 0 venusclient/osc/plugin.py | 115 ++++ venusclient/osc/v1/__init__.py | 0 venusclient/shell.py | 642 ++++++++++++++++++ venusclient/v1/__init__.py | 19 + venusclient/v1/basemodels.py | 115 ++++ venusclient/v1/client.py | 192 ++++++ venusclient/v1/config.py | 36 + venusclient/v1/config_shell.py | 24 + venusclient/v1/shell.py | 20 + venusclient/version.py | 17 + 58 files changed, 4809 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore create mode 100644 .mailmap create mode 100644 .stestr.conf create mode 100644 CONTRIBUTING.rst create mode 100644 HACKING.rst create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 doc/source/admin/index.rst create mode 100644 doc/source/cli/index.rst create mode 100644 doc/source/conf.py create mode 100644 doc/source/contributor/contributing.rst create mode 100644 doc/source/contributor/index.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/install/index.rst create mode 100644 doc/source/library/index.rst create mode 100644 doc/source/readme.rst create mode 100644 doc/source/reference/index.rst create mode 100644 doc/source/user/index.rst create mode 100644 releasenotes/notes/.placeholder create mode 100644 releasenotes/notes/drop-py-2-7-d47826d4387f40fe.yaml create mode 100644 releasenotes/source/_static/.placeholder create mode 100644 releasenotes/source/_templates/.placeholder create mode 100644 releasenotes/source/conf.py create mode 100644 releasenotes/source/index.rst create mode 100644 releasenotes/source/rocky.rst create mode 100644 releasenotes/source/stein.rst create mode 100644 releasenotes/source/train.rst create mode 100644 releasenotes/source/unreleased.rst create mode 100644 releasenotes/source/ussuri.rst create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test-requirements.txt create mode 100644 tox.ini create mode 100644 venusclient/__init__.py create mode 100644 venusclient/common/__init__.py create mode 100644 venusclient/common/apiclient/__init__.py create mode 100644 venusclient/common/apiclient/base.py create mode 100644 venusclient/common/apiclient/exceptions.py create mode 100644 venusclient/common/base.py create mode 100644 venusclient/common/cliutils.py create mode 100644 venusclient/common/http.py create mode 100644 venusclient/common/httpclient.py create mode 100644 venusclient/common/utils.py create mode 100644 venusclient/exceptions.py create mode 100644 venusclient/i18n.py create mode 100644 venusclient/osc/__init__.py create mode 100644 venusclient/osc/plugin.py create mode 100644 venusclient/osc/v1/__init__.py create mode 100644 venusclient/shell.py create mode 100644 venusclient/v1/__init__.py create mode 100644 venusclient/v1/basemodels.py create mode 100644 venusclient/v1/client.py create mode 100644 venusclient/v1/config.py create mode 100644 venusclient/v1/config_shell.py create mode 100644 venusclient/v1/shell.py create mode 100644 venusclient/version.py diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..db0143b --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[run] +branch = True +source = venusclient + +[report] +ignore_errors = True diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87dcd94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg* +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +.idea* + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +cover/ +.coverage* +!.coveragerc +.tox +nosetests.xml +.testrepository +.stestr +.venv + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +doc/build + +# pbr generates these +AUTHORS +ChangeLog + +# Editors +*~ +.*.swp +.*sw? + +# Files created by releasenotes build +releasenotes/build + diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..516ae6f --- /dev/null +++ b/.mailmap @@ -0,0 +1,3 @@ +# Format is: +# +# diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..aba4eb4 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./venusclient/tests +top_dir=./ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..ce4edb0 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: + + http://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/python-venusclient diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..8afe002 --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +python-venusclient Style Commandments +=============================================== + +Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..863fc61 --- /dev/null +++ b/README.rst @@ -0,0 +1,9 @@ +======================== +Team and repository tags +======================== + +.. image:: https://governance.openstack.org/tc/badges/python-venusclient.svg + :target: https://governance.openstack.org/tc/reference/tags/index.html + +.. Change things from this point on + diff --git a/doc/source/admin/index.rst b/doc/source/admin/index.rst new file mode 100644 index 0000000..c042555 --- /dev/null +++ b/doc/source/admin/index.rst @@ -0,0 +1,5 @@ +==================== +Administrators guide +==================== + +Administrators guide of python-venusclient. diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst new file mode 100644 index 0000000..c0fc3f9 --- /dev/null +++ b/doc/source/cli/index.rst @@ -0,0 +1,5 @@ +================================ +Command line interface reference +================================ + +CLI reference of python-venusclient. diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..84f1b78 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,80 @@ +# -*- 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 + +sys.path.insert(0, os.path.abspath('../..')) +# -- 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', + 'openstackdocstheme', +] + +# 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'python-venusclient' +copyright = u'2017, OpenStack Developers' + +# openstackdocstheme options +openstackdocs_repo_name = 'openstack/python-venusclient' +openstackdocs_bug_project = 'https://bugs.launchpad.net/python-venusclient' +openstackdocs_bug_tag = '' + +# 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 = 'native' + +# -- 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'] +html_theme = 'openstackdocs' + +# 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 Developers', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst new file mode 100644 index 0000000..2aa0707 --- /dev/null +++ b/doc/source/contributor/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../../CONTRIBUTING.rst diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst new file mode 100644 index 0000000..036e449 --- /dev/null +++ b/doc/source/contributor/index.rst @@ -0,0 +1,9 @@ +=========================== + Contributor Documentation +=========================== + +.. toctree:: + :maxdepth: 2 + + contributing + diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..25d708b --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,29 @@ +.. python-venusclient 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. + +============================================ +Welcome to the documentation of venusclient +============================================ + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + install/index + library/index + contributor/index + cli/index + user/index + admin/index + reference/index + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/install/index.rst b/doc/source/install/index.rst new file mode 100644 index 0000000..14f9945 --- /dev/null +++ b/doc/source/install/index.rst @@ -0,0 +1,12 @@ +======================================= +venus Python Client installation guide +======================================= + +At the command line:: + + $ pip install python-venusclient + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv python-venusclient + $ pip install python-venusclient diff --git a/doc/source/library/index.rst b/doc/source/library/index.rst new file mode 100644 index 0000000..eb8de90 --- /dev/null +++ b/doc/source/library/index.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use python-venusclient in a project:: + + import venusclient diff --git a/doc/source/readme.rst b/doc/source/readme.rst new file mode 100644 index 0000000..a6210d3 --- /dev/null +++ b/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst new file mode 100644 index 0000000..a0f18b1 --- /dev/null +++ b/doc/source/reference/index.rst @@ -0,0 +1,5 @@ +========== +References +========== + +References of python-venusclient. diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst new file mode 100644 index 0000000..c00f260 --- /dev/null +++ b/doc/source/user/index.rst @@ -0,0 +1,5 @@ +=========== +Users guide +=========== + +Users guide of python-venusclient. diff --git a/releasenotes/notes/.placeholder b/releasenotes/notes/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/notes/drop-py-2-7-d47826d4387f40fe.yaml b/releasenotes/notes/drop-py-2-7-d47826d4387f40fe.yaml new file mode 100644 index 0000000..72e6e19 --- /dev/null +++ b/releasenotes/notes/drop-py-2-7-d47826d4387f40fe.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + Python 2.7 support has been dropped. Last release of python-venusclient + to support python 2.7 is OpenStack Train. The minimum version of Python now + supported is Python 3.6. diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 0000000..f29fed6 --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,267 @@ +# 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 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 = [ + 'openstackdocstheme', + '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. +copyright = u'2017, OpenStack Developers' + +# openstackdocstheme options +openstackdocs_repo_name = 'openstack/python-venusclient' +openstackdocs_bug_project = 'https://bugs.launchpad.net/python-venusclient' +openstackdocs_bug_tag = '' + +# 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 = 'native' + +# 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 = 'openstackdocs' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'venusclientReleaseNotesdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +# 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', 'venusclientReleaseNotes.tex', + u'venusclient Release Notes Documentation', + u'OpenStack Foundation', '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', 'venusclientrereleasenotes', + u'venusclient Release Notes Documentation', + [u'OpenStack Foundation'], 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', 'venusclient ReleaseNotes', + u'venusclient Release Notes Documentation', + u'OpenStack Foundation', 'venusclientReleaseNotes', + 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + +# -- Options for Internationalization output ------------------------------ +locale_dirs = ['locale/'] diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 0000000..6c63eda --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,12 @@ +============================================ + venusclient Release Notes +============================================ + +.. toctree:: + :maxdepth: 1 + + unreleased + ussuri + train + stein + rocky diff --git a/releasenotes/source/rocky.rst b/releasenotes/source/rocky.rst new file mode 100644 index 0000000..40dd517 --- /dev/null +++ b/releasenotes/source/rocky.rst @@ -0,0 +1,6 @@ +=================================== + Rocky Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/rocky diff --git a/releasenotes/source/stein.rst b/releasenotes/source/stein.rst new file mode 100644 index 0000000..9ea5d36 --- /dev/null +++ b/releasenotes/source/stein.rst @@ -0,0 +1,6 @@ +=========================== + Stein Series Release Notes +=========================== + + .. release-notes:: + :branch: stable/stein diff --git a/releasenotes/source/train.rst b/releasenotes/source/train.rst new file mode 100644 index 0000000..b3c5d08 --- /dev/null +++ b/releasenotes/source/train.rst @@ -0,0 +1,6 @@ +=========================== + Train Series Release Notes +=========================== + + .. release-notes:: + :branch: stable/train diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 0000000..cd22aab --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +============================== + Current Series Release Notes +============================== + +.. release-notes:: diff --git a/releasenotes/source/ussuri.rst b/releasenotes/source/ussuri.rst new file mode 100644 index 0000000..e21e50e --- /dev/null +++ b/releasenotes/source/ussuri.rst @@ -0,0 +1,6 @@ +=========================== +Ussuri Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/ussuri diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..faef588 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,19 @@ +# 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. +pbr!=2.1.0,>=2.0.0 # Apache-2.0 + +six>=1.10.0 # MIT +keystoneauth1>=3.3.0 # Apache-2.0 +stevedore>=1.20.0 # Apache-2.0 +requests>=2.14.2 # Apache-2.0 +oslo.i18n>=3.15.3 # Apache-2.0 +oslo.log>=3.36.0 # Apache-2.0 +oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 +oslo.utils>=3.33.0 # Apache-2.0 +os-client-config>=1.28.0 # Apache-2.0 +osc-lib>=1.8.0 # Apache-2.0 +PrettyTable<0.8,>=0.7.1 # BSD +cryptography!=2.0,>=1.9 # BSD/Apache-2.0 +decorator>=3.4.0 # BSD +openstacksdk>=0.42.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..02a8d78 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,31 @@ +[metadata] +name = python-venusclient +summary = Python client for venus API +description-file = + README.rst +author = OpenStack +author-email = openstack-discuss@lists.openstack.org +python-requires = >=3.6 +classifier = + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[files] +packages = + venusclient + +[entry_points] +console_scripts = + venus = venusclient.shell:main + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f76858d --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +# 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. + +import setuptools + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..88ba997 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,16 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +hacking>=3.0,<3.1.0 # Apache-2.0 + +coverage>=4.0,!=4.4 # Apache-2.0 +python-subunit>=0.0.18 # Apache-2.0/BSD +sphinx>=2.0.0,!=2.1.0 # BSD +oslotest>=1.10.0 # Apache-2.0 +stestr>=1.0.0 # Apache-2.0 +testtools>=1.4.0 # MIT +openstackdocstheme>=2.2.1 # Apache-2.0 +requests-mock>=0.6.0 # Apache-2.0 +# releasenotes +reno>=3.1.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..281a6de --- /dev/null +++ b/tox.ini @@ -0,0 +1,69 @@ +[tox] +minversion = 3.1.1 +envlist = py37,pep8 +skipsdist = True +ignore_basepython_conflict = True + +[testenv] +basepython = python3 +usedevelop = True +whitelist_externals = rm +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} + PYTHONWARNINGS=default::DeprecationWarning + OS_STDOUT_CAPTURE=1 + OS_STDERR_CAPTURE=1 + OS_TEST_TIMEOUT=60 +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = stestr run {posargs} + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +setenv = + VIRTUAL_ENV={envdir} + PYTHON=coverage run --source venusclient --parallel-mode +commands = + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + +[testenv:docs] +commands = sphinx-build -W -b html doc/source doc/build/html + +[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 {posargs} + +[testenv:osc_plugins] +deps = + -c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt} + -r{toxinidir}/requirements.txt + ../../x/pbrx +whitelist_externals = + bash +commands = + # bash wrapper is required to handle a subshell of find. + bash ./tests/install-siblings.sh + pbr freeze + openstack --version + python tests/check_osc_commands.py + +[flake8] +# E123, E125 skipped as they are invalid PEP-8. +# W504 line break after binary operator +# E402 module level import not at top of file +show-source = True +ignore = E123,E125,W504,E402 +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build diff --git a/venusclient/__init__.py b/venusclient/__init__.py new file mode 100644 index 0000000..1c6136d --- /dev/null +++ b/venusclient/__init__.py @@ -0,0 +1,19 @@ +# -*- 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 pbr.version + + +__version__ = pbr.version.VersionInfo( + 'python-venusclient').version_string() diff --git a/venusclient/common/__init__.py b/venusclient/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venusclient/common/apiclient/__init__.py b/venusclient/common/apiclient/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venusclient/common/apiclient/base.py b/venusclient/common/apiclient/base.py new file mode 100644 index 0000000..8a2ca3c --- /dev/null +++ b/venusclient/common/apiclient/base.py @@ -0,0 +1,108 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 OpenStack Foundation +# Copyright 2012 Grid Dynamics +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Base utilities to build API operation managers and objects on top of. +""" + +import copy + + +class Resource(object): + """Base class for OpenStack resources (tenant, user, etc.). + + This is pretty much just a bag for attributes. + """ + + def __init__(self, manager, info, loaded=False): + """Populate and bind to a manager. + + :param manager: BaseManager object + :param info: dictionary representing resource attributes + :param loaded: prevent lazy-loading if set to True + """ + self.manager = manager + self._info = info + self._add_details(info) + self._loaded = loaded + + def __repr__(self): + reprkeys = sorted(k + for k in self.__dict__.keys() + if k[0] != '_' and k != 'manager') + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) + + def _add_details(self, info): + for (k, v) in info.items(): + try: + setattr(self, k, v) + self._info[k] = v + except AttributeError: + # In this case we already defined the attribute on the class + pass + + def __getattr__(self, k): + if k not in self.__dict__: + # NOTE(bcwaldon): disallow lazy-loading if already loaded once + if not self.is_loaded(): + self.get() + return self.__getattr__(k) + + raise AttributeError(k) + else: + return self.__dict__[k] + + def get(self): + """Support for lazy loading details. + + Some clients, such as novaclient have the option to lazy load the + details, details which can be loaded with this function. + """ + # set_loaded() first ... so if we have to bail, we know we tried. + self.set_loaded(True) + if not hasattr(self.manager, 'get'): + return + + new = self.manager.get(self.id) + if new: + self._add_details(new._info) + self._add_details( + {'x_request_id': self.manager.client.last_request_id}) + + def __eq__(self, other): + if not isinstance(other, Resource): + return NotImplemented + # two resources of different types are not equal + if not isinstance(other, self.__class__): + return False + if hasattr(self, 'id') and hasattr(other, 'id'): + return self.id == other.id + return self._info == other._info + + def __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) diff --git a/venusclient/common/apiclient/exceptions.py b/venusclient/common/apiclient/exceptions.py new file mode 100644 index 0000000..7701f37 --- /dev/null +++ b/venusclient/common/apiclient/exceptions.py @@ -0,0 +1,460 @@ +# Copyright 2010 Jacob Kaplan-Moss +# Copyright 2011 Nebula, Inc. +# Copyright 2013 Alessio Ababilov +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Exception definitions. +""" + + +import inspect +import sys + +import six + +from venusclient.i18n import _ + + +class ClientException(Exception): + """The base exception class for all exceptions this library raises.""" + pass + + +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 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.""" + http_status = 0 + message = _("HTTP Error") + + def __init__(self, message=None, details=None, + response=None, request_id=None, + url=None, method=None, http_status=None): + self.http_status = http_status or self.http_status + self.message = message or self.message + self.details = details + self.request_id = request_id + self.response = response + self.url = url + self.method = method + formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) + if request_id: + formatted_string += " (Request-ID: %s)" % request_id + super(HttpError, self).__init__(formatted_string) + + +class HTTPRedirection(HttpError): + """HTTP Redirection.""" + message = _("HTTP Redirection") + + +class HTTPClientError(HttpError): + """Client-side HTTP error. + + Exception for cases in which the client seems to have erred. + """ + message = _("HTTP Client Error") + + +class HttpServerError(HttpError): + """Server-side HTTP error. + + Exception for cases in which the server is aware that it has + erred or is incapable of performing the request. + """ + message = _("HTTP Server Error") + + +class MultipleChoices(HTTPRedirection): + """HTTP 300 - Multiple Choices. + + Indicates multiple options for the resource that the client may follow. + """ + + http_status = 300 + message = _("Multiple Choices") + + +class BadRequest(HTTPClientError): + """HTTP 400 - Bad Request. + + The request cannot be fulfilled due to bad syntax. + """ + http_status = 400 + message = _("Bad Request") + + +class Unauthorized(HTTPClientError): + """HTTP 401 - Unauthorized. + + Similar to 403 Forbidden, but specifically for use when authentication + is required and has failed or has not yet been provided. + """ + http_status = 401 + message = _("Unauthorized") + + +class PaymentRequired(HTTPClientError): + """HTTP 402 - Payment Required. + + Reserved for future use. + """ + http_status = 402 + message = _("Payment Required") + + +class Forbidden(HTTPClientError): + """HTTP 403 - Forbidden. + + The request was a valid request, but the server is refusing to respond + to it. + """ + http_status = 403 + message = _("Forbidden") + + +class NotFound(HTTPClientError): + """HTTP 404 - Not Found. + + The requested resource could not be found but may be available again + in the future. + """ + http_status = 404 + message = _("Not Found") + + +class MethodNotAllowed(HTTPClientError): + """HTTP 405 - Method Not Allowed. + + A request was made of a resource using a request method not supported + by that resource. + """ + http_status = 405 + message = _("Method Not Allowed") + + +class NotAcceptable(HTTPClientError): + """HTTP 406 - Not Acceptable. + + The requested resource is only capable of generating content not + acceptable according to the Accept headers sent in the request. + """ + http_status = 406 + message = _("Not Acceptable") + + +class ProxyAuthenticationRequired(HTTPClientError): + """HTTP 407 - Proxy Authentication Required. + + The client must first authenticate itself with the proxy. + """ + http_status = 407 + message = _("Proxy Authentication Required") + + +class RequestTimeout(HTTPClientError): + """HTTP 408 - Request Timeout. + + The server timed out waiting for the request. + """ + http_status = 408 + message = _("Request Timeout") + + +class Conflict(HTTPClientError): + """HTTP 409 - Conflict. + + Indicates that the request could not be processed because of conflict + in the request, such as an edit conflict. + """ + http_status = 409 + message = _("Conflict") + + +class Gone(HTTPClientError): + """HTTP 410 - Gone. + + Indicates that the resource requested is no longer available and will + not be available again. + """ + http_status = 410 + message = _("Gone") + + +class LengthRequired(HTTPClientError): + """HTTP 411 - Length Required. + + The request did not specify the length of its content, which is + required by the requested resource. + """ + http_status = 411 + message = _("Length Required") + + +class PreconditionFailed(HTTPClientError): + """HTTP 412 - Precondition Failed. + + The server does not meet one of the preconditions that the requester + put on the request. + """ + http_status = 412 + message = _("Precondition Failed") + + +class RequestEntityTooLarge(HTTPClientError): + """HTTP 413 - Request Entity Too Large. + + The request is larger than the server is willing or able to process. + """ + http_status = 413 + message = _("Request Entity Too Large") + + def __init__(self, *args, **kwargs): + try: + self.retry_after = int(kwargs.pop('retry_after')) + except (KeyError, ValueError): + self.retry_after = 0 + + super(RequestEntityTooLarge, self).__init__(*args, **kwargs) + + +class RequestUriTooLong(HTTPClientError): + """HTTP 414 - Request-URI Too Long. + + The URI provided was too long for the server to process. + """ + http_status = 414 + message = _("Request-URI Too Long") + + +class UnsupportedMediaType(HTTPClientError): + """HTTP 415 - Unsupported Media Type. + + The request entity has a media type which the server or resource does + not support. + """ + http_status = 415 + message = _("Unsupported Media Type") + + +class RequestedRangeNotSatisfiable(HTTPClientError): + """HTTP 416 - Requested Range Not Satisfiable. + + The client has asked for a portion of the file, but the server cannot + supply that portion. + """ + http_status = 416 + message = _("Requested Range Not Satisfiable") + + +class ExpectationFailed(HTTPClientError): + """HTTP 417 - Expectation Failed. + + The server cannot meet the requirements of the Expect request-header field. + """ + http_status = 417 + message = _("Expectation Failed") + + +class UnprocessableEntity(HTTPClientError): + """HTTP 422 - Unprocessable Entity. + + The request was well-formed but was unable to be followed due to semantic + errors. + """ + http_status = 422 + message = _("Unprocessable Entity") + + +class InternalServerError(HttpServerError): + """HTTP 500 - Internal Server Error. + + A generic error message, given when no more specific message is suitable. + """ + http_status = 500 + message = _("Internal Server Error") + + +# NotImplemented is a python keyword. +class HttpNotImplemented(HttpServerError): + """HTTP 501 - Not Implemented. + + The server either does not recognize the request method, or it lacks + the ability to fulfill the request. + """ + http_status = 501 + message = _("Not Implemented") + + +class BadGateway(HttpServerError): + """HTTP 502 - Bad Gateway. + + The server was acting as a gateway or proxy and received an invalid + response from the upstream server. + """ + http_status = 502 + message = _("Bad Gateway") + + +class ServiceUnavailable(HttpServerError): + """HTTP 503 - Service Unavailable. + + The server is currently unavailable. + """ + http_status = 503 + message = _("Service Unavailable") + + +class GatewayTimeout(HttpServerError): + """HTTP 504 - Gateway Timeout. + + The server was acting as a gateway or proxy and did not receive a timely + response from the upstream server. + """ + http_status = 504 + message = _("Gateway Timeout") + + +class HttpVersionNotSupported(HttpServerError): + """HTTP 505 - HttpVersion Not Supported. + + The server does not support the HTTP protocol version used in the request. + """ + http_status = 505 + message = _("HTTP Version Not Supported") + + +# _code_map contains all the classes that have http_status attribute. +_code_map = dict( + (getattr(obj, 'http_status', None), obj) + for name, obj in vars(sys.modules[__name__]).items() + if inspect.isclass(obj) and getattr(obj, 'http_status', False) +) + + +def from_response(response, method, url): + """Returns an instance of :class:`HttpError` or subclass based on response. + + :param response: instance of `requests.Response` class + :param method: HTTP method used for request + :param url: URL used for request + """ + + req_id = response.headers.get("x-openstack-request-id") + # NOTE(hdd) true for older versions of nova and cinder + if not req_id: + req_id = response.headers.get("x-compute-request-id") + kwargs = { + "http_status": response.status_code, + "response": response, + "method": method, + "url": url, + "request_id": req_id, + } + if "retry-after" in response.headers: + kwargs["retry_after"] = response.headers["retry-after"] + + content_type = response.headers.get("Content-Type", "") + if content_type.startswith("application/json"): + try: + body = response.json() + except ValueError: + pass + else: + if isinstance(body, dict): + error = body.get(list(body)[0]) + if isinstance(error, dict): + kwargs["message"] = (error.get("message") or + error.get("faultstring")) + kwargs["details"] = (error.get("details") or + six.text_type(body)) + elif content_type.startswith("text/"): + kwargs["details"] = getattr(response, 'text', '') + + try: + cls = _code_map[response.status_code] + except KeyError: + if 500 <= response.status_code < 600: + cls = HttpServerError + elif 400 <= response.status_code < 500: + cls = HTTPClientError + else: + cls = HttpError + return cls(**kwargs) diff --git a/venusclient/common/base.py b/venusclient/common/base.py new file mode 100644 index 0000000..9fe5cc5 --- /dev/null +++ b/venusclient/common/base.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2012 OpenStack LLC. +# 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 copy + +import six.moves.urllib.parse as urlparse + +from venusclient.common.apiclient import base + + +def getid(obj): + """Wrapper to get object's ID. + + 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.id + except AttributeError: + return obj + + +class Manager(object): + """Provides CRUD operations with a particular API.""" + resource_class = None + + def __init__(self, api): + self.api = api + + def _create(self, url, body): + resp, body = self.api.json_request('POST', url, body=body) + if body: + return self.resource_class(self, body) + + def _format_body_data(self, body, response_key): + if response_key: + try: + data = body[response_key] + except KeyError: + return [] + else: + data = body + + if not isinstance(data, list): + data = [data] + + return data + + def _list_pagination(self, url, response_key=None, obj_class=None, + limit=None): + """Retrieve a list of items. + + The venus API is configured to return a maximum number of + items per request, (FIXME: see venus's api.max_limit option). This + iterates over the 'next' link (pagination) in the responses, + to get the number of items specified by 'limit'. If 'limit' + is None this function will continue pagination until there are + no more values to be returned. + + :param url: a partial URL, e.g. '/nodes' + :param response_key: the key to be looked up in response + dictionary, e.g. 'nodes' + :param obj_class: class for constructing the returned objects. + :param limit: maximum number of items to return. If None returns + everything. + + """ + if obj_class is None: + obj_class = self.resource_class + + if limit is not None: + limit = int(limit) + + object_list = [] + object_count = 0 + limit_reached = False + while url: + resp, body = self.api.json_request('GET', url) + data = self._format_body_data(body, response_key) + for obj in data: + object_list.append(obj_class(self, obj, loaded=True)) + object_count += 1 + if limit and object_count >= limit: + # break the for loop + limit_reached = True + break + + # break the while loop and return + if limit_reached: + break + + url = body.get('next') + if url: + # NOTE(lucasagomes): We need to edit the URL to remove + # the scheme and netloc + url_parts = list(urlparse.urlparse(url)) + url_parts[0] = url_parts[1] = '' + url = urlparse.urlunparse(url_parts) + + return object_list + + def _list(self, url, response_key=None, obj_class=None, body=None): + resp, body = self.api.json_request('GET', url) + + if obj_class is None: + obj_class = self.resource_class + + data = self._format_body_data(body, response_key) + return [obj_class(self, res, loaded=True) for res in data if res] + + def _update(self, url, body=None, method='PATCH', response_key=None): + if body: + resp, resp_body = self.api.json_request(method, url, body=body) + else: + resp, resp_body = self.api.raw_request(method, url) + # PATCH/PUT requests may not return a body + if resp_body: + return self.resource_class(self, resp_body) + + def _delete(self, url): + self.api.raw_request('DELETE', url) + + +class Resource(base.Resource): + """Represents a particular instance of an object (tenant, user, etc). + + This is pretty much just a bag for attributes. + """ + + def to_dict(self): + return copy.deepcopy(self._info) diff --git a/venusclient/common/cliutils.py b/venusclient/common/cliutils.py new file mode 100644 index 0000000..c7c1472 --- /dev/null +++ b/venusclient/common/cliutils.py @@ -0,0 +1,508 @@ +# Copyright 2012 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# W0603: Using the global statement +# W0621: Redefining name %s from outer scope +# pylint: disable=W0603,W0621 + +from __future__ import print_function + +import getpass +import inspect +import os +import sys +import textwrap + +import decorator +from oslo_utils import encodeutils +from oslo_utils import strutils +import prettytable +import six +from six import moves + +from venusclient.common.apiclient import exceptions +from venusclient.i18n import _ + + +DEPRECATION_BASE = ('%sThe --%s parameter is deprecated and ' + 'will be removed in a future release. Use the ' + '<%s> positional parameter %s.') + +NAME_DEPRECATION_HELP = DEPRECATION_BASE % ('', 'name', 'name', 'instead') + +NAME_DEPRECATION_WARNING = DEPRECATION_BASE % ( + 'WARNING: ', 'name', 'name', 'to avoid seeing this message') + +CLUSTER_DEPRECATION_HELP = DEPRECATION_BASE % ('', 'cluster', 'cluster', + 'instead') + +CLUSTER_DEPRECATION_WARNING = DEPRECATION_BASE % ( + 'WARNING: ', 'cluster', 'cluster', 'to avoid seeing this message') + +VENUS_CLIENT_DEPRECATION_WARNING = ( + 'WARNING: The venus client is deprecated and will be removed in a future ' + 'release.\nUse the OpenStack client to avoid seeing this message.') + + +def deprecation_message(preamble, new_name): + msg = ('%s This parameter is deprecated and will be removed in a future ' + 'release. Use --%s instead.' % (preamble, new_name)) + return msg + + +class MissingArgs(Exception): + """Supplied arguments are not sufficient for calling a function.""" + def __init__(self, missing): + self.missing = missing + msg = _("Missing arguments: %s") % ", ".join(missing) + super(MissingArgs, self).__init__(msg) + + +class DuplicateArgs(Exception): + """More than one of the same argument type was passed.""" + def __init__(self, param, dupes): + msg = _('Duplicate "%(param)s" arguments: %(dupes)s') % { + 'param': param, 'dupes': ", ".join(dupes)} + super(DuplicateArgs, self).__init__(msg) + + +def validate_args(fn, *args, **kwargs): + """Check that the supplied args are sufficient for calling a function. + + >>> validate_args(lambda a: None) + Traceback (most recent call last): + ... + MissingArgs: Missing argument(s): a + >>> validate_args(lambda a, b, c, d: None, 0, c=1) + Traceback (most recent call last): + ... + MissingArgs: Missing argument(s): b, d + + :param fn: the function to check + :param arg: the positional arguments supplied + :param kwargs: the keyword arguments supplied + """ + argspec = inspect.getargspec(fn) + + num_defaults = len(argspec.defaults or []) + required_args = argspec.args[:len(argspec.args) - num_defaults] + + def isbound(method): + return getattr(method, '__self__', None) is not None + + if isbound(fn): + required_args.pop(0) + + missing = [arg for arg in required_args if arg not in kwargs] + missing = missing[len(args):] + if missing: + raise MissingArgs(missing) + + +def validate_name_args(positional_name, optional_name): + if optional_name: + print(NAME_DEPRECATION_WARNING) + if positional_name and optional_name: + raise DuplicateArgs("", (positional_name, optional_name)) + + +def validate_cluster_args(positional_cluster, optional_cluster): + if optional_cluster: + print(CLUSTER_DEPRECATION_WARNING) + if positional_cluster and optional_cluster: + raise DuplicateArgs("", (positional_cluster, + optional_cluster)) + + +def deprecated(message): + """Decorator for marking a call as deprecated by printing a given message. + + Example: + >>> @deprecated("Bay functions are deprecated and should be replaced by " + ... "calls to cluster") + ... def bay_create(args): + ... pass + """ + @decorator.decorator + def wrapper(func, *args, **kwargs): + print(message) + return func(*args, **kwargs) + return wrapper + + +def deprecation_map(dep_map): + """Decorator for applying a map of deprecating arguments to a function. + + The map connects deprecating arguments and their replacements. The + shell.py script uses this map to create mutually exclusive argument groups + in argparse and also prints a deprecation warning telling the user to + switch to the updated argument. + + NOTE: This decorator MUST be the outermost in the chain of argument + decorators to work correctly. + + Example usage: + >>> @deprecation_map({ "old-argument": "new-argument" }) + ... @args("old-argument", required=True) + ... @args("new-argument", required=True) + ... def do_command_line_stuff(): + ... pass + """ + def _decorator(func): + if not hasattr(func, 'arguments'): + return func + + func.deprecated_groups = [] + for old_param, new_param in dep_map.items(): + old_info, new_info = None, None + required = False + for (args, kwargs) in func.arguments: + if old_param in args: + old_info = (args, kwargs) + # Old arguments shouldn't be required if they were not + # previously, so prioritize old requirement + if 'required' in kwargs: + required = kwargs['required'] + # Set to false so argparse doesn't get angry + kwargs['required'] = False + elif new_param in args: + new_info = (args, kwargs) + kwargs['required'] = False + if old_info and new_info: + break + # Add a tuple of (old, new, required), which in turn is: + # ((old_args, old_kwargs), (new_args, new_kwargs), required) + func.deprecated_groups.append((old_info, new_info, required)) + # Remove arguments that would be duplicated by the groups we made + func.arguments.remove(old_info) + func.arguments.remove(new_info) + + return func + return _decorator + + +def arg(*args, **kwargs): + """Decorator for CLI args. + + Example: + + >>> @arg("name", help="Name of the new entity") + ... def entity_create(args): + ... pass + """ + def _decorator(func): + add_arg(func, *args, **kwargs) + return func + return _decorator + + +def env(*args, **kwargs): + """Returns the first environment variable set. + + If all are empty, defaults to '' or keyword arg `default`. + """ + for arg in args: + value = os.environ.get(arg) + if value: + return value + return kwargs.get('default', '') + + +def add_arg(func, *args, **kwargs): + """Bind CLI arguments to a shell.py `do_foo` function.""" + + if not hasattr(func, 'arguments'): + func.arguments = [] + + # NOTE(sirp): avoid dups that can occur when the module is shared across + # tests. + if (args, kwargs) not in func.arguments: + # Because of the semantics of decorator composition if we just append + # to the options list positional options will appear to be backwards. + func.arguments.insert(0, (args, kwargs)) + + +def unauthenticated(func): + """Adds 'unauthenticated' attribute to decorated function. + + Usage: + + >>> @unauthenticated + ... def mymethod(f): + ... pass + """ + func.unauthenticated = True + return func + + +def isunauthenticated(func): + """Checks if the function does not require authentication. + + Mark such functions with the `@unauthenticated` decorator. + + :returns: bool + """ + return getattr(func, 'unauthenticated', False) + + +def print_list(objs, fields, formatters=None, sortby_index=0, + mixed_case_fields=None, field_labels=None): + """Print a list or objects as a table, one row per object. + + :param objs: iterable of :class:`Resource` + :param fields: attributes that correspond to columns, in order + :param formatters: `dict` of callables for field formatting + :param sortby_index: index of the field for sorting table rows + :param mixed_case_fields: fields corresponding to object attributes that + have mixed case names (e.g., 'serverId') + :param field_labels: Labels to use in the heading of the table, default to + fields. + """ + formatters = formatters or {} + mixed_case_fields = mixed_case_fields or [] + field_labels = field_labels or fields + if len(field_labels) != len(fields): + raise ValueError(_("Field labels list %(labels)s has different number " + "of elements than fields list %(fields)s"), + {'labels': field_labels, 'fields': fields}) + + if sortby_index is None: + kwargs = {} + else: + kwargs = {'sortby': field_labels[sortby_index]} + pt = prettytable.PrettyTable(field_labels) + pt.align = 'l' + + for o in objs: + row = [] + for field in fields: + data = '-' + if field in formatters: + data = formatters[field](o) + else: + if field in mixed_case_fields: + field_name = field.replace(' ', '_') + else: + field_name = field.lower().replace(' ', '_') + data = getattr(o, field_name, '') + if data is None: + data = '-' + row.append(data) + pt.add_row(row) + + if six.PY3: + print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode()) + else: + print(encodeutils.safe_encode(pt.get_string(**kwargs))) + + +def keys_and_vals_to_strs(dictionary): + """Recursively convert a dictionary's keys and values to strings. + + :param dictionary: dictionary whose keys/vals are to be converted to strs + """ + def to_str(k_or_v): + if isinstance(k_or_v, dict): + return keys_and_vals_to_strs(k_or_v) + elif isinstance(k_or_v, six.text_type): + return str(k_or_v) + else: + return k_or_v + return dict((to_str(k), to_str(v)) for k, v in dictionary.items()) + + +def print_dict(dct, dict_property="Property", wrap=0): + """Print a `dict` as a table of two columns. + + :param dct: `dict` to print + :param dict_property: name of the first column + :param wrap: wrapping for the second column + """ + pt = prettytable.PrettyTable([dict_property, 'Value']) + pt.align = 'l' + for k, v in dct.items(): + # convert dict to str to check length + if isinstance(v, dict): + v = six.text_type(keys_and_vals_to_strs(v)) + if wrap > 0: + v = textwrap.fill(six.text_type(v), wrap) + # if value has a newline, add in multiple rows + # e.g. fault with stacktrace + if v and isinstance(v, six.string_types) and r'\n' in v: + lines = v.strip().split(r'\n') + col1 = k + for line in lines: + pt.add_row([col1, line]) + col1 = '' + elif isinstance(v, list): + val = str([str(i) for i in v]) + if val is None: + val = '-' + pt.add_row([k, val]) + else: + if v is None: + v = '-' + pt.add_row([k, v]) + + if six.PY3: + print(encodeutils.safe_encode(pt.get_string()).decode()) + else: + print(encodeutils.safe_encode(pt.get_string())) + + +def get_password(max_password_prompts=3): + """Read password from TTY.""" + verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD")) + pw = None + if hasattr(sys.stdin, "isatty") and sys.stdin.isatty(): + # Check for Ctrl-D + try: + for __ in moves.range(max_password_prompts): + pw1 = getpass.getpass("OS Password: ") + if verify: + pw2 = getpass.getpass("Please verify: ") + else: + pw2 = pw1 + if pw1 == pw2 and pw1: + pw = pw1 + break + except EOFError: + pass + return pw + + +def service_type(stype): + """Adds 'service_type' attribute to decorated function. + + Usage: + + .. code-block:: python + + @service_type('volume') + def mymethod(f): + ... + """ + def inner(f): + f.service_type = stype + return f + return inner + + +def get_service_type(f): + """Retrieves service type from function.""" + return getattr(f, 'service_type', None) + + +def pretty_choice_list(l): + return ', '.join("'%s'" % i for i in l) + + +def exit(msg=''): + if msg: + print(msg, file=sys.stderr) + sys.exit(1) + + +def _format_field_name(attr): + """Format an object attribute in a human-friendly way.""" + # Split at ':' and leave the extension name as-is. + parts = attr.rsplit(':', 1) + name = parts[-1].replace('_', ' ') + # Don't title() on mixed case + if name.isupper() or name.islower(): + name = name.title() + parts[-1] = name + return ': '.join(parts) + + +def make_field_formatter(attr, filters=None): + """Given an object attribute. + + Return a formatted field name and a formatter suitable for passing to + print_list. + Optionally pass a dict mapping attribute names to a function. The function + will be passed the value of the attribute and should return the string to + display. + """ + + filter_ = None + if filters: + filter_ = filters.get(attr) + + def get_field(obj): + field = getattr(obj, attr, '') + if field and filter_: + field = filter_(field) + return field + + name = _format_field_name(attr) + formatter = get_field + return name, formatter + + +def _get_list_table_columns_and_formatters(fields, objs, exclude_fields=(), + filters=None): + """Check and add fields to output columns. + + If there is any value in fields that not an attribute of obj, + CommandError will be raised. + If fields has duplicate values (case sensitive), we will make them unique + and ignore duplicate ones. + :param fields: A list of string contains the fields to be printed. + :param objs: An list of object which will be used to check if field is + valid or not. Note, we don't check fields if obj is None or + empty. + :param exclude_fields: A tuple of string which contains the fields to be + excluded. + :param filters: A dictionary defines how to get value from fields, this + is useful when field's value is a complex object such as + dictionary. + :return: columns, formatters. + columns is a list of string which will be used as table header. + formatters is a dictionary specifies how to display the value + of the field. + They can be [], {}. + :raise: venusclient.common.apiclient.exceptions.CommandError. + """ + + if objs and isinstance(objs, list): + obj = objs[0] + else: + obj = None + fields = None + + columns = [] + formatters = {} + + if fields: + non_existent_fields = [] + exclude_fields = set(exclude_fields) + + for field in fields.split(','): + if not hasattr(obj, field): + non_existent_fields.append(field) + continue + if field in exclude_fields: + continue + field_title, formatter = make_field_formatter(field, filters) + columns.append(field_title) + formatters[field_title] = formatter + exclude_fields.add(field) + + if non_existent_fields: + raise exceptions.CommandError( + _("Non-existent fields are specified: %s") % + non_existent_fields + ) + return columns, formatters diff --git a/venusclient/common/http.py b/venusclient/common/http.py new file mode 100644 index 0000000..f34bc4c --- /dev/null +++ b/venusclient/common/http.py @@ -0,0 +1,347 @@ +# 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. +# + +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 venusclient.common import exceptions as exc +from venusclient.common import utils +from venusclient.i18n import _ + +LOG = logging.getLogger(__name__) + +USER_AGENT = 'python-venusclient' +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("System ca file could not be found.") + + +class HTTPClient(object): + + def __init__(self, endpoint, **kwargs): + self.endpoint = endpoint + self.auth_url = kwargs.get('auth_url') + self.auth_token = kwargs.get('token') + self.username = kwargs.get('username') + self.password = kwargs.get('password') + self.region_name = kwargs.get('region_name') + self.include_pass = kwargs.get('include_pass') + self.endpoint_url = endpoint + + self.cert_file = kwargs.get('cert_file') + self.key_file = kwargs.get('key_file') + self.timeout = kwargs.get('timeout') + + self.ssl_connection_params = { + 'ca_file': kwargs.get('ca_file'), + 'cert_file': kwargs.get('cert_file'), + 'key_file': kwargs.get('key_file'), + 'insecure': kwargs.get('insecure'), + } + + self.verify_cert = None + if parse.urlparse(endpoint).scheme == "https": + if kwargs.get('insecure'): + self.verify_cert = False + else: + self.verify_cert = kwargs.get('ca_file', get_system_ca_file()) + + # FIXME(RuiChen): We need this for compatibility with the oslo + # apiclient we should move to inheriting this class from the oslo + # HTTPClient + self.last_request_id = None + + def safe_header(self, name, value): + if name in SENSITIVE_HEADERS: + # because in python3 byte string handling is ... ug + v = value.encode('utf-8') + h = hashlib.sha1(v) + d = h.hexdigest() + return encodeutils.safe_decode(name), "{SHA1}%s" % d + else: + return (encodeutils.safe_decode(name), + encodeutils.safe_decode(value)) + + def log_curl_request(self, method, url, kwargs): + curl = ['curl -g -i -X %s' % method] + + for (key, value) in kwargs['headers'].items(): + header = '-H \'%s: %s\'' % self.safe_header(key, value) + curl.append(header) + + conn_params_fmt = [ + ('key_file', '--key %s'), + ('cert_file', '--cert %s'), + ('ca_file', '--cacert %s'), + ] + for (key, fmt) in conn_params_fmt: + value = self.ssl_connection_params.get(key) + if value: + curl.append(fmt % value) + + if self.ssl_connection_params.get('insecure'): + curl.append('-k') + + if 'data' in kwargs: + curl.append('-d \'%s\'' % kwargs['data']) + + curl.append('%s%s' % (self.endpoint, url)) + LOG.debug(' '.join(curl)) + + @staticmethod + def log_http_response(resp): + status = (resp.raw.version / 10.0, resp.status_code, resp.reason) + dump = ['\nHTTP/%.1f %s %s' % status] + dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()]) + dump.append('') + if resp.content: + content = resp.content + if isinstance(content, six.binary_type): + content = content.decode() + dump.extend([content, '']) + LOG.debug('\n'.join(dump)) + + def _http_request(self, url, method, **kwargs): + """Send an http request with the specified characteristics. + + Wrapper around requests.request to handle tasks such as + setting headers and error handling. + """ + # Copy the kwargs so we can reuse the original in case of redirects + kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) + kwargs['headers'].setdefault('User-Agent', USER_AGENT) + if self.auth_token: + kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) + else: + kwargs['headers'].update(self.credentials_headers()) + if self.auth_url: + kwargs['headers'].setdefault('X-Auth-Url', self.auth_url) + if self.region_name: + kwargs['headers'].setdefault('X-Region-Name', self.region_name) + if self.include_pass and 'X-Auth-Key' not in kwargs['headers']: + kwargs['headers'].update(self.credentials_headers()) + if osprofiler_web: + kwargs['headers'].update(osprofiler_web.get_trace_id_headers()) + + self.log_curl_request(method, url, kwargs) + + if self.cert_file and self.key_file: + kwargs['cert'] = (self.cert_file, self.key_file) + + if self.verify_cert is not None: + kwargs['verify'] = self.verify_cert + + if self.timeout is not None: + kwargs['timeout'] = float(self.timeout) + + # Allow caller to specify not to follow redirects, in which case we + # just return the redirect response. Useful for using stacks:lookup. + redirect = kwargs.pop('redirect', True) + + # Since requests does not follow the RFC when doing redirection to sent + # back the same method on a redirect we are simply bypassing it. For + # example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says + # that we should follow that URL with the same method as before, + # requests doesn't follow that and send a GET instead for the method. + # Hopefully this could be fixed as they say in a comment in a future + # point version i.e.: 3.x + # See issue: https://github.com/kennethreitz/requests/issues/1704 + allow_redirects = False + + # Use fully qualified URL from response header for redirects + if not parse.urlparse(url).netloc: + url = self.endpoint_url + url + + try: + resp = requests.request( + method, + url, + allow_redirects=allow_redirects, + **kwargs) + except socket.gaierror as e: + message = (_("Error finding address for %(url)s: %(e)s") % + {'url': self.endpoint_url + url, 'e': e}) + raise exc.EndpointNotFound(message=message) + except (socket.error, socket.timeout) as e: + endpoint = self.endpoint + message = (_("Error communicating with %(endpoint)s %(e)s") % + {'endpoint': endpoint, 'e': e}) + raise exc.ConnectionError(message=message) + + self.log_http_response(resp) + + if not ('X-Auth-Key' in kwargs['headers']) and ( + resp.status_code == 401 or + (resp.status_code == 500 and "(HTTP 401)" in resp.content)): + raise exc.AuthorizationFailure(_("Authentication failed: %s") + % resp.content) + elif 400 <= resp.status_code < 600: + raise exc.from_response(resp, method, url) + elif resp.status_code in (301, 302, 305): + # Redirected. Reissue the request to the new location, + # unless caller specified redirect=False + if redirect: + location = resp.headers.get('location') + location = self.strip_endpoint(location) + resp = self._http_request(location, method, **kwargs) + elif resp.status_code == 300: + raise exc.from_response(resp, method, url) + + return resp + + def strip_endpoint(self, location): + if location is None: + message = _("Location not returned with redirect") + raise exc.EndpointException(message=message) + if location.lower().startswith(self.endpoint): + return location[len(self.endpoint):] + else: + return location + + def credentials_headers(self): + creds = {} + # NOTE(RuiChen): When deferred_auth_method=password, Heat + # encrypts and stores username/password. For Keystone v3, the + # intent is to use trusts since SHARDY is working towards + # deferred_auth_method=trusts as the default. + if self.username: + creds['X-Auth-User'] = self.username + if self.password: + creds['X-Auth-Key'] = self.password + return creds + + def json_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', 'application/json') + kwargs['headers'].setdefault('Accept', 'application/json') + + if 'data' in kwargs: + kwargs['data'] = jsonutils.dumps(kwargs['data']) + + resp = self._http_request(url, method, **kwargs) + body = utils.get_response_body(resp) + return resp, body + + def raw_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', + 'application/octet-stream') + resp = self._http_request(url, method, **kwargs) + body = utils.get_response_body(resp) + return resp, body + + def head(self, url, **kwargs): + return self.json_request("HEAD", url, **kwargs) + + def get(self, url, **kwargs): + return self.json_request("GET", url, **kwargs) + + def post(self, url, **kwargs): + return self.json_request("POST", url, **kwargs) + + def put(self, url, **kwargs): + return self.json_request("PUT", url, **kwargs) + + def delete(self, url, **kwargs): + return self.raw_request("DELETE", url, **kwargs) + + def patch(self, url, **kwargs): + return self.json_request("PATCH", url, **kwargs) + + +class SessionClient(adapter.LegacyJsonAdapter): + """HTTP client based on Keystone client session.""" + + def request(self, url, method, **kwargs): + redirect = kwargs.get('redirect') + kwargs.setdefault('user_agent', USER_AGENT) + + if 'data' in kwargs: + kwargs['json'] = kwargs.pop('data') + + resp, body = super(SessionClient, self).request( + url, method, + raise_exc=False, + **kwargs) + + if 400 <= resp.status_code < 600: + raise exc.from_response(resp, method, url) + elif resp.status_code in (301, 302, 305): + if redirect: + location = resp.headers.get('location') + path = self.strip_endpoint(location) + resp, body = self.request(path, method, **kwargs) + elif resp.status_code == 300: + raise exc.from_response(resp, method, url) + + return resp, body + + def credentials_headers(self): + return {} + + def strip_endpoint(self, location): + if location is None: + message = _("Location not returned with redirect") + raise exc.EndpointException(message=message) + if (self.endpoint_override is not None and + location.lower().startswith(self.endpoint_override.lower())): + return location[len(self.endpoint_override):] + else: + return location + + +def _construct_http_client(endpoint=None, username=None, password=None, + include_pass=None, endpoint_type=None, + auth_url=None, **kwargs): + session = kwargs.pop('session', None) + auth = kwargs.pop('auth', None) + + if session: + kwargs['endpoint_override'] = endpoint + return SessionClient(session, auth=auth, **kwargs) + else: + return HTTPClient(endpoint=endpoint, username=username, + password=password, include_pass=include_pass, + endpoint_type=endpoint_type, auth_url=auth_url, + **kwargs) diff --git a/venusclient/common/httpclient.py b/venusclient/common/httpclient.py new file mode 100644 index 0000000..f6c95a4 --- /dev/null +++ b/venusclient/common/httpclient.py @@ -0,0 +1,435 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2012 OpenStack LLC. +# 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 json +import logging +import os +import socket +import ssl + +from keystoneauth1 import adapter +from oslo_utils import importutils +import six +import six.moves.urllib.parse as urlparse + +from venusclient import exceptions + +osprofiler_web = importutils.try_import("osprofiler.web") + +LOG = logging.getLogger(__name__) +USER_AGENT = 'python-venusclient' +CHUNKSIZE = 1024 * 64 # 64kB + +API_VERSION = '/v1' +DEFAULT_API_VERSION = 'latest' + + +def _extract_error_json(body): + """Return error_message from the HTTP response body.""" + error_json = {} + try: + body_json = json.loads(body) + if 'error_message' in body_json: + raw_msg = body_json['error_message'] + error_json = json.loads(raw_msg) + elif 'error' in body_json: + error_body = body_json['error'] + error_json = {'faultstring': error_body['title'], + 'debuginfo': error_body['message']} + else: + error_body = body_json['errors'][0] + error_json = {'faultstring': error_body['title']} + if 'detail' in error_body: + error_json['debuginfo'] = error_body['detail'] + elif 'description' in error_body: + error_json['debuginfo'] = error_body['description'] + + except ValueError: + return {} + + return error_json + + +class HTTPClient(object): + + def __init__(self, endpoint, api_version=DEFAULT_API_VERSION, **kwargs): + self.endpoint = endpoint + self.auth_token = kwargs.get('token') + self.auth_ref = kwargs.get('auth_ref') + self.api_version = api_version + self.connection_params = self.get_connection_params(endpoint, **kwargs) + + @staticmethod + def get_connection_params(endpoint, **kwargs): + parts = urlparse.urlparse(endpoint) + + # trim API version and trailing slash from endpoint + path = parts.path + path = path.rstrip('/').rstrip(API_VERSION) + + _args = (parts.hostname, parts.port, path) + _kwargs = {'timeout': (float(kwargs.get('timeout')) + if kwargs.get('timeout') else 600)} + + if parts.scheme == 'https': + _class = VerifiedHTTPSConnection + _kwargs['ca_file'] = kwargs.get('ca_file', None) + _kwargs['cert_file'] = kwargs.get('cert_file', None) + _kwargs['key_file'] = kwargs.get('key_file', None) + _kwargs['insecure'] = kwargs.get('insecure', False) + elif parts.scheme == 'http': + _class = six.moves.http_client.HTTPConnection + else: + msg = 'Unsupported scheme: %s' % parts.scheme + raise exceptions.EndpointException(msg) + + return (_class, _args, _kwargs) + + def get_connection(self): + _class = self.connection_params[0] + return _class(*self.connection_params[1][0:2], + **self.connection_params[2]) + + def log_curl_request(self, method, url, kwargs): + curl = ['curl -i -X %s' % method] + + for (key, value) in kwargs['headers'].items(): + header = '-H \'%s: %s\'' % (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.connection_params[2].get(key) + if value: + curl.append(fmt % value) + + if self.connection_params[2].get('insecure'): + curl.append('-k') + + if 'body' in kwargs: + curl.append('-d \'%s\'' % kwargs['body']) + + curl.append('%s/%s' % (self.endpoint, url.lstrip(API_VERSION))) + LOG.debug(' '.join(curl)) + + @staticmethod + def log_http_response(resp, body=None): + status = (resp.version / 10.0, resp.status, resp.reason) + dump = ['\nHTTP/%.1f %s %s' % status] + dump.extend(['%s: %s' % (k, v) for k, v in resp.getheaders()]) + dump.append('') + if body: + dump.extend([body, '']) + LOG.debug('\n'.join(dump)) + + def _make_connection_url(self, url): + (_class, _args, _kwargs) = self.connection_params + base_url = _args[2] + return '%s/%s' % (base_url, url.lstrip('/')) + + def _http_request(self, url, method, **kwargs): + """Send an http request with the specified characteristics. + + Wrapper around httplib.HTTP(S)Connection.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.api_version: + version_string = 'accelerator %s' % self.api_version + kwargs['headers'].setdefault( + 'OpenStack-API-Version', version_string) + if self.auth_token: + kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) + + self.log_curl_request(method, url, kwargs) + conn = self.get_connection() + + try: + conn_url = self._make_connection_url(url) + conn.request(method, conn_url, **kwargs) + resp = conn.getresponse() + except socket.gaierror as e: + message = ("Error finding address for %(url)s: %(e)s" + % dict(url=url, e=e)) + raise exceptions.EndpointNotFound(message) + except (socket.error, socket.timeout) as e: + endpoint = self.endpoint + message = ("Error communicating with %(endpoint)s %(e)s" + % dict(endpoint=endpoint, e=e)) + raise exceptions.ConnectionRefused(message) + + body_iter = ResponseBodyIterator(resp) + + # Read body into string if it isn't obviously image data + body_str = None + if resp.getheader('content-type', None) != 'application/octet-stream': + # decoding byte to string is necessary for Python 3.4 compatibility + # this issues has not been found with Python 3.4 unit tests + # because the test creates a fake http response of type str + # the if statement satisfies test (str) and real (bytes) behavior + body_list = [ + chunk.decode("utf-8") if isinstance(chunk, bytes) + else chunk for chunk in body_iter + ] + body_str = ''.join(body_list) + self.log_http_response(resp, body_str) + body_iter = six.StringIO(body_str) + else: + self.log_http_response(resp) + + if 400 <= resp.status < 600: + LOG.warning("Request returned failure status.") + error_json = _extract_error_json(body_str) + raise exceptions.from_response( + resp, method, url, + error_json.get('faultstring'), + error_json.get('debuginfo')) + elif resp.status in (301, 302, 305): + # Redirected. Reissue the request to the new location. + return self._http_request(resp['location'], method, **kwargs) + elif resp.status == 300: + raise exceptions.from_response(resp, method=method, url=url) + + return resp, body_iter + + def json_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', 'application/json') + kwargs['headers'].setdefault('Accept', 'application/json') + + if 'body' in kwargs: + kwargs['body'] = json.dumps(kwargs['body']) + + resp, body_iter = self._http_request(url, method, **kwargs) + content_type = resp.getheader('content-type', None) + + if resp.status == 204 or resp.status == 205 or content_type is None: + return resp, list() + + if 'application/json' in content_type: + body = ''.join([chunk for chunk in body_iter]) + try: + body = json.loads(body) + except ValueError: + LOG.error('Could not decode response body as JSON') + else: + body = None + + return resp, body + + def raw_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', + 'application/octet-stream') + return self._http_request(url, method, **kwargs) + + +class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection): + """httplib-compatibile connection using client-side SSL authentication + + :see http://code.activestate.com/recipes/ + 577548-https-httplib-client-connection-with-certificate-v/ + """ + + def __init__(self, host, port, key_file=None, cert_file=None, + ca_file=None, timeout=None, insecure=False): + six.moves.http_client.HTTPSConnection.__init__(self, host, port, + key_file=key_file, + cert_file=cert_file) + self.key_file = key_file + self.cert_file = cert_file + if ca_file is not None: + self.ca_file = ca_file + else: + self.ca_file = self.get_system_ca_file() + self.timeout = timeout + self.insecure = insecure + + def connect(self): + """Connect to a host on a given (SSL) port. + + If ca_file is pointing somewhere, use it to check Server Certificate. + + Redefined/copied and extended from httplib.py:1105 (Python 2.6.x). + This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to + ssl.wrap_socket(), which forces SSL to check server certificate against + our client certificate. + """ + sock = socket.create_connection((self.host, self.port), self.timeout) + + if self._tunnel_host: + self.sock = sock + self._tunnel() + + if self.insecure is True: + kwargs = {'cert_reqs': ssl.CERT_NONE} + else: + kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file} + + if self.cert_file: + kwargs['certfile'] = self.cert_file + if self.key_file: + kwargs['keyfile'] = self.key_file + + self.sock = ssl.wrap_socket(sock, **kwargs) + + @staticmethod + def get_system_ca_file(): + """Return path to system default CA file.""" + # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, + # Suse, FreeBSD/OpenBSD + ca_path = ['/etc/ssl/certs/ca-certificates.crt', + '/etc/pki/tls/certs/ca-bundle.crt', + '/etc/ssl/ca-bundle.pem', + '/etc/ssl/cert.pem'] + for ca in ca_path: + if os.path.exists(ca): + return ca + return None + + +class SessionClient(adapter.LegacyJsonAdapter): + """HTTP client based on Keystone client session.""" + + def __init__(self, user_agent=USER_AGENT, logger=LOG, + api_version=DEFAULT_API_VERSION, *args, **kwargs): + self.user_agent = USER_AGENT + self.api_version = api_version + super(SessionClient, self).__init__(*args, **kwargs) + + def _http_request(self, url, method, **kwargs): + if url.startswith(API_VERSION): + url = url[len(API_VERSION):] + + kwargs.setdefault('user_agent', self.user_agent) + kwargs.setdefault('auth', self.auth) + kwargs.setdefault('endpoint_override', self.endpoint_override) + + # 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', self.user_agent) + # NOTE(tovin07): osprofiler_web.get_trace_id_headers does not add any + # headers in case if osprofiler is not initialized. + if osprofiler_web: + kwargs['headers'].update(osprofiler_web.get_trace_id_headers()) + if self.api_version: + version_string = 'accelerator %s' % self.api_version + kwargs['headers'].setdefault( + 'OpenStack-API-Version', version_string) + + endpoint_filter = kwargs.setdefault('endpoint_filter', {}) + endpoint_filter.setdefault('interface', self.interface) + endpoint_filter.setdefault('service_type', self.service_type) + endpoint_filter.setdefault('region_name', self.region_name) + + resp = self.session.request(url, method, + raise_exc=False, **kwargs) + + if 400 <= resp.status_code < 600: + error_json = _extract_error_json(resp.content) + raise exceptions.from_response( + resp, method, url, + error_json.get('faultstring'), + error_json.get('debuginfo')) + elif resp.status_code in (301, 302, 305): + # Redirected. Reissue the request to the new location. + location = resp.headers.get('location') + resp = self._http_request(location, method, **kwargs) + elif resp.status_code == 300: + raise exceptions.from_response(resp, method=method, url=url) + return resp + + def json_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', 'application/json') + kwargs['headers'].setdefault('Accept', 'application/json') + if 'body' in kwargs: + kwargs['data'] = json.dumps(kwargs.pop('body')) + + resp = self._http_request(url, method, **kwargs) + body = resp.content + content_type = resp.headers.get('content-type', None) + status = resp.status_code + if status == 204 or status == 205 or content_type is None: + return resp, list() + if 'application/json' in content_type: + try: + body = resp.json() + except ValueError: + LOG.error('Could not decode response body as JSON') + else: + body = None + + return resp, body + + def raw_request(self, method, url, **kwargs): + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Type', + 'application/octet-stream') + return self._http_request(url, method, **kwargs) + + +class ResponseBodyIterator(object): + """A class that acts as an iterator over an HTTP response.""" + + def __init__(self, resp): + self.resp = resp + + def __iter__(self): + while True: + try: + yield self.next() + except StopIteration: + return + + def __bool__(self): + return hasattr(self, 'items') + + __nonzero__ = __bool__ # Python 2.x compatibility + + def next(self): + chunk = self.resp.read(CHUNKSIZE) + if chunk: + return chunk + else: + raise StopIteration() + + +def _construct_http_client(*args, **kwargs): + session = kwargs.pop('session', None) + auth = kwargs.pop('auth', None) + + if session: + service_type = kwargs.pop('service_type', 'baremetal') + interface = kwargs.pop('endpoint_type', None) + region_name = kwargs.pop('region_name', None) + return SessionClient(session=session, + auth=auth, + interface=interface, + service_type=service_type, + region_name=region_name, + service_name=None, + user_agent='python-venusclient') + else: + return HTTPClient(*args, **kwargs) diff --git a/venusclient/common/utils.py b/venusclient/common/utils.py new file mode 100644 index 0000000..ad70eb7 --- /dev/null +++ b/venusclient/common/utils.py @@ -0,0 +1,159 @@ +# 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. +# +import logging + +from oslo_serialization import jsonutils + +from venusclient import exceptions as exc + +LOG = logging.getLogger(__name__) + + +def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None): + """Generate common filters for any list request. + + :param marker: entity ID from which to start returning entities. + :param limit: maximum number of entities to return. + :param sort_key: field to use for sorting. + :param sort_dir: direction of sorting: 'asc' or 'desc'. + :returns: list of string filters. + """ + filters = [] + if isinstance(limit, int): + filters.append('filters.field=limit') + filters.append('filters.value=%d' % limit) + if marker is not None: + filters.append('filters.field=marker') + filters.append('filters.value=%s' % marker) + if sort_key is not None: + filters.append('filters.field=sort_key') + filters.append('filters.value=%s' % sort_key) + if sort_dir is not None: + filters.append('filters.field=sort_dir') + filters.append('filters.value=%s' % sort_dir) + return filters + + +def add_filters(filters, **kwargs): + if kwargs: + for field, value in kwargs.iteritems(): + filters.append('filters.field=%s' % field) + filters.append('filters.value=%s' % value) + return filters + + +def print_list_field(field): + return lambda obj: ', '.join(getattr(obj, field)) + + +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('Could not decode response body as JSON') + elif 'application/octet-stream' in content_type: + try: + body = resp.body() + except ValueError: + LOG.error('Could not decode response body as raw') + else: + body = None + return body + + +def addresses_formatter(network_client, networks): + output = [] + for (network, addresses) in networks.items(): + if not addresses: + continue + addrs = [addr['addr'] for addr in addresses] + network_data = network_client.find_network( + network, ignore_missing=False) + net_ident = network_data.name or network_data.id + addresses_csv = ', '.join(addrs) + group = "%s=%s" % (net_ident, addresses_csv) + output.append(group) + return '; '.join(output) + + +def image_formatter(image_client, image_id): + if image_id: + image = image_client.images.get(image_id) + return '%s (%s)' % (image.name, image_id) + return '' + + +def flavor_formatter(bc_client, flavor_id): + if flavor_id: + flavor = bc_client.flavor.get(flavor_id) + return '%s (%s)' % (flavor.name, flavor_id) + return '' + + +def clean_listing_columns(headers, columns, data_sample): + col_headers = [] + cols = [] + for header, col in zip(headers, columns): + if hasattr(data_sample, col): + col_headers.append(header) + cols.append(col) + return tuple(col_headers), tuple(cols) + + +def json_formatter(js): + return jsonutils.dumps(js, indent=2, ensure_ascii=False) + + +def split_and_deserialize(string): + """Split and try to JSON deserialize a string. + + Gets a string with the KEY=VALUE format, split it (using '=' as the + separator) and try to JSON deserialize the VALUE. + :returns: A tuple of (key, value). + """ + + try: + key, value = string.split("=", 1) + except ValueError: + raise exc.CommandError(_('Attributes must be a list of ' + 'PATH=VALUE not "%s"') % string) + try: + value = jsonutils.loads(value) + except ValueError: + pass + + return (key, value) + + +def args_array_to_patch(op, attributes): + patch = [] + for attr in attributes: + # Sanitize + if not attr.startswith('/'): + attr = '/' + attr + + if op in ['add', 'replace']: + path, value = split_and_deserialize(attr) + patch.append({'op': op, 'path': path, 'value': value}) + + elif op == "remove": + # For remove only the key is needed + patch.append({'op': op, 'path': attr}) + else: + raise exc.CommandError(_('Unknown PATCH operation: %s') % op) + return patch diff --git a/venusclient/exceptions.py b/venusclient/exceptions.py new file mode 100644 index 0000000..6703ece --- /dev/null +++ b/venusclient/exceptions.py @@ -0,0 +1,515 @@ +# 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. +# + +import inspect +import sys + +from oslo_serialization import jsonutils +import six + +from venusclient.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 InvalidAttribute(ClientException): + """Invalid attribute on API client side.""" + pass + + +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 vars(sys.modules[__name__]).items() + if inspect.isclass(obj) and getattr(obj, 'status_code', False) +) + + +def from_response(response, method, url, message=None, traceback=None): + """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 + """ + error_body = {} + if message: + error_body['message'] = message + if traceback: + error_body['details'] = traceback + + if hasattr(response, 'status') and not hasattr(response, 'status_code'): + # NOTE(akurilin): These modifications around response object give + # ability to get all necessary information in method `from_response` + # from common code, which expecting response object from `requests` + # library instead of object from `httplib/httplib2` library. + response.status_code = response.status + response.headers = { + 'Content-Type': response.getheader('content-type', "")} + + if hasattr(response, 'status_code'): + # NOTE(hongbin): This allows SessionClient to handle faultstring. + response.json = lambda: {'error': error_body} + + if (response.headers.get('Content-Type', '').startswith('text/') and + not hasattr(response, 'text')): + # NOTE(clif_h): There seems to be a case in the + # common.apiclient.exceptions module where if the + # content-type of the response is text/* then it expects + # the response to have a 'text' attribute, but that + # doesn't always seem to necessarily be the case. + # This is to work around that problem. + response.text = '' + + # NOTE(liusheng): for pecan's response, the request_id is + # "Openstack-Request-Id" + req_id = (response.headers.get("x-openstack-request-id") or + response.headers.get("Openstack-Request-Id")) + kwargs = { + "status_code": response.status_code, + "response": response, + "method": method, + "url": url, + "request_id": req_id, + } + if "retry-after" in response.headers: + kwargs["retry_after"] = response.headers["retry-after"] + + content_type = response.headers.get("Content-Type", "") + if content_type.startswith("application/json"): + try: + body = response.json() + except ValueError: + pass + else: + if hasattr(body, 'keys'): + # NOTE(RuiChen): WebOb<1.6.0 will return a nested dict + # structure where the error keys to the message/details/code. + # WebOb>=1.6.0 returns just a response body as a single dict, + # not nested, so we have to handle both cases (since we can't + # trust what we're given with content_type: application/json + # either way. + if 'message' in body: + # WebOb>=1.6.0 case + error = body + else: + # WebOb<1.6.0 where we assume there is a single error + # message key to the body that has the message and details. + error = body.get(list(body)[0]) + # NOTE(liusheng): the response.json() may like this: + # {u'error_message': u'{"debuginfo": null, "faultcode": + # "Client", "faultstring": "error message"}'}, the + # "error_message" in the body is also a json string. + if isinstance(error, six.string_types): + error = jsonutils.loads(error) + + if hasattr(error, 'keys'): + kwargs['message'] = (error.get('message') or + error.get('faultstring')) + kwargs['details'] = (error.get('details') or + six.text_type(body)) + elif content_type.startswith("text/"): + kwargs["details"] = getattr(response, 'text', '') + + try: + cls = _code_map[response.status_code] + except KeyError: + if 500 <= response.status_code < 600: + cls = HttpServerError + elif 400 <= response.status_code < 500: + cls = HTTPClientError + else: + cls = HttpError + + return cls(**kwargs) diff --git a/venusclient/i18n.py b/venusclient/i18n.py new file mode 100644 index 0000000..8160c9b --- /dev/null +++ b/venusclient/i18n.py @@ -0,0 +1,19 @@ +# 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. +# + +import oslo_i18n + +# The primary translation function using the well-known name "_" +_ = oslo_i18n.TranslatorFactory(domain='venusclient').primary diff --git a/venusclient/osc/__init__.py b/venusclient/osc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venusclient/osc/plugin.py b/venusclient/osc/plugin.py new file mode 100644 index 0000000..89ba0b1 --- /dev/null +++ b/venusclient/osc/plugin.py @@ -0,0 +1,115 @@ +# 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. +# + +"""OpenStackClient plugin for Accelerator management service.""" + +import logging + +from openstack.config import cloud_region +from openstack.config import defaults as config_defaults +from openstack import connection +from osc_lib import utils + +LOG = logging.getLogger(__name__) + +DEFAULT_ACCELERATOR_API_VERSION = '2' +API_VERSION_OPTION = 'os_accelerator_api_version' +API_NAME = 'accelerator' +CURRENT_API_VERSION = '2' + + +def _make_key(service_type, key): + if not service_type: + return key + else: + service_type = service_type.lower().replace('-', '_') + return "_".join([service_type, key]) + + +def _get_config_from_profile(profile, **kwargs): + # Deal with clients still trying to use legacy profile objects + region_name = None + for service in profile.get_services(): + if service.region: + region_name = service.region + service_type = service.service_type + if service.interface: + key = _make_key(service_type, 'interface') + kwargs[key] = service.interface + if service.version: + version = service.version + if version.startswith('v'): + version = version[2:] + key = _make_key(service_type, 'api_version') + kwargs[key] = version + if service.api_version: + version = service.api_version + key = _make_key(service_type, 'default_microversion') + kwargs[key] = version + + config_kwargs = config_defaults.get_defaults() + config_kwargs.update(kwargs) + config = cloud_region.CloudRegion( + region_name=region_name, config=config_kwargs) + return config + + +def create_connection(prof=None, cloud_region=None, **kwargs): + version_key = _make_key(API_NAME, 'api_version') + kwargs[version_key] = CURRENT_API_VERSION + + if not cloud_region: + if prof: + cloud_region = _get_config_from_profile(prof, **kwargs) + else: + # If we got the CloudRegion from python-openstackclient and it doesn't + # already have a default microversion set, set it here. + microversion_key = _make_key(API_NAME, 'default_microversion') + cloud_region.config.setdefault(microversion_key, CURRENT_API_VERSION) + + user_agent = kwargs.pop('user_agent', None) + app_name = kwargs.pop('app_name', None) + app_version = kwargs.pop('app_version', None) + if user_agent is not None and (not app_name and not app_version): + app_name, app_version = user_agent.split('/', 1) + + return connection.Connection( + config=cloud_region, + app_name=app_name, + app_version=app_version, **kwargs) + + +def make_client(instance): + """Returns a accelerator proxy""" + conn = create_connection( + cloud_region=instance._cli_options, + ) + + LOG.debug('Connection: %s', conn) + LOG.debug('Accelerator client initialized using OpenStackSDK: %s', + conn.accelerator) + return conn.accelerator + + +def build_option_parser(parser): + """Hook to add global options""" + parser.add_argument( + '--os-accelerator-api-version', + metavar='', + default=utils.env( + 'OS_ACCELERATOR_API_VERSION', + default=DEFAULT_ACCELERATOR_API_VERSION), + help='Accelerator API version, default=' + + DEFAULT_ACCELERATOR_API_VERSION + + ' (Env: OS_ACCELERATOR_API_VERSION)') + return parser diff --git a/venusclient/osc/v1/__init__.py b/venusclient/osc/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venusclient/shell.py b/venusclient/shell.py new file mode 100644 index 0000000..b93ae54 --- /dev/null +++ b/venusclient/shell.py @@ -0,0 +1,642 @@ +# Copyright 2014 +# The Cloudscaling Group, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +### +# This code is taken from python-novaclient. Goal is minimal modification. +### + +""" +Command-line interface to the OpenStack API. +""" + +from __future__ import print_function + +import argparse +import logging +import os +import sys + +import six +from oslo_utils import encodeutils +from oslo_utils import importutils +from oslo_utils import strutils + +profiler = importutils.try_import("osprofiler.profiler") + +HAS_KEYRING = False +all_errors = ValueError +try: + import keyring + HAS_KEYRING = True + try: + if isinstance(keyring.get_keyring(), keyring.backend.GnomeKeyring): + import gnomekeyring + all_errors = (ValueError, + gnomekeyring.IOError, + gnomekeyring.NoKeyringDaemonError) + except Exception: + pass +except ImportError: + pass + +from venusclient.common import cliutils +from venusclient import exceptions as exc +from venusclient.i18n import _ +from venusclient.v1 import client as client_v1 +from venusclient.v1 import shell as shell_v1 +from venusclient import version + +LATEST_API_VERSION = ('1', 'latest') +DEFAULT_INTERFACE = 'public' +DEFAULT_SERVICE_TYPE = 'log-system' + +logger = logging.getLogger(__name__) + + +def positive_non_zero_float(text): + if text is None: + return None + try: + value = float(text) + except ValueError: + msg = "%s must be a float" % text + raise argparse.ArgumentTypeError(msg) + if value <= 0: + msg = "%s must be greater than 0" % text + raise argparse.ArgumentTypeError(msg) + return value + + +class VenusClientArgumentParser(argparse.ArgumentParser): + + def __init__(self, *args, **kwargs): + super(VenusClientArgumentParser, self).__init__(*args, **kwargs) + + def error(self, message): + """error(message: string) + + Prints a usage message incorporating the message to stderr and + exits. + """ + self.print_usage(sys.stderr) + # FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value + choose_from = ' (choose from' + progparts = self.prog.partition(' ') + self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'" + " for more information.\n" % + {'errmsg': message.split(choose_from)[0], + 'mainp': progparts[0], + 'subp': progparts[2]}) + + +class OpenStackVenusShell(object): + + def get_base_parser(self): + parser = VenusClientArgumentParser( + prog='venus', + description=__doc__.strip(), + epilog='See "venus help COMMAND" ' + 'for help on a specific command.', + add_help=False, + formatter_class=OpenStackHelpFormatter, + ) + + # Global arguments + parser.add_argument('-h', '--help', + action='store_true', + help=argparse.SUPPRESS) + + parser.add_argument('--version', + action='version', + version=version.version_info.version_string()) + + parser.add_argument('--debug', + default=False, + action='store_true', + help=_("Print debugging output.")) + + parser.add_argument('--os-cache', + default=strutils.bool_from_string( + cliutils.env('OS_CACHE', default=False)), + action='store_true', + help=_("Use the auth token cache. Defaults to " + "False if env[OS_CACHE] is not set.")) + + parser.add_argument('--os-region-name', + metavar='', + default=os.environ.get('OS_REGION_NAME'), + help=_('Region name. ' + 'Default=env[OS_REGION_NAME].')) + + + + parser.add_argument('--os-auth-url', + metavar='', + default=cliutils.env('OS_AUTH_URL', default=None), + help=_('Defaults to env[OS_AUTH_URL].')) + + parser.add_argument('--os-user-id', + metavar='', + default=cliutils.env('OS_USER_ID', default=None), + help=_('Defaults to env[OS_USER_ID].')) + + parser.add_argument('--os-username', + metavar='', + default=cliutils.env('OS_USERNAME', default=None), + help=_('Defaults to env[OS_USERNAME].')) + + parser.add_argument('--os-user-domain-id', + metavar='', + default=cliutils.env('OS_USER_DOMAIN_ID', + default=None), + help=_('Defaults to env[OS_USER_DOMAIN_ID].')) + + parser.add_argument('--os-user-domain-name', + metavar='', + default=cliutils.env('OS_USER_DOMAIN_NAME', + default=None), + help=_('Defaults to env[OS_USER_DOMAIN_NAME].')) + + parser.add_argument('--os-project-id', + metavar='', + default=cliutils.env('OS_PROJECT_ID', + default=None), + help=_('Defaults to env[OS_PROJECT_ID].')) + + parser.add_argument('--os-project-name', + metavar='', + default=cliutils.env('OS_PROJECT_NAME', + default=None), + help=_('Defaults to env[OS_PROJECT_NAME].')) + + parser.add_argument('--os-tenant-id', + metavar='', + default=cliutils.env('OS_TENANT_ID', + default=None), + help=argparse.SUPPRESS) + + parser.add_argument('--os-tenant-name', + metavar='', + default=cliutils.env('OS_TENANT_NAME', + default=None), + help=argparse.SUPPRESS) + + parser.add_argument('--os-project-domain-id', + metavar='', + default=cliutils.env('OS_PROJECT_DOMAIN_ID', + default=None), + help=_('Defaults to env[OS_PROJECT_DOMAIN_ID].')) + + parser.add_argument('--os-project-domain-name', + metavar='', + default=cliutils.env('OS_PROJECT_DOMAIN_NAME', + default=None), + help=_('Defaults to ' + 'env[OS_PROJECT_DOMAIN_NAME].')) + + parser.add_argument('--os-token', + metavar='', + default=cliutils.env('OS_TOKEN', default=None), + help=_('Defaults to env[OS_TOKEN].')) + + parser.add_argument('--os-password', + metavar='', + default=cliutils.env('OS_PASSWORD', + default=None), + help=_('Defaults to env[OS_PASSWORD].')) + + parser.add_argument('--service-type', + metavar='', + help=_('Defaults to accelerator for all ' + 'actions.')) + parser.add_argument('--service_type', + help=argparse.SUPPRESS) + + parser.add_argument('--endpoint-type', + metavar='', + default=cliutils.env('OS_ENDPOINT_TYPE', + default=None), + help=argparse.SUPPRESS) + + parser.add_argument('--os-endpoint-type', + metavar='', + default=cliutils.env('OS_ENDPOINT_TYPE', + default=None), + help=_('Defaults to env[OS_ENDPOINT_TYPE]')) + + parser.add_argument('--os-interface', + metavar='', + default=cliutils.env( + 'OS_INTERFACE', + default=DEFAULT_INTERFACE), + help=argparse.SUPPRESS) + + parser.add_argument('--os-cloud', + metavar='', + default=cliutils.env('OS_CLOUD', default=None), + help=_('Defaults to env[OS_CLOUD].')) + + # NOTE(dtroyer): We can't add --endpoint_type here due to argparse + # thinking usage-list --end is ambiguous; but it + # works fine with only --endpoint-type present + # Go figure. I'm leaving this here for doc purposes. + # parser.add_argument('--endpoint_type', + # help=argparse.SUPPRESS) + + parser.add_argument('--venus-api-version', + metavar='', + default=cliutils.env( + 'VENUS_API_VERSION', + default='latest'), + help=_('Accepts "api", ' + 'defaults to env[VENUS_API_VERSION].')) + parser.add_argument('--venus_api_version', + help=argparse.SUPPRESS) + + parser.add_argument('--os-cacert', + metavar='', + default=cliutils.env('OS_CACERT', default=None), + help=_('Specify a CA bundle file to use in ' + 'verifying a TLS (https) server ' + 'certificate. Defaults to env[OS_CACERT].')) + + parser.add_argument('--os-endpoint-override', + metavar='', + default=cliutils.env('OS_ENDPOINT_OVERRIDE', + default=None), + help=_("Use this API endpoint instead of the " + "Service Catalog.")) + parser.add_argument('--bypass-url', + metavar='', + default=cliutils.env('BYPASS_URL', default=None), + dest='bypass_url', + help=argparse.SUPPRESS) + parser.add_argument('--bypass_url', + help=argparse.SUPPRESS) + + parser.add_argument('--insecure', + default=cliutils.env('VENUSCLIENT_INSECURE', + default=False), + action='store_true', + help=_("Do not verify https connections")) + + if profiler: + parser.add_argument('--profile', + metavar='HMAC_KEY', + default=cliutils.env('OS_PROFILE', + default=None), + help='HMAC key to use for encrypting context ' + 'data for performance profiling of operation. ' + 'This key should be the value of the HMAC key ' + 'configured for the OSprofiler middleware in ' + 'venus; it is specified in the venus ' + 'configuration file at ' + '"/etc/venus/magnum.conf". ' + 'Without the key, profiling will not be ' + 'triggered even if OSprofiler is enabled on ' + 'the server side.') + + return parser + + def get_subcommand_parser(self, version): + parser = self.get_base_parser() + + self.subcommands = {} + subparsers = parser.add_subparsers(metavar='') + + try: + actions_modules = { + '1': shell_v1.COMMAND_MODULES + }[version] + except KeyError: + actions_modules = shell_v1.COMMAND_MODULES + + for actions_module in actions_modules: + self._find_actions(subparsers, actions_module) + self._find_actions(subparsers, self) + + # self._add_bash_completion_subparser(subparsers) + + return parser + + def _add_bash_completion_subparser(self, subparsers): + subparser = ( + subparsers.add_parser('bash_completion', + add_help=False, + formatter_class=OpenStackHelpFormatter) + ) + self.subcommands['bash_completion'] = subparser + subparser.set_defaults(func=self.do_bash_completion) + + def _find_actions(self, subparsers, actions_module): + # print(actions_module) + # for a in dir(actions_module): + # print(a) + + for attr in (a for a in dir(actions_module) if a.startswith('do_')): + # I prefer to be hyphen-separated instead of underscores. + command = attr[3:].replace('_', '-') + callback = getattr(actions_module, attr) + desc = callback.__doc__ or '' + action_help = desc.strip() + arguments = getattr(callback, 'arguments', []) + group_args = getattr(callback, 'deprecated_groups', []) + + subparser = ( + subparsers.add_parser(command, + help=action_help, + description=desc, + add_help=False, + formatter_class=OpenStackHelpFormatter) + ) + subparser.add_argument('-h', '--help', + action='help', + help=argparse.SUPPRESS,) + self.subcommands[command] = subparser + + for (old_info, new_info, req) in group_args: + group = subparser.add_mutually_exclusive_group(required=req) + group.add_argument(*old_info[0], **old_info[1]) + group.add_argument(*new_info[0], **new_info[1]) + + for (args, kwargs) in arguments: + subparser.add_argument(*args, **kwargs) + subparser.set_defaults(func=callback) + + def setup_debugging(self, debug): + if debug: + streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s" + # Set up the root logger to debug so that the submodules can + # print debug messages + logging.basicConfig(level=logging.DEBUG, + format=streamformat) + else: + streamformat = "%(levelname)s %(message)s" + logging.basicConfig(level=logging.CRITICAL, + format=streamformat) + + def _check_version(self, api_version): + if api_version == 'latest': + return LATEST_API_VERSION + else: + try: + versions = tuple(int(i) for i in api_version.split('.')) + except ValueError: + versions = () + if len(versions) == 1: + # Default value of venus_api_version is '1'. + # If user not specify the value of api version, not passing + # headers at all. + venus_api_version = None + elif len(versions) == 2: + venus_api_version = api_version + # In the case of '1.0' + if versions[1] == 0: + venus_api_version = None + else: + msg = _("The requested API version %(ver)s is an unexpected " + "format. Acceptable formats are 'X', 'X.Y', or the " + "literal string '%(latest)s'." + ) % {'ver': api_version, 'latest': 'latest'} + raise exc.CommandError(msg) + + api_major_version = versions[0] + return (api_major_version, venus_api_version) + + def _ensure_auth_info(self, args): + if not cliutils.isunauthenticated(args.func): + if (not (args.os_token and + (args.os_auth_url or args.os_endpoint_override)) and + not args.os_cloud): + + if not (args.os_username or args.os_user_id): + raise exc.CommandError( + "You must provide a username via either --os-username " + "or via env[OS_USERNAME]" + ) + if not args.os_password: + raise exc.CommandError( + "You must provide a password via either " + "--os-password, env[OS_PASSWORD], or prompted " + "response" + ) + if (not args.os_project_name and not args.os_project_id): + raise exc.CommandError( + "You must provide a project name or project id via " + "--os-project-name, --os-project-id, " + "env[OS_PROJECT_NAME] or env[OS_PROJECT_ID]" + ) + if not args.os_auth_url: + raise exc.CommandError( + "You must provide an auth url via either " + "--os-auth-url or via env[OS_AUTH_URL]" + ) + + def main(self, argv): + + # NOTE(Christoph Jansen): With Python 3.4 argv somehow becomes a Map. + # This hack fixes it. + argv = list(argv) + + # Parse args once to find version and debug settings + parser = self.get_base_parser() + (options, args) = parser.parse_known_args(argv) + self.setup_debugging(options.debug) + + # NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse + # thinking usage-list --end is ambiguous; but it + # works fine with only --endpoint-type present + # Go figure. + if '--endpoint_type' in argv: + spot = argv.index('--endpoint_type') + argv[spot] = '--endpoint-type' + + # build available subcommands based on version + (api_major_version, venus_api_version) = ( + self._check_version(options.venus_api_version)) + + subcommand_parser = ( + self.get_subcommand_parser(api_major_version) + ) + self.parser = subcommand_parser + + if options.help or not argv: + subcommand_parser.print_help() + return 0 + + args = subcommand_parser.parse_args(argv) + + # Short-circuit and deal with help right away. + # NOTE(jamespage): args.func is not guaranteed with python >= 3.4 + if not hasattr(args, 'func') or args.func == self.do_help: + self.do_help(args) + return 0 + elif args.func == self.do_bash_completion: + self.do_bash_completion(args) + return 0 + + if not args.service_type: + args.service_type = DEFAULT_SERVICE_TYPE + + if args.bypass_url: + args.os_endpoint_override = args.bypass_url + + args.os_project_id = (args.os_project_id or args.os_tenant_id) + args.os_project_name = (args.os_project_name or args.os_tenant_name) + + #self._ensure_auth_info(args) + + try: + client = { + '1': client_v1, + }[api_major_version] + except KeyError: + client = client_v1 + + args.os_endpoint_type = (args.os_endpoint_type or args.endpoint_type) + if args.os_endpoint_type: + args.os_interface = args.os_endpoint_type + + if args.os_interface.endswith('URL'): + args.os_interface = args.os_interface[:-3] + + kwargs = {} + if profiler: + kwargs["profile"] = args.profile + + self.cs = client.Client( + cloud=args.os_cloud, + user_id=args.os_user_id, + username=args.os_username, + password=args.os_password, + auth_token=args.os_token, + project_id=args.os_project_id, + project_name=args.os_project_name, + user_domain_id=args.os_user_domain_id, + user_domain_name=args.os_user_domain_name, + project_domain_id=args.os_project_domain_id, + project_domain_name=args.os_project_domain_name, + auth_url=args.os_auth_url, + service_type=args.service_type, + region_name=args.os_region_name, + venus_url=args.os_endpoint_override, + interface=args.os_interface, + insecure=args.insecure, + api_version=args.venus_api_version, + **kwargs + ) + + self._check_deprecation(args.func, argv) + try: + args.func(self.cs, args) + except (cliutils.DuplicateArgs, cliutils.MissingArgs): + self.do_help(args) + raise + + if profiler and args.profile: + trace_id = profiler.get().get_base_id() + print("To display trace use the command:\n\n" + " osprofiler trace show --html %s " % trace_id) + + def _check_deprecation(self, func, argv): + if not hasattr(func, 'deprecated_groups'): + return + + for (old_info, new_info, required) in func.deprecated_groups: + old_param = old_info[0][0] + new_param = new_info[0][0] + old_value, new_value = None, None + for i in range(len(argv)): + cur_arg = argv[i] + if cur_arg == old_param: + old_value = argv[i + 1] + elif cur_arg == new_param[0]: + new_value = argv[i + 1] + + if old_value and not new_value: + print( + 'WARNING: The %s parameter is deprecated and will be ' + 'removed in a future release. Use the %s parameter to ' + 'avoid seeing this message.' + % (old_param, new_param)) + + def _dump_timings(self, timings): + class Tyme(object): + def __init__(self, url, seconds): + self.url = url + self.seconds = seconds + results = [Tyme(url, end - start) for url, start, end in timings] + total = 0.0 + for tyme in results: + total += tyme.seconds + results.append(Tyme("Total", total)) + cliutils.print_list(results, ["url", "seconds"], sortby_index=None) + + def do_bash_completion(self, _args): + """Prints arguments for bash-completion. + + Prints all of the commands and options to stdout so that the + venus.bash_completion script doesn't have to hard code them. + """ + commands = set() + options = set() + for sc_str, sc in self.subcommands.items(): + commands.add(sc_str) + for option in sc._optionals._option_string_actions.keys(): + options.add(option) + + commands.remove('bash-completion') + commands.remove('bash_completion') + print(' '.join(commands | options)) + + @cliutils.arg('command', metavar='', nargs='?', + help=_('Display help for .')) + def do_help(self, args): + """Display help about this program or one of its subcommands.""" + # NOTE(jamespage): args.command is not guaranteed with python >= 3.4 + command = getattr(args, 'command', '') + + if command: + if args.command in self.subcommands: + self.subcommands[args.command].print_help() + else: + raise exc.CommandError("'%s' is not a valid subcommand" % + args.command) + else: + self.parser.print_help() + + +# I'm picky about my shell help. +class OpenStackHelpFormatter(argparse.HelpFormatter): + def start_section(self, heading): + # Title-case the headings + heading = '%s%s' % (heading[0].upper(), heading[1:]) + super(OpenStackHelpFormatter, self).start_section(heading) + + +def main(): + try: + OpenStackVenusShell().main(map(encodeutils.safe_decode, sys.argv[1:])) + + except Exception as e: + logger.debug(e, exc_info=1) + print("ERROR: %s" % encodeutils.safe_encode(six.text_type(e)), + file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/venusclient/v1/__init__.py b/venusclient/v1/__init__.py new file mode 100644 index 0000000..1c6136d --- /dev/null +++ b/venusclient/v1/__init__.py @@ -0,0 +1,19 @@ +# -*- 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 pbr.version + + +__version__ = pbr.version.VersionInfo( + 'python-venusclient').version_string() diff --git a/venusclient/v1/basemodels.py b/venusclient/v1/basemodels.py new file mode 100644 index 0000000..de669fa --- /dev/null +++ b/venusclient/v1/basemodels.py @@ -0,0 +1,115 @@ +# 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 venusclient.common import base +from venusclient.common import utils +from venusclient import exceptions + + +CREATION_ATTRIBUTES = [] + +OUTPUT_ATTRIBUTES = CREATION_ATTRIBUTES + ['apiserver_port', 'created_at', + 'insecure_registry', 'links', + 'updated_at', 'cluster_distro', + 'uuid'] + + +class BaseModel(base.Resource): + # model_name needs to be overridden by any derived class. + # model_name should be capitalized and singular, e.g. "Cluster" + model_name = '' + + def __repr__(self): + return "<" + self.__class__.model_name + "%s>" % self._info + + +class BaseModelManager(base.Manager): + # base_url needs to be overridden by any derived class. + # base_url should be pluralized and lowercase, e.g. "clustertemplates", as + # it shows up in the URL path: "/v2/{base_url}" + api_name = '' + base_url = '' + + @classmethod + def _path(cls, id=None, filters=None): + if filters: + return '/v1/' + cls.base_url + filters + return '/v1/' + cls.base_url + \ + '/%s' % id if id else '/v1/' + cls.base_url + + def list(self, limit=None, marker=None, sort_key=None, + sort_dir=None, detail=False, **add_filters): + """Retrieve a list of accelerators. + + :param marker: Optional, the UUID of a baymodel, eg the last + baymodel from a previous result set. Return + the next result set. + :param limit: The maximum number of results to return per + request, if: + + 1) limit > 0, the maximum number of accelerators to return. + 2) limit == 0, return the entire list of accelerators. + 3) limit param is NOT specified (None), the number of items + returned respect the maximum imposed by the venus API + (see venus's api.max_limit option). + + :param sort_key: Optional, field used for sorting. + + :param sort_dir: Optional, direction of sorting, either 'asc' (the + default) or 'desc'. + + :param detail: Optional, boolean whether to return detailed information + about accelerators. + + :returns: A list of accelerators. + + """ + if limit is not None: + limit = int(limit) + + filters = utils.common_filters(marker, limit, sort_key, sort_dir) + utils.add_filters(filters, **add_filters) + + path = '' + if detail: + path += 'detail' + if filters: + path += '?' + '&'.join(filters) + if limit is None: + return self._list(self._path(filters=path), + self.__class__.api_name) + else: + return self._list_pagination(self._path(filters=path), + self.__class__.api_name, + limit=limit) + + def get(self, id): + try: + return self._list(self._path(id))[0] + except IndexError: + return None + + def create(self, **kwargs): + new = {} + for (key, value) in kwargs.items(): + if key in CREATION_ATTRIBUTES: + new[key] = value + else: + raise exceptions.InvalidAttribute( + "Key must be in %s" % ",".join(CREATION_ATTRIBUTES)) + return self._create(self._path(), new) + + def delete(self, id): + return self._delete(self._path(id)) + + def update(self, id, patch): + return self._update(self._path(id), patch) diff --git a/venusclient/v1/client.py b/venusclient/v1/client.py new file mode 100644 index 0000000..18ddab3 --- /dev/null +++ b/venusclient/v1/client.py @@ -0,0 +1,192 @@ +# Copyright 2014 +# The Cloudscaling Group, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os_client_config +from keystoneauth1 import session as ksa_session +from oslo_utils import importutils +from venusclient.common import httpclient +from venusclient.v1 import config + + +profiler = importutils.try_import("osprofiler.profiler") + + +DEFAULT_SERVICE_TYPE = 'compute' + + +def _load_session(cloud=None, insecure=False, timeout=None, **kwargs): + cloud_config = os_client_config.OpenStackConfig() + cloud_config = cloud_config.get_one_cloud( + cloud=cloud, + verify=not insecure, + **kwargs) + verify, cert = cloud_config.get_requests_verify_args() + + auth = cloud_config.get_auth() + session = ksa_session.Session( + auth=auth, verify=verify, cert=cert, + timeout=timeout) + + return session + + +def _load_service_type(session, + service_type=None, service_name=None, + interface=None, region_name=None, **kwargs): + try: + # Trigger an auth error so that we can throw the exception + # we always have + aaa = session.get_endpoint( + service_type=service_type, + service_name=service_name, + interface=interface, + region_name=region_name, + **kwargs) + except Exception as e: + raise RuntimeError(str(e)) + + return service_type + + +def _load_session_client(session=None, endpoint_override=None, username=None, + project_id=None, project_name=None, + auth_url=None, password=None, auth_type=None, + insecure=None, user_domain_id=None, + user_domain_name=None, project_domain_id=None, + project_domain_name=None, auth_token=None, + timeout=None, service_type=None, service_name=None, + interface=None, region_name=None, api_version=None, + **kwargs): + if not session: + session = _load_session( + username=username, + project_id=project_id, + project_name=project_name, + auth_url=auth_url, + password=password, + auth_type=auth_type, + insecure=insecure, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name, + auth_token=auth_token, + timeout=timeout, + **kwargs + ) + + if not endpoint_override: + service_type = _load_service_type( + session, + service_type=service_type, + service_name=service_name, + interface=interface, + region_name=region_name, + **kwargs + ) + + return httpclient.SessionClient( + service_type=service_type, + service_name=service_name, + interface=interface, + region_name=region_name, + session=session, + endpoint_override=endpoint_override, + api_version=api_version, + ) + + +class Client(object): + def __init__(self, username=None, api_key=None, project_id=None, + project_name=None, auth_url=None, venus_url=None, + endpoint_type=None, endpoint_override=None, + service_type=DEFAULT_SERVICE_TYPE, + region_name=None, input_auth_token=None, + session=None, password=None, auth_type='password', + interface=None, service_name=None, insecure=False, + user_domain_id=None, user_domain_name=None, + project_domain_id=None, project_domain_name=None, + auth_token=None, timeout=600, api_version=None, + **kwargs): + + # We have to keep the api_key are for backwards compat, but let's + # remove it from the rest of our code since it's not a keystone + # concept + if not password: + password = api_key + # Backwards compat for people passing in input_auth_token + if input_auth_token: + auth_token = input_auth_token + # Backwards compat for people passing in endpoint_type + if endpoint_type: + interface = endpoint_type + + # osc sometimes give 'None' value + if not interface: + interface = 'public' + + if interface.endswith('URL'): + interface = interface[:-3] + + # fix (yolanda): os-cloud-config is using endpoint_override + # instead of venus_url + if venus_url and not endpoint_override: + endpoint_override = venus_url + + if endpoint_override and auth_token: + self.http_client = httpclient.HTTPClient( + endpoint_override, + token=auth_token, + api_version=api_version, + timeout=timeout, + insecure=insecure, + **kwargs + ) + else: + self.http_client = _load_session_client( + session=session, + endpoint_override=endpoint_override, + username=username, + project_id=project_id, + project_name=project_name, + auth_url=auth_url, + password=password, + auth_type=auth_type, + insecure=insecure, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name, + auth_token=auth_token, + timeout=timeout, + service_type=service_type, + service_name=service_name, + interface=interface, + region_name=region_name, + api_version=api_version, + **kwargs + ) + + self.config = config.ConfigManager(self.http_client) + + profile = kwargs.pop("profile", None) + if profiler and profile: + # Initialize the root of the future trace: the created trace ID + # will be used as the very first parent to which all related + # traces will be bound to. The given HMAC key must correspond to + # the one set in venus-api venus.conf, otherwise the latter + # will fail to check the request signature and will skip + # initialization of osprofiler on the server side. + profiler.init(profile) diff --git a/venusclient/v1/config.py b/venusclient/v1/config.py new file mode 100644 index 0000000..1cedadd --- /dev/null +++ b/venusclient/v1/config.py @@ -0,0 +1,36 @@ +# Copyright (c) 2018 Intel, 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 venusclient.v1 import basemodels + + +CREATION_ATTRIBUTES = basemodels.CREATION_ATTRIBUTES + + +class LogConfig(basemodels.BaseModel): + model_name = "Configs" + + +class ConfigManager(basemodels.BaseModelManager): + api_name = "configs" + base_url = "configs" + resource_class = LogConfig + + def get_days(self): + url = '/v1/custom_config' + try: + resp, body = self.api.json_request('GET', url) + except: + print("") + diff --git a/venusclient/v1/config_shell.py b/venusclient/v1/config_shell.py new file mode 100644 index 0000000..1b0463a --- /dev/null +++ b/venusclient/v1/config_shell.py @@ -0,0 +1,24 @@ +# Copyright (c) 2018 Intel, 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 venusclient.common import cliutils as utils +from venusclient.i18n import _ + + + +def do_get_log_storage_days(cs,args): + """get the elasticsearch days of svae the logs.""" + endpoint = cs.config.get_days() + + diff --git a/venusclient/v1/shell.py b/venusclient/v1/shell.py new file mode 100644 index 0000000..24a54dc --- /dev/null +++ b/venusclient/v1/shell.py @@ -0,0 +1,20 @@ +# Copyright 2014 +# The Cloudscaling Group, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from venusclient.v1 import config_shell + +COMMAND_MODULES = [ + config_shell +] diff --git a/venusclient/version.py b/venusclient/version.py new file mode 100644 index 0000000..b4ae30e --- /dev/null +++ b/venusclient/version.py @@ -0,0 +1,17 @@ +# Copyright (c) 2018 Intel, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pbr import version + +version_info = version.VersionInfo('python-venusclient')