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')