first commit
Change-Id: Id880cbc92ee4de7da5e63044d0c68138d7ecbf64
This commit is contained in:
parent
d7fed6abd1
commit
b59b288ac8
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
.coverage
|
||||
.venv
|
||||
.testrepository
|
||||
subunit.log
|
||||
.tox
|
||||
*,cover
|
||||
cover
|
||||
*.pyc
|
||||
.idea
|
||||
*.sw?
|
||||
*~
|
||||
build
|
||||
dist
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
*.egg
|
||||
*egg-info
|
||||
|
||||
# Files created by releasenotes build
|
||||
releasenotes/build
|
||||
|
17
CONTRIBUTING.rst
Normal file
17
CONTRIBUTING.rst
Normal file
@ -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/iotronic
|
176
LICENSE
Normal file
176
LICENSE
Normal file
@ -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.
|
||||
|
19
README.rst
Normal file
19
README.rst
Normal file
@ -0,0 +1,19 @@
|
||||
===============================
|
||||
replace with the name for the git repo
|
||||
===============================
|
||||
|
||||
Iotronic Client
|
||||
|
||||
Please fill here a long description which must be at least 3 lines wrapped on
|
||||
80 cols, so that distribution package maintainers can use it in their packages.
|
||||
Note that this is a hard requirement.
|
||||
|
||||
* Free software: Apache license
|
||||
* Documentation: http://docs.openstack.org/developer/replace with the name for the git repo
|
||||
* Source: http://git.openstack.org/cgit/openstack/replace with the name for the git repo
|
||||
* Bugs: http://bugs.launchpad.net/iotronic
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* TODO
|
75
doc/source/conf.py
Executable file
75
doc/source/conf.py
Executable file
@ -0,0 +1,75 @@
|
||||
# -*- 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',
|
||||
#'sphinx.ext.intersphinx',
|
||||
'oslosphinx'
|
||||
]
|
||||
|
||||
# autodoc generation is a bit aggressive and a nuisance when doing heavy
|
||||
# text edit cycles.
|
||||
# execute "export SPHINX_DEBUG=1" in your terminal to disable
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'replace with the name for the git repo'
|
||||
copyright = u'2016, OpenStack Foundation'
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
add_module_names = True
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||
# html_theme_path = ["."]
|
||||
# html_theme = '_theme'
|
||||
# html_static_path = ['static']
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = '%sdoc' % project
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass
|
||||
# [howto/manual]).
|
||||
latex_documents = [
|
||||
('index',
|
||||
'%s.tex' % project,
|
||||
u'%s Documentation' % project,
|
||||
u'OpenStack Foundation', 'manual'),
|
||||
]
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
#intersphinx_mapping = {'http://docs.python.org/': None}
|
4
doc/source/contributing.rst
Normal file
4
doc/source/contributing.rst
Normal file
@ -0,0 +1,4 @@
|
||||
============
|
||||
Contributing
|
||||
============
|
||||
.. include:: ../../CONTRIBUTING.rst
|
25
doc/source/index.rst
Normal file
25
doc/source/index.rst
Normal file
@ -0,0 +1,25 @@
|
||||
.. replace with the name for the git repo 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 replace with the name for the git repo's documentation!
|
||||
========================================================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
readme
|
||||
installation
|
||||
usage
|
||||
contributing
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
12
doc/source/installation.rst
Normal file
12
doc/source/installation.rst
Normal file
@ -0,0 +1,12 @@
|
||||
============
|
||||
Installation
|
||||
============
|
||||
|
||||
At the command line::
|
||||
|
||||
$ pip install replace with the name for the git repo
|
||||
|
||||
Or, if you have virtualenvwrapper installed::
|
||||
|
||||
$ mkvirtualenv replace with the name for the git repo
|
||||
$ pip install replace with the name for the git repo
|
1
doc/source/readme.rst
Normal file
1
doc/source/readme.rst
Normal file
@ -0,0 +1 @@
|
||||
.. include:: ../../README.rst
|
7
doc/source/usage.rst
Normal file
7
doc/source/usage.rst
Normal file
@ -0,0 +1,7 @@
|
||||
========
|
||||
Usage
|
||||
========
|
||||
|
||||
To use replace with the name for the git repo in a project::
|
||||
|
||||
import iotronicclient
|
27
iotronicclient/__init__.py
Normal file
27
iotronicclient/__init__.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import pbr.version
|
||||
|
||||
from iotronicclient import client
|
||||
from iotronicclient import exc as exceptions
|
||||
|
||||
__version__ = pbr.version.VersionInfo('python-iotronicclient').version_string()
|
||||
|
||||
__all__ = (
|
||||
'client',
|
||||
'exc',
|
||||
'exceptions',
|
||||
)
|
153
iotronicclient/client.py
Normal file
153
iotronicclient/client.py
Normal file
@ -0,0 +1,153 @@
|
||||
# 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 keystoneauth1 import loading as kaloading
|
||||
from oslo_utils import importutils
|
||||
|
||||
from iotronicclient.common.i18n import _
|
||||
from iotronicclient import exc
|
||||
|
||||
|
||||
def get_client(api_version, os_auth_token=None, iotronic_url=None,
|
||||
os_username=None, os_password=None, os_auth_url=None,
|
||||
os_project_id=None, os_project_name=None, os_tenant_id=None,
|
||||
os_tenant_name=None, os_region_name=None,
|
||||
os_user_domain_id=None, os_user_domain_name=None,
|
||||
os_project_domain_id=None, os_project_domain_name=None,
|
||||
os_service_type=None, os_endpoint_type=None,
|
||||
insecure=None, timeout=None, os_cacert=None, ca_file=None,
|
||||
os_cert=None, cert_file=None, os_key=None, key_file=None,
|
||||
os_iotronic_api_version=None, max_retries=None,
|
||||
retry_interval=None, session=None, **ignored_kwargs):
|
||||
"""Get an authenticated client, based on the credentials.
|
||||
|
||||
:param api_version: the API version to use. Valid value: '1'.
|
||||
:param os_auth_token: pre-existing token to re-use
|
||||
:param iotronic_url: iotronic API endpoint
|
||||
:param os_username: name of a user
|
||||
:param os_password: user's password
|
||||
:param os_auth_url: endpoint to authenticate against
|
||||
:param os_tenant_name: name of a tenant (deprecated in favour of
|
||||
os_project_name)
|
||||
:param os_tenant_id: ID of a tenant (deprecated in favour of
|
||||
os_project_id)
|
||||
:param os_project_name: name of a project
|
||||
:param os_project_id: ID of a project
|
||||
:param os_region_name: name of a keystone region
|
||||
:param os_user_domain_name: name of a domain the user belongs to
|
||||
:param os_user_domain_id: ID of a domain the user belongs to
|
||||
:param os_project_domain_name: name of a domain the project belongs to
|
||||
:param os_project_domain_id: ID of a domain the project belongs to
|
||||
:param os_service_type: the type of service to lookup the endpoint for
|
||||
:param os_endpoint_type: the type (exposure) of the endpoint
|
||||
:param insecure: allow insecure SSL (no cert verification)
|
||||
:param timeout: allows customization of the timeout for client HTTP
|
||||
requests
|
||||
:param os_cacert: path to cacert file
|
||||
:param ca_file: path to cacert file, deprecated in favour of os_cacert
|
||||
:param os_cert: path to cert file
|
||||
:param cert_file: path to cert file, deprecated in favour of os_cert
|
||||
:param os_key: path to key file
|
||||
:param key_file: path to key file, deprecated in favour of os_key
|
||||
:param os_iotronic_api_version: iotronic API version to use
|
||||
:param max_retries: Maximum number of retries in case of conflict error
|
||||
:param retry_interval: Amount of time (in seconds) between retries in case
|
||||
of conflict error
|
||||
:param session: Keystone session to use
|
||||
:param ignored_kwargs: all the other params that are passed. Left for
|
||||
backwards compatibility. They are ignored.
|
||||
"""
|
||||
os_service_type = os_service_type or 'iot'
|
||||
os_endpoint_type = os_endpoint_type or 'publicURL'
|
||||
project_id = (os_project_id or os_tenant_id)
|
||||
project_name = (os_project_name or os_tenant_name)
|
||||
kwargs = {
|
||||
'os_iotronic_api_version': os_iotronic_api_version,
|
||||
'max_retries': max_retries,
|
||||
'retry_interval': retry_interval,
|
||||
}
|
||||
endpoint = iotronic_url
|
||||
cacert = os_cacert or ca_file
|
||||
cert = os_cert or cert_file
|
||||
key = os_key or key_file
|
||||
if os_auth_token and endpoint:
|
||||
kwargs.update({
|
||||
'token': os_auth_token,
|
||||
'insecure': insecure,
|
||||
'ca_file': cacert,
|
||||
'cert_file': cert,
|
||||
'key_file': key,
|
||||
'timeout': timeout,
|
||||
})
|
||||
elif os_auth_url:
|
||||
auth_type = 'password'
|
||||
auth_kwargs = {
|
||||
'auth_url': os_auth_url,
|
||||
'project_id': project_id,
|
||||
'project_name': project_name,
|
||||
'user_domain_id': os_user_domain_id,
|
||||
'user_domain_name': os_user_domain_name,
|
||||
'project_domain_id': os_project_domain_id,
|
||||
'project_domain_name': os_project_domain_name,
|
||||
}
|
||||
if os_username and os_password:
|
||||
auth_kwargs.update({
|
||||
'username': os_username,
|
||||
'password': os_password,
|
||||
})
|
||||
elif os_auth_token:
|
||||
auth_type = 'token'
|
||||
auth_kwargs.update({
|
||||
'token': os_auth_token,
|
||||
})
|
||||
# Create new session only if it was not passed in
|
||||
if not session:
|
||||
loader = kaloading.get_plugin_loader(auth_type)
|
||||
auth_plugin = loader.load_from_options(**auth_kwargs)
|
||||
# Let keystoneauth do the necessary parameter conversions
|
||||
session = kaloading.session.Session().load_from_options(
|
||||
auth=auth_plugin, insecure=insecure, cacert=cacert,
|
||||
cert=cert, key=key, timeout=timeout,
|
||||
)
|
||||
|
||||
exception_msg = _('Must provide Keystone credentials or user-defined '
|
||||
'endpoint and token')
|
||||
if not endpoint:
|
||||
if session:
|
||||
try:
|
||||
# Pass the endpoint, it will be used to get hostname
|
||||
# and port that will be used for API version caching. It will
|
||||
# be also set as endpoint_override.
|
||||
endpoint = session.get_endpoint(
|
||||
service_type=os_service_type,
|
||||
interface=os_endpoint_type,
|
||||
region_name=os_region_name
|
||||
)
|
||||
except Exception as e:
|
||||
raise exc.AmbiguousAuthSystem(
|
||||
_('%(message)s, error was: %(error)s') %
|
||||
{'message': exception_msg, 'error': e})
|
||||
else:
|
||||
# Neither session, nor valid auth parameters provided
|
||||
raise exc.AmbiguousAuthSystem(exception_msg)
|
||||
|
||||
# Always pass the session
|
||||
kwargs['session'] = session
|
||||
|
||||
return Client(api_version, endpoint, **kwargs)
|
||||
|
||||
|
||||
def Client(version, *args, **kwargs):
|
||||
module = importutils.import_versioned_module('iotronicclient',
|
||||
version, 'client')
|
||||
client_class = getattr(module, 'Client')
|
||||
return client_class(*args, **kwargs)
|
0
iotronicclient/common/__init__.py
Normal file
0
iotronicclient/common/__init__.py
Normal file
0
iotronicclient/common/apiclient/__init__.py
Normal file
0
iotronicclient/common/apiclient/__init__.py
Normal file
517
iotronicclient/common/apiclient/base.py
Normal file
517
iotronicclient/common/apiclient/base.py
Normal file
@ -0,0 +1,517 @@
|
||||
# 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.
|
||||
"""
|
||||
|
||||
|
||||
# E1102: %s is not callable
|
||||
# pylint: disable=E1102
|
||||
|
||||
import abc
|
||||
import copy
|
||||
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
from six.moves import http_client
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from iotronicclient.common.apiclient import exceptions
|
||||
from iotronicclient.common.i18n import _
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""Return id if argument is a Resource.
|
||||
|
||||
Abstracts the common pattern of allowing both an object or an object's ID
|
||||
(UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
try:
|
||||
if obj.uuid:
|
||||
return obj.uuid
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
|
||||
class HookableMixin(object):
|
||||
"""Mixin so classes can register and run hooks."""
|
||||
_hooks_map = {}
|
||||
|
||||
@classmethod
|
||||
def add_hook(cls, hook_type, hook_func):
|
||||
"""Add a new hook of specified type.
|
||||
|
||||
:param cls: class that registers hooks
|
||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||
:param hook_func: hook function
|
||||
"""
|
||||
if hook_type not in cls._hooks_map:
|
||||
cls._hooks_map[hook_type] = []
|
||||
|
||||
cls._hooks_map[hook_type].append(hook_func)
|
||||
|
||||
@classmethod
|
||||
def run_hooks(cls, hook_type, *args, **kwargs):
|
||||
"""Run all hooks of specified type.
|
||||
|
||||
:param cls: class that registers hooks
|
||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||
:param args: args to be passed to every hook function
|
||||
:param kwargs: kwargs to be passed to every hook function
|
||||
"""
|
||||
hook_funcs = cls._hooks_map.get(hook_type) or []
|
||||
for hook_func in hook_funcs:
|
||||
hook_func(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseManager(HookableMixin):
|
||||
"""Basic manager type providing common operations.
|
||||
|
||||
Managers interact with a particular type of API (servers, flavors, images,
|
||||
etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initializes BaseManager with `client`.
|
||||
|
||||
:param client: instance of BaseClient descendant for HTTP requests
|
||||
"""
|
||||
super(BaseManager, self).__init__()
|
||||
self.client = client
|
||||
|
||||
def _list(self, url, response_key=None, obj_class=None, json=None):
|
||||
"""List the collection.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'. If response_key is None - all response body
|
||||
will be used.
|
||||
:param obj_class: class for constructing the returned objects
|
||||
(self.resource_class will be used by default)
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
"""
|
||||
if json:
|
||||
body = self.client.post(url, json=json).json()
|
||||
else:
|
||||
body = self.client.get(url).json()
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
|
||||
data = body[response_key] if response_key is not None else body
|
||||
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
|
||||
# unlike other services which just return the list...
|
||||
try:
|
||||
data = data['values']
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||
|
||||
def _get(self, url, response_key=None):
|
||||
"""Get an object from collection.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'server'. If response_key is None - all response body
|
||||
will be used.
|
||||
"""
|
||||
body = self.client.get(url).json()
|
||||
data = body[response_key] if response_key is not None else body
|
||||
return self.resource_class(self, data, loaded=True)
|
||||
|
||||
def _head(self, url):
|
||||
"""Retrieve request headers for an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
"""
|
||||
resp = self.client.head(url)
|
||||
return resp.status_code == http_client.NO_CONTENT
|
||||
|
||||
def _post(self, url, json, response_key=None, return_raw=False):
|
||||
"""Create an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'server'. If response_key is None - all response body
|
||||
will be used.
|
||||
:param return_raw: flag to force returning raw JSON instead of
|
||||
Python object of self.resource_class
|
||||
"""
|
||||
body = self.client.post(url, json=json).json()
|
||||
data = body[response_key] if response_key is not None else body
|
||||
if return_raw:
|
||||
return data
|
||||
return self.resource_class(self, data)
|
||||
|
||||
def _put(self, url, json=None, response_key=None):
|
||||
"""Update an object with PUT method.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'. If response_key is None - all response body
|
||||
will be used.
|
||||
"""
|
||||
resp = self.client.put(url, json=json)
|
||||
# PUT requests may not return a body
|
||||
if resp.content:
|
||||
body = resp.json()
|
||||
if response_key is not None:
|
||||
return self.resource_class(self, body[response_key])
|
||||
else:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _patch(self, url, json=None, response_key=None):
|
||||
"""Update an object with PATCH method.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'. If response_key is None - all response body
|
||||
will be used.
|
||||
"""
|
||||
body = self.client.patch(url, json=json).json()
|
||||
if response_key is not None:
|
||||
return self.resource_class(self, body[response_key])
|
||||
else:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _delete(self, url):
|
||||
"""Delete an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers/my-server'
|
||||
"""
|
||||
return self.client.delete(url)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ManagerWithFind(BaseManager):
|
||||
"""Manager with additional `find()`/`findall()` methods."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
def find(self, **kwargs):
|
||||
"""Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
matches = self.findall(**kwargs)
|
||||
num_matches = len(matches)
|
||||
if num_matches == 0:
|
||||
msg = _("No %(name)s matching %(args)s.") % {
|
||||
'name': self.resource_class.__name__,
|
||||
'args': kwargs
|
||||
}
|
||||
raise exceptions.NotFound(msg)
|
||||
elif num_matches > 1:
|
||||
raise exceptions.NoUniqueMatch()
|
||||
else:
|
||||
return matches[0]
|
||||
|
||||
def findall(self, **kwargs):
|
||||
"""Find all items with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
found = []
|
||||
searches = kwargs.items()
|
||||
|
||||
for obj in self.list():
|
||||
try:
|
||||
if all(getattr(obj, attr) == value
|
||||
for (attr, value) in searches):
|
||||
found.append(obj)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
return found
|
||||
|
||||
|
||||
class CrudManager(BaseManager):
|
||||
"""Base manager class for manipulating entities.
|
||||
|
||||
Children of this class are expected to define a `collection_key` and `key`.
|
||||
|
||||
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
|
||||
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
|
||||
objects containing a list of member resources (e.g. `{'entities': [{},
|
||||
{}, {}]}`).
|
||||
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
|
||||
refer to an individual member of the collection.
|
||||
|
||||
"""
|
||||
collection_key = None
|
||||
key = None
|
||||
|
||||
def build_url(self, base_url=None, **kwargs):
|
||||
"""Builds a resource URL for the given kwargs.
|
||||
|
||||
Given an example collection where `collection_key = 'entities'` and
|
||||
`key = 'entity'`, the following URL's could be generated.
|
||||
|
||||
By default, the URL will represent a collection of entities, e.g.::
|
||||
|
||||
/entities
|
||||
|
||||
If kwargs contains an `entity_id`, then the URL will represent a
|
||||
specific member, e.g.::
|
||||
|
||||
/entities/{entity_id}
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
url = base_url if base_url is not None else ''
|
||||
|
||||
url += '/%s' % self.collection_key
|
||||
|
||||
# do we have a specific entity?
|
||||
entity_id = kwargs.get('%s_id' % self.key)
|
||||
if entity_id is not None:
|
||||
url += '/%s' % entity_id
|
||||
|
||||
return url
|
||||
|
||||
def _filter_kwargs(self, kwargs):
|
||||
"""Drop null values and handle ids."""
|
||||
for key, ref in kwargs.copy().items():
|
||||
if ref is None:
|
||||
kwargs.pop(key)
|
||||
else:
|
||||
if isinstance(ref, Resource):
|
||||
kwargs.pop(key)
|
||||
kwargs['%s_id' % key] = getid(ref)
|
||||
return kwargs
|
||||
|
||||
def create(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._post(
|
||||
self.build_url(**kwargs),
|
||||
{self.key: kwargs},
|
||||
self.key)
|
||||
|
||||
def get(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._get(
|
||||
self.build_url(**kwargs),
|
||||
self.key)
|
||||
|
||||
def head(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._head(self.build_url(**kwargs))
|
||||
|
||||
def list(self, base_url=None, **kwargs):
|
||||
"""List the collection.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._list(
|
||||
'%(base_url)s%(query)s' % {
|
||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
||||
},
|
||||
self.collection_key)
|
||||
|
||||
def put(self, base_url=None, **kwargs):
|
||||
"""Update an element.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._put(self.build_url(base_url=base_url, **kwargs))
|
||||
|
||||
def update(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
params = kwargs.copy()
|
||||
params.pop('%s_id' % self.key)
|
||||
|
||||
return self._patch(
|
||||
self.build_url(**kwargs),
|
||||
{self.key: params},
|
||||
self.key)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._delete(
|
||||
self.build_url(**kwargs))
|
||||
|
||||
def find(self, base_url=None, **kwargs):
|
||||
"""Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
rl = self._list(
|
||||
'%(base_url)s%(query)s' % {
|
||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
||||
},
|
||||
self.collection_key)
|
||||
num = len(rl)
|
||||
|
||||
if num == 0:
|
||||
msg = _("No %(name)s matching %(args)s.") % {
|
||||
'name': self.resource_class.__name__,
|
||||
'args': kwargs
|
||||
}
|
||||
raise exceptions.NotFound(msg)
|
||||
elif num > 1:
|
||||
raise exceptions.NoUniqueMatch
|
||||
else:
|
||||
return rl[0]
|
||||
|
||||
|
||||
class Extension(HookableMixin):
|
||||
"""Extension descriptor."""
|
||||
|
||||
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
|
||||
manager_class = None
|
||||
|
||||
def __init__(self, name, module):
|
||||
super(Extension, self).__init__()
|
||||
self.name = name
|
||||
self.module = module
|
||||
self._parse_extension_module()
|
||||
|
||||
def _parse_extension_module(self):
|
||||
self.manager_class = None
|
||||
for attr_name, attr_value in self.module.__dict__.items():
|
||||
if attr_name in self.SUPPORTED_HOOKS:
|
||||
self.add_hook(attr_name, attr_value)
|
||||
else:
|
||||
try:
|
||||
if issubclass(attr_value, BaseManager):
|
||||
self.manager_class = attr_value
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return "<Extension '%s'>" % self.name
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""Base class for OpenStack resources (tenant, user, etc.).
|
||||
|
||||
This is pretty much just a bag for attributes.
|
||||
"""
|
||||
|
||||
HUMAN_ID = False
|
||||
NAME_ATTR = 'name'
|
||||
|
||||
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)
|
||||
|
||||
@property
|
||||
def human_id(self):
|
||||
"""Human-readable ID which can be used for bash completion."""
|
||||
if self.HUMAN_ID:
|
||||
name = getattr(self, self.NAME_ATTR, None)
|
||||
if name is not None:
|
||||
return strutils.to_slug(name)
|
||||
return None
|
||||
|
||||
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
|
||||
return self._info == other._info
|
||||
|
||||
def is_loaded(self):
|
||||
return self._loaded
|
||||
|
||||
def set_loaded(self, val):
|
||||
self._loaded = val
|
||||
|
||||
def to_dict(self):
|
||||
return copy.deepcopy(self._info)
|
469
iotronicclient/common/apiclient/exceptions.py
Normal file
469
iotronicclient/common/apiclient/exceptions.py
Normal file
@ -0,0 +1,469 @@
|
||||
# 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 six.moves import http_client
|
||||
|
||||
from iotronicclient.common.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 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."""
|
||||
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 = http_client.MULTIPLE_CHOICES
|
||||
message = _("Multiple Choices")
|
||||
|
||||
|
||||
class BadRequest(HTTPClientError):
|
||||
"""HTTP 400 - Bad Request.
|
||||
|
||||
The request cannot be fulfilled due to bad syntax.
|
||||
"""
|
||||
http_status = http_client.BAD_REQUEST
|
||||
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 = http_client.UNAUTHORIZED
|
||||
message = _("Unauthorized")
|
||||
|
||||
|
||||
class PaymentRequired(HTTPClientError):
|
||||
"""HTTP 402 - Payment Required.
|
||||
|
||||
Reserved for future use.
|
||||
"""
|
||||
http_status = http_client.PAYMENT_REQUIRED
|
||||
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 = http_client.FORBIDDEN
|
||||
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 = http_client.NOT_FOUND
|
||||
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 = http_client.METHOD_NOT_ALLOWED
|
||||
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 = http_client.NOT_ACCEPTABLE
|
||||
message = _("Not Acceptable")
|
||||
|
||||
|
||||
class ProxyAuthenticationRequired(HTTPClientError):
|
||||
"""HTTP 407 - Proxy Authentication Required.
|
||||
|
||||
The client must first authenticate itself with the proxy.
|
||||
"""
|
||||
http_status = http_client.PROXY_AUTHENTICATION_REQUIRED
|
||||
message = _("Proxy Authentication Required")
|
||||
|
||||
|
||||
class RequestTimeout(HTTPClientError):
|
||||
"""HTTP 408 - Request Timeout.
|
||||
|
||||
The server timed out waiting for the request.
|
||||
"""
|
||||
http_status = http_client.REQUEST_TIMEOUT
|
||||
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 = http_client.CONFLICT
|
||||
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 = http_client.GONE
|
||||
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 = http_client.LENGTH_REQUIRED
|
||||
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 = http_client.PRECONDITION_FAILED
|
||||
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 = http_client.REQUEST_ENTITY_TOO_LARGE
|
||||
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 = http_client.REQUEST_URI_TOO_LONG
|
||||
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 = http_client.UNSUPPORTED_MEDIA_TYPE
|
||||
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 = http_client.REQUESTED_RANGE_NOT_SATISFIABLE
|
||||
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 = http_client.EXPECTATION_FAILED
|
||||
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 = http_client.UNPROCESSABLE_ENTITY
|
||||
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 = http_client.INTERNAL_SERVER_ERROR
|
||||
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 = http_client.NOT_IMPLEMENTED
|
||||
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 = http_client.BAD_GATEWAY
|
||||
message = _("Bad Gateway")
|
||||
|
||||
|
||||
class ServiceUnavailable(HttpServerError):
|
||||
"""HTTP 503 - Service Unavailable.
|
||||
|
||||
The server is currently unavailable.
|
||||
"""
|
||||
http_status = http_client.SERVICE_UNAVAILABLE
|
||||
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 = http_client.GATEWAY_TIMEOUT
|
||||
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 = http_client.HTTP_VERSION_NOT_SUPPORTED
|
||||
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:
|
||||
# 5XX status codes are server errors
|
||||
if response.status_code >= http_client.INTERNAL_SERVER_ERROR:
|
||||
cls = HttpServerError
|
||||
# 4XX status codes are client request errors
|
||||
elif (http_client.BAD_REQUEST <= response.status_code <
|
||||
http_client.INTERNAL_SERVER_ERROR):
|
||||
cls = HTTPClientError
|
||||
else:
|
||||
cls = HttpError
|
||||
return cls(**kwargs)
|
252
iotronicclient/common/base.py
Normal file
252
iotronicclient/common/base.py
Normal file
@ -0,0 +1,252 @@
|
||||
# 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 abc
|
||||
import copy
|
||||
import six
|
||||
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from iotronicclient.common.apiclient import base
|
||||
from iotronicclient import exc
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Manager(object):
|
||||
"""Provides CRUD operations with a particular API."""
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def _path(self, resource_id=None):
|
||||
"""Returns a request path for a given resource identifier.
|
||||
|
||||
:param resource_id: Identifier of the resource to generate the request
|
||||
path.
|
||||
"""
|
||||
return ('/v1/%s/%s' % (self._resource_name, resource_id)
|
||||
if resource_id else '/v1/%s' % self._resource_name)
|
||||
|
||||
@abc.abstractproperty
|
||||
def resource_class(self):
|
||||
"""The resource class
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractproperty
|
||||
def _resource_name(self):
|
||||
"""The resource name.
|
||||
|
||||
"""
|
||||
|
||||
def _get(self, resource_id, fields=None):
|
||||
"""Retrieve a resource.
|
||||
|
||||
:param resource_id: Identifier of the resource.
|
||||
:param fields: List of specific fields to be returned.
|
||||
:raises exc.ValidationError: For invalid resource_id arg value.
|
||||
"""
|
||||
|
||||
if not resource_id:
|
||||
raise exc.ValidationError(
|
||||
"The identifier argument is invalid. "
|
||||
"Value provided: {!r}".format(resource_id))
|
||||
|
||||
if fields is not None:
|
||||
resource_id = '%s?fields=' % resource_id
|
||||
resource_id += ','.join(fields)
|
||||
|
||||
try:
|
||||
return self._list(self._path(resource_id))[0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def _get_as_dict(self, resource_id, fields=None):
|
||||
"""Retrieve a resource as a dictionary
|
||||
|
||||
:param resource_id: Identifier of the resource.
|
||||
:param fields: List of specific fields to be returned.
|
||||
:returns: a dictionary representing the resource; may be empty
|
||||
"""
|
||||
|
||||
resource = self._get(resource_id, fields=fields)
|
||||
if resource:
|
||||
return resource.to_dict()
|
||||
else:
|
||||
return {}
|
||||
|
||||
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 Iotronic API is configured to return a maximum number of
|
||||
items per request, (see Iotronic'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. '/boards'
|
||||
:param response_key: the key to be looked up in response
|
||||
dictionary, e.g. 'boards'
|
||||
: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, resource_id, patch, method='PATCH'):
|
||||
"""Update a resource.
|
||||
|
||||
:param resource_id: Resource identifier.
|
||||
:param patch: New version of a given resource.
|
||||
:param method: Name of the method for the request.
|
||||
"""
|
||||
|
||||
url = self._path(resource_id)
|
||||
resp, body = self.api.json_request(method, url, body=patch)
|
||||
# PATCH/PUT requests may not return a body
|
||||
if body:
|
||||
try:
|
||||
return self.resource_class(self, body)
|
||||
except Exception:
|
||||
return body
|
||||
|
||||
def _delete(self, resource_id):
|
||||
"""Delete a resource.
|
||||
|
||||
:param resource_id: Resource identifier.
|
||||
"""
|
||||
self.api.raw_request('DELETE', self._path(resource_id))
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CreateManager(Manager):
|
||||
"""Provides creation operations with a particular API."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def _creation_attributes(self):
|
||||
"""A list of required creation attributes for a resource type.
|
||||
|
||||
"""
|
||||
|
||||
def create(self, **kwargs):
|
||||
"""Create a resource based on a kwargs dictionary of attributes.
|
||||
|
||||
:param kwargs: A dictionary containing the attributes of the resource
|
||||
that will be created.
|
||||
:raises exc.InvalidAttribute: For invalid attributes that are not
|
||||
needed to create the resource.
|
||||
"""
|
||||
|
||||
new = {}
|
||||
invalid = []
|
||||
for (key, value) in kwargs.items():
|
||||
if key in self._creation_attributes:
|
||||
new[key] = value
|
||||
else:
|
||||
invalid.append(key)
|
||||
if invalid:
|
||||
raise exc.InvalidAttribute(
|
||||
'The attribute(s) "%(attrs)s" are invalid; they are not '
|
||||
'needed to create %(resource)s.' %
|
||||
{'resource': self._resource_name,
|
||||
'attrs': '","'.join(invalid)})
|
||||
url = self._path()
|
||||
resp, body = self.api.json_request('POST', url, body=new)
|
||||
if body:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
|
||||
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)
|
293
iotronicclient/common/cliutils.py
Normal file
293
iotronicclient/common/cliutils.py
Normal file
@ -0,0 +1,293 @@
|
||||
# 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 json
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import strutils
|
||||
import prettytable
|
||||
import six
|
||||
from six import moves
|
||||
|
||||
from iotronicclient.common.i18n import _
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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 args: 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 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, json_flag=False):
|
||||
"""Print a list of objects or dict as a table, one row per object or dict.
|
||||
|
||||
: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.
|
||||
:param json_flag: print the list as JSON instead of table
|
||||
"""
|
||||
def _get_name_and_data(field):
|
||||
if field in formatters:
|
||||
# The value of the field has to be modified.
|
||||
# For example, it can be used to add extra fields.
|
||||
return (field, formatters[field](o))
|
||||
|
||||
field_name = field.replace(' ', '_')
|
||||
if field not in mixed_case_fields:
|
||||
field_name = field.lower()
|
||||
if isinstance(o, dict):
|
||||
data = o.get(field_name, '')
|
||||
else:
|
||||
data = getattr(o, field_name, '')
|
||||
return (field_name, data)
|
||||
|
||||
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'
|
||||
|
||||
json_array = []
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
row.append(_get_name_and_data(field))
|
||||
if json_flag:
|
||||
json_array.append(dict(row))
|
||||
else:
|
||||
pt.add_row([r[1] for r in row])
|
||||
|
||||
if json_flag:
|
||||
print(json.dumps(json_array, indent=4, separators=(',', ': ')))
|
||||
elif six.PY3:
|
||||
print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
|
||||
else:
|
||||
print(encodeutils.safe_encode(pt.get_string(**kwargs)))
|
||||
|
||||
|
||||
def print_dict(dct, dict_property="Property", wrap=0, dict_value='Value',
|
||||
json_flag=False):
|
||||
"""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
|
||||
:param dict_value: header label for the value (second) column
|
||||
:param json_flag: print `dict` as JSON instead of table
|
||||
"""
|
||||
if json_flag:
|
||||
print(json.dumps(dct, indent=4, separators=(',', ': ')))
|
||||
return
|
||||
pt = prettytable.PrettyTable([dict_property, dict_value])
|
||||
pt.align = 'l'
|
||||
for k, v in sorted(dct.items()):
|
||||
# convert dict to str to check length
|
||||
if isinstance(v, dict):
|
||||
v = six.text_type(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 = ''
|
||||
else:
|
||||
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)
|
104
iotronicclient/common/filecache.py
Normal file
104
iotronicclient/common/filecache.py
Normal file
@ -0,0 +1,104 @@
|
||||
#
|
||||
# Copyright 2015 Rackspace, 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
|
||||
import os
|
||||
|
||||
import appdirs
|
||||
import dogpile.cache
|
||||
|
||||
from iotronicclient.common.i18n import _LW
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
AUTHOR = 'openstack'
|
||||
PROGNAME = 'python-iotronicclient'
|
||||
|
||||
CACHE = None
|
||||
CACHE_DIR = appdirs.user_cache_dir(PROGNAME, AUTHOR)
|
||||
CACHE_EXPIRY_ENV_VAR = 'IOTRONICCLIENT_CACHE_EXPIRY' # environment variable
|
||||
CACHE_FILENAME = os.path.join(CACHE_DIR, 'iotronic-api-version.dbm')
|
||||
DEFAULT_EXPIRY = 300 # seconds
|
||||
|
||||
|
||||
def _get_cache():
|
||||
"""Configure file caching."""
|
||||
global CACHE
|
||||
if CACHE is None:
|
||||
|
||||
# Ensure cache directory present
|
||||
if not os.path.exists(CACHE_DIR):
|
||||
os.makedirs(CACHE_DIR)
|
||||
|
||||
# Use the cache expiry if specified in an env var
|
||||
expiry_time = os.environ.get(CACHE_EXPIRY_ENV_VAR, DEFAULT_EXPIRY)
|
||||
try:
|
||||
expiry_time = int(expiry_time)
|
||||
except ValueError:
|
||||
LOG.warning(_LW("Environment variable %(env_var)s should be an "
|
||||
"integer (not '%(curr_val)s'). Using default "
|
||||
"expiry of %(default)s seconds instead."),
|
||||
{'env_var': CACHE_EXPIRY_ENV_VAR,
|
||||
'curr_val': expiry_time,
|
||||
'default': DEFAULT_EXPIRY})
|
||||
expiry_time = DEFAULT_EXPIRY
|
||||
|
||||
CACHE = dogpile.cache.make_region(key_mangler=str).configure(
|
||||
'dogpile.cache.dbm',
|
||||
expiration_time=expiry_time,
|
||||
arguments={
|
||||
"filename": CACHE_FILENAME,
|
||||
}
|
||||
)
|
||||
return CACHE
|
||||
|
||||
|
||||
def _build_key(host, port):
|
||||
"""Build a key based upon the hostname or address supplied."""
|
||||
return "%s:%s" % (host, port)
|
||||
|
||||
|
||||
def save_data(host, port, data):
|
||||
"""Save 'data' for a particular 'host' in the appropriate cache dir.
|
||||
|
||||
param host: The host that we need to save data for
|
||||
param port: The port on the host that we need to save data for
|
||||
param data: The data we want saved
|
||||
"""
|
||||
key = _build_key(host, port)
|
||||
_get_cache().set(key, data)
|
||||
|
||||
|
||||
def retrieve_data(host, port, expiry=None):
|
||||
"""Retrieve the version stored for an iotronic 'host', if it's not stale.
|
||||
|
||||
Check to see if there is valid cached data for the host/port
|
||||
combination and return that if it isn't stale.
|
||||
|
||||
param host: The host that we need to retrieve data for
|
||||
param port: The port on the host that we need to retrieve data for
|
||||
param expiry: The age in seconds before cached data is deemed invalid
|
||||
"""
|
||||
# Ensure that a cache file exists first
|
||||
if not os.path.isfile(CACHE_FILENAME):
|
||||
return None
|
||||
|
||||
key = _build_key(host, port)
|
||||
data = _get_cache().get(key, expiration_time=expiry)
|
||||
|
||||
if data == dogpile.cache.api.NO_VALUE:
|
||||
return None
|
||||
return data
|
645
iotronicclient/common/http.py
Normal file
645
iotronicclient/common/http.py
Normal file
@ -0,0 +1,645 @@
|
||||
# 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
|
||||
from distutils.version import StrictVersion
|
||||
import functools
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import textwrap
|
||||
import time
|
||||
|
||||
from keystoneauth1 import adapter
|
||||
from keystoneauth1 import exceptions as kexc
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import strutils
|
||||
import requests
|
||||
import six
|
||||
from six.moves import http_client
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from iotronicclient.common import filecache
|
||||
from iotronicclient.common.i18n import _
|
||||
from iotronicclient.common.i18n import _LE
|
||||
from iotronicclient.common.i18n import _LW
|
||||
from iotronicclient import exc
|
||||
|
||||
# NOTE(deva): Record the latest version that this client was tested with.
|
||||
# We still have a lot of work to do in the client to implement
|
||||
# microversion support in the client properly! See
|
||||
# http://specs.openstack.org/openstack/iotronic-specs/specs/kilo/
|
||||
# api-microversions.html # noqa
|
||||
# for full details.
|
||||
DEFAULT_VER = '1.9'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
USER_AGENT = 'python-iotronicclient'
|
||||
CHUNKSIZE = 1024 * 64 # 64kB
|
||||
|
||||
API_VERSION = '/v1'
|
||||
API_VERSION_SELECTED_STATES = ('user', 'negotiated', 'cached', 'default')
|
||||
|
||||
DEFAULT_MAX_RETRIES = 5
|
||||
DEFAULT_RETRY_INTERVAL = 2
|
||||
SENSITIVE_HEADERS = ('X-Auth-Token',)
|
||||
|
||||
SUPPORTED_ENDPOINT_SCHEME = ('http', 'https')
|
||||
|
||||
|
||||
def _trim_endpoint_api_version(url):
|
||||
"""Trim API version and trailing slash from endpoint."""
|
||||
return url.rstrip('/').rstrip(API_VERSION)
|
||||
|
||||
|
||||
def _extract_error_json(body):
|
||||
"""Return error_message from the HTTP response body."""
|
||||
error_json = {}
|
||||
try:
|
||||
body_json = jsonutils.loads(body)
|
||||
if 'error_message' in body_json:
|
||||
raw_msg = body_json['error_message']
|
||||
error_json = jsonutils.loads(raw_msg)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return error_json
|
||||
|
||||
|
||||
def get_server(endpoint):
|
||||
"""Extract and return the server & port that we're connecting to."""
|
||||
if endpoint is None:
|
||||
return None, None
|
||||
parts = urlparse.urlparse(endpoint)
|
||||
return parts.hostname, str(parts.port)
|
||||
|
||||
|
||||
class VersionNegotiationMixin(object):
|
||||
def negotiate_version(self, conn, resp):
|
||||
"""Negotiate the server version
|
||||
|
||||
Assumption: Called after receiving a 406 error when doing a request.
|
||||
|
||||
param conn: A connection object
|
||||
param resp: The response object from http request
|
||||
"""
|
||||
if self.api_version_select_state not in API_VERSION_SELECTED_STATES:
|
||||
raise RuntimeError(
|
||||
_('Error: self.api_version_select_state should be one of the '
|
||||
'values in: "%(valid)s" but had the value: "%(value)s"') %
|
||||
{'valid': ', '.join(API_VERSION_SELECTED_STATES),
|
||||
'value': self.api_version_select_state})
|
||||
min_ver, max_ver = self._parse_version_headers(resp)
|
||||
# NOTE: servers before commit 32fb6e99 did not return version headers
|
||||
# on error, so we need to perform a GET to determine
|
||||
# the supported version range
|
||||
if not max_ver:
|
||||
LOG.debug('No version header in response, requesting from server')
|
||||
if self.os_iotronic_api_version:
|
||||
base_version = ("/v%s" %
|
||||
str(self.os_iotronic_api_version).split('.')[
|
||||
0])
|
||||
else:
|
||||
base_version = API_VERSION
|
||||
resp = self._make_simple_request(conn, 'GET', base_version)
|
||||
min_ver, max_ver = self._parse_version_headers(resp)
|
||||
# If the user requested an explicit version or we have negotiated a
|
||||
# version and still failing then error now. The server could
|
||||
# support the version requested but the requested operation may not
|
||||
# be supported by the requested version.
|
||||
if self.api_version_select_state == 'user':
|
||||
raise exc.UnsupportedVersion(textwrap.fill(
|
||||
_("Requested API version %(req)s is not supported by the "
|
||||
"server or the requested operation is not supported by the "
|
||||
"requested version. Supported version range is %(min)s to "
|
||||
"%(max)s")
|
||||
% {'req': self.os_iotronic_api_version,
|
||||
'min': min_ver, 'max': max_ver}))
|
||||
if self.api_version_select_state == 'negotiated':
|
||||
raise exc.UnsupportedVersion(textwrap.fill(
|
||||
_("No API version was specified and the requested operation "
|
||||
"was not supported by the client's negotiated API version "
|
||||
"%(req)s. Supported version range is: %(min)s to %(max)s")
|
||||
% {'req': self.os_iotronic_api_version,
|
||||
'min': min_ver, 'max': max_ver}))
|
||||
|
||||
negotiated_ver = str(min(StrictVersion(self.os_iotronic_api_version),
|
||||
StrictVersion(max_ver)))
|
||||
if negotiated_ver < min_ver:
|
||||
negotiated_ver = min_ver
|
||||
# server handles microversions, but doesn't support
|
||||
# the requested version, so try a negotiated version
|
||||
self.api_version_select_state = 'negotiated'
|
||||
self.os_iotronic_api_version = negotiated_ver
|
||||
LOG.debug('Negotiated API version is %s', negotiated_ver)
|
||||
|
||||
# Cache the negotiated version for this server
|
||||
host, port = get_server(self.endpoint)
|
||||
filecache.save_data(host=host, port=port, data=negotiated_ver)
|
||||
|
||||
return negotiated_ver
|
||||
|
||||
def _generic_parse_version_headers(self, accessor_func):
|
||||
min_ver = accessor_func('X-OpenStack-Iotronic-API-Minimum-Version',
|
||||
None)
|
||||
max_ver = accessor_func('X-OpenStack-Iotronic-API-Maximum-Version',
|
||||
None)
|
||||
return min_ver, max_ver
|
||||
|
||||
def _parse_version_headers(self, accessor_func):
|
||||
# NOTE(jlvillal): Declared for unit testing purposes
|
||||
raise NotImplementedError()
|
||||
|
||||
def _make_simple_request(self, conn, method, url):
|
||||
# NOTE(jlvillal): Declared for unit testing purposes
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
_RETRY_EXCEPTIONS = (exc.Conflict, exc.ServiceUnavailable,
|
||||
exc.ConnectionRefused, kexc.RetriableConnectionFailure)
|
||||
|
||||
|
||||
def with_retries(func):
|
||||
"""Wrapper for _http_request adding support for retries."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, url, method, **kwargs):
|
||||
if self.conflict_max_retries is None:
|
||||
self.conflict_max_retries = DEFAULT_MAX_RETRIES
|
||||
if self.conflict_retry_interval is None:
|
||||
self.conflict_retry_interval = DEFAULT_RETRY_INTERVAL
|
||||
|
||||
num_attempts = self.conflict_max_retries + 1
|
||||
for attempt in range(1, num_attempts + 1):
|
||||
try:
|
||||
return func(self, url, method, **kwargs)
|
||||
except _RETRY_EXCEPTIONS as error:
|
||||
msg = (_LE("Error contacting Iotronic server: %(error)s. "
|
||||
"Attempt %(attempt)d of %(total)d") %
|
||||
{'attempt': attempt,
|
||||
'total': num_attempts,
|
||||
'error': error})
|
||||
if attempt == num_attempts:
|
||||
LOG.error(msg)
|
||||
raise
|
||||
else:
|
||||
LOG.debug(msg)
|
||||
time.sleep(self.conflict_retry_interval)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class HTTPClient(VersionNegotiationMixin):
|
||||
def __init__(self, endpoint, **kwargs):
|
||||
self.endpoint = endpoint
|
||||
self.endpoint_trimmed = _trim_endpoint_api_version(endpoint)
|
||||
self.auth_token = kwargs.get('token')
|
||||
self.auth_ref = kwargs.get('auth_ref')
|
||||
self.os_iotronic_api_version = kwargs.get('os_iotronic_api_version',
|
||||
DEFAULT_VER)
|
||||
self.api_version_select_state = kwargs.get(
|
||||
'api_version_select_state', 'default')
|
||||
self.conflict_max_retries = kwargs.pop('max_retries',
|
||||
DEFAULT_MAX_RETRIES)
|
||||
self.conflict_retry_interval = kwargs.pop('retry_interval',
|
||||
DEFAULT_RETRY_INTERVAL)
|
||||
self.session = requests.Session()
|
||||
|
||||
parts = urlparse.urlparse(endpoint)
|
||||
if parts.scheme not in SUPPORTED_ENDPOINT_SCHEME:
|
||||
msg = _('Unsupported scheme: %s') % parts.scheme
|
||||
raise exc.EndpointException(msg)
|
||||
|
||||
if parts.scheme == 'https':
|
||||
if kwargs.get('insecure') is True:
|
||||
self.session.verify = False
|
||||
elif kwargs.get('ca_file'):
|
||||
self.session.verify = kwargs['ca_file']
|
||||
self.session.cert = (kwargs.get('cert_file'),
|
||||
kwargs.get('key_file'))
|
||||
|
||||
def _process_header(self, name, value):
|
||||
"""Redacts any sensitive header
|
||||
|
||||
Redact a header that contains sensitive information, by returning an
|
||||
updated header with the sha1 hash of that value. The redacted value is
|
||||
prefixed by '{SHA1}' because that's the convention used within
|
||||
OpenStack.
|
||||
|
||||
:returns: A tuple of (name, value)
|
||||
name: the safe encoding format of name
|
||||
value: the redacted value if name is x-auth-token,
|
||||
or the safe encoding format of name
|
||||
|
||||
"""
|
||||
if name in SENSITIVE_HEADERS:
|
||||
v = value.encode('utf-8')
|
||||
h = hashlib.sha1(v)
|
||||
d = h.hexdigest()
|
||||
return (name, "{SHA1}%s" % d)
|
||||
else:
|
||||
return (name, value)
|
||||
|
||||
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\'' % self._process_header(key, value)
|
||||
curl.append(header)
|
||||
|
||||
if not self.session.verify:
|
||||
curl.append('-k')
|
||||
elif isinstance(self.session.verify, six.string_types):
|
||||
curl.append('--cacert %s' % self.session.verify)
|
||||
|
||||
if self.session.cert:
|
||||
curl.append('--cert %s' % self.session.cert[0])
|
||||
curl.append('--key %s' % self.session.cert[1])
|
||||
|
||||
if 'body' in kwargs:
|
||||
body = strutils.mask_password(kwargs['body'])
|
||||
curl.append('-d \'%s\'' % body)
|
||||
|
||||
curl.append(urlparse.urljoin(self.endpoint_trimmed, url))
|
||||
LOG.debug(' '.join(curl))
|
||||
|
||||
@staticmethod
|
||||
def log_http_response(resp, body=None):
|
||||
# NOTE(aarefiev): resp.raw is urllib3 response object, it's used
|
||||
# only to get 'version', response from request with 'stream = True'
|
||||
# should be used for raw reading.
|
||||
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 body:
|
||||
body = strutils.mask_password(body)
|
||||
dump.extend([body, ''])
|
||||
LOG.debug('\n'.join(dump))
|
||||
|
||||
def _make_connection_url(self, url):
|
||||
return urlparse.urljoin(self.endpoint_trimmed, url)
|
||||
|
||||
def _parse_version_headers(self, resp):
|
||||
return self._generic_parse_version_headers(resp.headers.get)
|
||||
|
||||
def _make_simple_request(self, conn, method, url):
|
||||
return conn.request(method, self._make_connection_url(url))
|
||||
|
||||
@with_retries
|
||||
def _http_request(self, url, method, **kwargs):
|
||||
"""Send an http request with the specified characteristics.
|
||||
|
||||
Wrapper around request.Session.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.os_iotronic_api_version:
|
||||
kwargs['headers'].setdefault('X-OpenStack-Iotronic-API-Version',
|
||||
self.os_iotronic_api_version)
|
||||
if self.auth_token:
|
||||
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
|
||||
|
||||
self.log_curl_request(method, url, kwargs)
|
||||
|
||||
# NOTE(aarefiev): This is for backwards compatibility, request
|
||||
# expected body in 'data' field, previously we used httplib,
|
||||
# which expected 'body' field.
|
||||
body = kwargs.pop('body', None)
|
||||
if body:
|
||||
kwargs['data'] = body
|
||||
|
||||
conn_url = self._make_connection_url(url)
|
||||
try:
|
||||
resp = self.session.request(method,
|
||||
conn_url,
|
||||
**kwargs)
|
||||
|
||||
# TODO(deva): implement graceful client downgrade when connecting
|
||||
# to servers that did not support microversions. Details here:
|
||||
# http://specs.openstack.org/openstack/iotronic-specs/specs/kilo/
|
||||
# api-microversions.html#use-case-3b-new-client-communicating-with
|
||||
# -a-old-iotronic-user-specified # noqa
|
||||
|
||||
if resp.status_code == http_client.NOT_ACCEPTABLE:
|
||||
negotiated_ver = self.negotiate_version(self.session, resp)
|
||||
kwargs['headers']['X-OpenStack-Iotronic-API-Version'] = (
|
||||
negotiated_ver)
|
||||
return self._http_request(url, method, **kwargs)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
message = (_("Error has occurred while handling "
|
||||
"request for %(url)s: %(e)s") %
|
||||
dict(url=conn_url, e=e))
|
||||
# NOTE(aarefiev): not valid request(invalid url, missing schema,
|
||||
# and so on), retrying is not needed.
|
||||
if isinstance(e, ValueError):
|
||||
raise exc.ValidationError(message)
|
||||
|
||||
raise exc.ConnectionRefused(message)
|
||||
|
||||
body_str = None
|
||||
if resp.headers.get('Content-Type') == 'application/octet-stream':
|
||||
body_iter = resp.iter_content(chunk_size=CHUNKSIZE)
|
||||
self.log_http_response(resp)
|
||||
else:
|
||||
# Read body into string if it isn't obviously image data
|
||||
body_str = resp.text
|
||||
self.log_http_response(resp, body_str)
|
||||
body_iter = six.StringIO(body_str)
|
||||
|
||||
if resp.status_code >= http_client.BAD_REQUEST:
|
||||
error_json = _extract_error_json(body_str)
|
||||
# NOTE(vdrok): exceptions from iotronic controllers'
|
||||
# _lookup methods
|
||||
# are constructed directly by pecan instead of wsme, and contain
|
||||
# only description field
|
||||
raise exc.from_response(
|
||||
resp, (error_json.get('faultstring') or
|
||||
error_json.get('description')),
|
||||
error_json.get('debuginfo'), method, url)
|
||||
elif resp.status_code in (http_client.MOVED_PERMANENTLY,
|
||||
http_client.FOUND,
|
||||
http_client.USE_PROXY):
|
||||
# Redirected. Reissue the request to the new location.
|
||||
return self._http_request(resp['location'], method, **kwargs)
|
||||
elif resp.status_code == http_client.MULTIPLE_CHOICES:
|
||||
raise exc.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'] = jsonutils.dump_as_bytes(kwargs['body'])
|
||||
|
||||
resp, body_iter = self._http_request(url, method, **kwargs)
|
||||
content_type = resp.headers.get('Content-Type')
|
||||
|
||||
if (resp.status_code in (
|
||||
http_client.NO_CONTENT,
|
||||
http_client.RESET_CONTENT) or content_type is None):
|
||||
return resp, list()
|
||||
|
||||
if 'application/json' in content_type:
|
||||
body = ''.join([chunk for chunk in body_iter])
|
||||
try:
|
||||
body = jsonutils.loads(body)
|
||||
except ValueError:
|
||||
LOG.error(_LE('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-compatible 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(VersionNegotiationMixin, adapter.LegacyJsonAdapter):
|
||||
"""HTTP client based on Keystone client session."""
|
||||
|
||||
def __init__(self,
|
||||
os_iotronic_api_version,
|
||||
api_version_select_state,
|
||||
max_retries,
|
||||
retry_interval,
|
||||
endpoint,
|
||||
**kwargs):
|
||||
self.os_iotronic_api_version = os_iotronic_api_version
|
||||
self.api_version_select_state = api_version_select_state
|
||||
self.conflict_max_retries = max_retries
|
||||
self.conflict_retry_interval = retry_interval
|
||||
self.endpoint = endpoint
|
||||
|
||||
super(SessionClient, self).__init__(**kwargs)
|
||||
|
||||
def _parse_version_headers(self, resp):
|
||||
return self._generic_parse_version_headers(resp.headers.get)
|
||||
|
||||
def _make_simple_request(self, conn, method, url):
|
||||
# NOTE: conn is self.session for this class
|
||||
return conn.request(url, method, raise_exc=False)
|
||||
|
||||
@with_retries
|
||||
def _http_request(self, url, method, **kwargs):
|
||||
kwargs.setdefault('user_agent', USER_AGENT)
|
||||
kwargs.setdefault('auth', self.auth)
|
||||
if isinstance(self.endpoint_override, six.string_types):
|
||||
kwargs.setdefault(
|
||||
'endpoint_override',
|
||||
_trim_endpoint_api_version(self.endpoint_override)
|
||||
)
|
||||
|
||||
if getattr(self, 'os_iotronic_api_version', None):
|
||||
kwargs['headers'].setdefault('X-OpenStack-Iotronic-API-Version',
|
||||
self.os_iotronic_api_version)
|
||||
|
||||
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 resp.status_code == http_client.NOT_ACCEPTABLE:
|
||||
negotiated_ver = self.negotiate_version(self.session, resp)
|
||||
kwargs['headers']['X-OpenStack-Iotronic-API-Version'] = (
|
||||
negotiated_ver)
|
||||
return self._http_request(url, method, **kwargs)
|
||||
if resp.status_code >= http_client.BAD_REQUEST:
|
||||
error_json = _extract_error_json(resp.content)
|
||||
# NOTE(vdrok): exceptions from iotronic controllers' _lookup
|
||||
# methods
|
||||
# are constructed directly by pecan instead of wsme, and contain
|
||||
# only description field
|
||||
raise exc.from_response(resp, (error_json.get('faultstring') or
|
||||
error_json.get('description')),
|
||||
error_json.get('debuginfo'), method, url)
|
||||
elif resp.status_code in (http_client.MOVED_PERMANENTLY,
|
||||
http_client.FOUND, http_client.USE_PROXY):
|
||||
# Redirected. Reissue the request to the new location.
|
||||
location = resp.headers.get('location')
|
||||
resp = self._http_request(location, method, **kwargs)
|
||||
elif resp.status_code == http_client.MULTIPLE_CHOICES:
|
||||
raise exc.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'] = jsonutils.dump_as_bytes(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 in (
|
||||
http_client.NO_CONTENT, http_client.RESET_CONTENT) or
|
||||
content_type is None):
|
||||
return resp, list()
|
||||
if 'application/json' in content_type:
|
||||
try:
|
||||
body = resp.json()
|
||||
except ValueError:
|
||||
LOG.error(_LE('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)
|
||||
|
||||
|
||||
def _construct_http_client(endpoint=None,
|
||||
session=None,
|
||||
token=None,
|
||||
auth_ref=None,
|
||||
os_iotronic_api_version=DEFAULT_VER,
|
||||
api_version_select_state='default',
|
||||
max_retries=DEFAULT_MAX_RETRIES,
|
||||
retry_interval=DEFAULT_RETRY_INTERVAL,
|
||||
timeout=600,
|
||||
ca_file=None,
|
||||
cert_file=None,
|
||||
key_file=None,
|
||||
insecure=None,
|
||||
**kwargs):
|
||||
if session:
|
||||
kwargs.setdefault('service_type', 'iot')
|
||||
kwargs.setdefault('user_agent', 'python-iotronicclient')
|
||||
kwargs.setdefault('interface', kwargs.pop('endpoint_type', None))
|
||||
kwargs.setdefault('endpoint_override', endpoint)
|
||||
|
||||
ignored = {'token': token,
|
||||
'auth_ref': auth_ref,
|
||||
'timeout': timeout != 600,
|
||||
'ca_file': ca_file,
|
||||
'cert_file': cert_file,
|
||||
'key_file': key_file,
|
||||
'insecure': insecure}
|
||||
|
||||
dvars = [k for k, v in ignored.items() if v]
|
||||
|
||||
if dvars:
|
||||
LOG.warning(_LW('The following arguments are ignored when using '
|
||||
'the session to construct a client: %s'),
|
||||
', '.join(dvars))
|
||||
|
||||
return SessionClient(session=session,
|
||||
os_iotronic_api_version=os_iotronic_api_version,
|
||||
api_version_select_state=api_version_select_state,
|
||||
max_retries=max_retries,
|
||||
retry_interval=retry_interval,
|
||||
endpoint=endpoint,
|
||||
**kwargs)
|
||||
else:
|
||||
if kwargs:
|
||||
LOG.warning(_LW('The following arguments are being ignored when '
|
||||
'constructing the client: %s'), ', '.join(kwargs))
|
||||
|
||||
return HTTPClient(endpoint=endpoint,
|
||||
token=token,
|
||||
auth_ref=auth_ref,
|
||||
os_iotronic_api_version=os_iotronic_api_version,
|
||||
api_version_select_state=api_version_select_state,
|
||||
max_retries=max_retries,
|
||||
retry_interval=retry_interval,
|
||||
timeout=timeout,
|
||||
ca_file=ca_file,
|
||||
cert_file=cert_file,
|
||||
key_file=key_file,
|
||||
insecure=insecure)
|
31
iotronicclient/common/i18n.py
Normal file
31
iotronicclient/common/i18n.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import oslo_i18n
|
||||
|
||||
_translators = oslo_i18n.TranslatorFactory(domain='iotronicclient')
|
||||
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = _translators.primary
|
||||
|
||||
# Translators for log levels.
|
||||
#
|
||||
# The abbreviated names are meant to reflect the usual use of a short
|
||||
# name like '_'. The "L" is for "log" and the other letter comes from
|
||||
# the level.
|
||||
_LI = _translators.log_info
|
||||
_LW = _translators.log_warning
|
||||
_LE = _translators.log_error
|
||||
_LC = _translators.log_critical
|
371
iotronicclient/common/utils.py
Normal file
371
iotronicclient/common/utils.py
Normal file
@ -0,0 +1,371 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from oslo_serialization import base64
|
||||
from oslo_utils import strutils
|
||||
|
||||
from iotronicclient.common.i18n import _
|
||||
from iotronicclient import exc
|
||||
|
||||
|
||||
class HelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
super(HelpFormatter, self).start_section(heading.capitalize())
|
||||
|
||||
|
||||
def define_command(subparsers, command, callback, cmd_mapper):
|
||||
"""Define a command in the subparsers collection.
|
||||
|
||||
:param subparsers: subparsers collection where the command will go
|
||||
:param command: command name
|
||||
:param callback: function that will be used to process the command
|
||||
"""
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(command, help=help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter)
|
||||
subparser.add_argument('-h', '--help', action='help',
|
||||
help=argparse.SUPPRESS)
|
||||
cmd_mapper[command] = subparser
|
||||
required_args = subparser.add_argument_group(_("Required arguments"))
|
||||
|
||||
for (args, kwargs) in arguments:
|
||||
if kwargs.get('required'):
|
||||
required_args.add_argument(*args, **kwargs)
|
||||
else:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
|
||||
def define_commands_from_module(subparsers, command_module, cmd_mapper):
|
||||
"""Add *do_* methods in a module and add as commands into a subparsers."""
|
||||
|
||||
for method_name in (a for a in dir(command_module) if a.startswith('do_')):
|
||||
# Commands should be hypen-separated instead of underscores.
|
||||
command = method_name[3:].replace('_', '-')
|
||||
callback = getattr(command_module, method_name)
|
||||
define_command(subparsers, command, callback, cmd_mapper)
|
||||
|
||||
|
||||
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 = json.loads(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return (key, value)
|
||||
|
||||
|
||||
def json_from_file(file):
|
||||
with open(file, 'r') as pfil:
|
||||
return json.load(pfil)
|
||||
|
||||
|
||||
def key_value_pairs_to_dict(key_value_pairs):
|
||||
"""Convert a list of key-value pairs to a dictionary.
|
||||
|
||||
:param key_value_pairs: a list of strings, each string is in the form
|
||||
<key>=<value>
|
||||
:returns: a dictionary, possibly empty
|
||||
"""
|
||||
if key_value_pairs:
|
||||
return dict(split_and_deserialize(v) for v in key_value_pairs)
|
||||
return {}
|
||||
|
||||
|
||||
def args_array_to_dict(kwargs, key_to_convert):
|
||||
"""Convert the value in a dictionary entry to a dictionary.
|
||||
|
||||
From the kwargs dictionary, converts the value of the key_to_convert
|
||||
entry from a list of key-value pairs to a dictionary.
|
||||
|
||||
:param kwargs: a dictionary
|
||||
:param key_to_convert: the key (in kwargs), whose value is expected to
|
||||
be a list of key=value strings. This value will be converted to a
|
||||
dictionary.
|
||||
:returns: kwargs, the (modified) dictionary
|
||||
"""
|
||||
values_to_convert = kwargs.get(key_to_convert)
|
||||
if values_to_convert:
|
||||
kwargs[key_to_convert] = key_value_pairs_to_dict(values_to_convert)
|
||||
return kwargs
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def common_params_for_list(args, fields, field_labels):
|
||||
"""Generate 'params' dict that is common for every 'list' command.
|
||||
|
||||
:param args: arguments from command line.
|
||||
:param fields: possible fields for sorting.
|
||||
:param field_labels: possible field labels for sorting.
|
||||
:returns: a dict with params to pass to the client method.
|
||||
"""
|
||||
params = {}
|
||||
if args.marker is not None:
|
||||
params['marker'] = args.marker
|
||||
if args.limit is not None:
|
||||
if args.limit < 0:
|
||||
raise exc.CommandError(
|
||||
_('Expected non-negative --limit, got %s') % args.limit)
|
||||
params['limit'] = args.limit
|
||||
|
||||
if args.sort_key is not None:
|
||||
# Support using both heading and field name for sort_key
|
||||
fields_map = dict(zip(field_labels, fields))
|
||||
fields_map.update(zip(fields, fields))
|
||||
try:
|
||||
sort_key = fields_map[args.sort_key]
|
||||
except KeyError:
|
||||
raise exc.CommandError(
|
||||
_("%(sort_key)s is an invalid field for sorting, "
|
||||
"valid values for --sort-key are: %(valid)s") %
|
||||
{'sort_key': args.sort_key,
|
||||
'valid': list(fields_map)})
|
||||
params['sort_key'] = sort_key
|
||||
if args.sort_dir is not None:
|
||||
if args.sort_dir not in ('asc', 'desc'):
|
||||
raise exc.CommandError(
|
||||
_("%s is an invalid value for sort direction, "
|
||||
"valid values for --sort-dir are: 'asc', 'desc'") %
|
||||
args.sort_dir)
|
||||
params['sort_dir'] = args.sort_dir
|
||||
|
||||
params['detail'] = args.detail
|
||||
|
||||
requested_fields = args.fields[0] if args.fields else None
|
||||
if requested_fields is not None:
|
||||
params['fields'] = requested_fields
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None,
|
||||
fields=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'.
|
||||
:param fields: a list with a specified set of fields of the resource
|
||||
to be returned.
|
||||
:returns: list of string filters.
|
||||
"""
|
||||
filters = []
|
||||
if isinstance(limit, int) and limit > 0:
|
||||
filters.append('limit=%s' % limit)
|
||||
if marker is not None:
|
||||
filters.append('marker=%s' % marker)
|
||||
if sort_key is not None:
|
||||
filters.append('sort_key=%s' % sort_key)
|
||||
if sort_dir is not None:
|
||||
filters.append('sort_dir=%s' % sort_dir)
|
||||
if fields is not None:
|
||||
filters.append('fields=%s' % ','.join(fields))
|
||||
return filters
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def tempdir(*args, **kwargs):
|
||||
dirname = tempfile.mkdtemp(*args, **kwargs)
|
||||
try:
|
||||
yield dirname
|
||||
finally:
|
||||
shutil.rmtree(dirname)
|
||||
|
||||
|
||||
def make_configdrive(path):
|
||||
"""Make the config drive file.
|
||||
|
||||
:param path: The directory containing the config drive files.
|
||||
:returns: A gzipped and base64 encoded configdrive string.
|
||||
|
||||
"""
|
||||
# Make sure path it's readable
|
||||
if not os.access(path, os.R_OK):
|
||||
raise exc.CommandError(_('The directory "%s" is not readable') % path)
|
||||
|
||||
with tempfile.NamedTemporaryFile() as tmpfile:
|
||||
with tempfile.NamedTemporaryFile() as tmpzipfile:
|
||||
publisher = 'iotronicclient-configdrive 0.1'
|
||||
try:
|
||||
p = subprocess.Popen(['genisoimage', '-o', tmpfile.name,
|
||||
'-ldots', '-allow-lowercase',
|
||||
'-allow-multidot', '-l',
|
||||
'-publisher', publisher,
|
||||
'-quiet', '-J',
|
||||
'-r', '-V', 'config-2',
|
||||
path],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
except OSError as e:
|
||||
raise exc.CommandError(
|
||||
_('Error generating the config drive. Make sure the '
|
||||
'"genisoimage" tool is installed. Error: %s') % e)
|
||||
|
||||
stdout, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise exc.CommandError(
|
||||
_('Error generating the config drive.'
|
||||
'Stdout: "%(stdout)s". Stderr: %(stderr)s') %
|
||||
{'stdout': stdout, 'stderr': stderr})
|
||||
|
||||
# Compress file
|
||||
tmpfile.seek(0)
|
||||
g = gzip.GzipFile(fileobj=tmpzipfile, mode='wb')
|
||||
shutil.copyfileobj(tmpfile, g)
|
||||
g.close()
|
||||
|
||||
tmpzipfile.seek(0)
|
||||
return base64.encode_as_bytes(tmpzipfile.read())
|
||||
|
||||
|
||||
def check_empty_arg(arg, arg_descriptor):
|
||||
if not arg.strip():
|
||||
raise exc.CommandError(_('%(arg)s cannot be empty or only have blank'
|
||||
' spaces') % {'arg': arg_descriptor})
|
||||
|
||||
|
||||
def bool_argument_value(arg_name, bool_str, strict=True, default=False):
|
||||
"""Returns the Boolean represented by bool_str.
|
||||
|
||||
Returns the Boolean value for the argument named arg_name. The value is
|
||||
represented by the string bool_str. If the string is an invalid Boolean
|
||||
string: if strict is True, a CommandError exception is raised; otherwise
|
||||
the default value is returned.
|
||||
|
||||
:param arg_name: The name of the argument
|
||||
:param bool_str: The string representing a Boolean value
|
||||
:param strict: Used if the string is invalid. If True, raises an exception.
|
||||
If False, returns the default value.
|
||||
:param default: The default value to return if the string is invalid
|
||||
and not strict
|
||||
:returns: the Boolean value represented by bool_str or the default value
|
||||
if bool_str is invalid and strict is False
|
||||
:raises CommandError: if bool_str is an invalid Boolean string
|
||||
|
||||
"""
|
||||
try:
|
||||
val = strutils.bool_from_string(bool_str, strict, default)
|
||||
except ValueError as e:
|
||||
raise exc.CommandError(_("argument %(arg)s: %(err)s.")
|
||||
% {'arg': arg_name, 'err': e})
|
||||
return val
|
||||
|
||||
|
||||
def check_for_invalid_fields(fields, valid_fields):
|
||||
"""Check for invalid fields.
|
||||
|
||||
:param fields: A list of fields specified by the user.
|
||||
:param valid_fields: A list of valid fields.
|
||||
:raises CommandError: If invalid fields were specified by the user.
|
||||
"""
|
||||
if not fields:
|
||||
return
|
||||
|
||||
invalid_fields = set(fields) - set(valid_fields)
|
||||
if invalid_fields:
|
||||
raise exc.CommandError(
|
||||
_('Invalid field(s) requested: %(invalid)s. Valid fields '
|
||||
'are: %(valid)s.') % {'invalid': ', '.join(invalid_fields),
|
||||
'valid': ', '.join(valid_fields)})
|
||||
|
||||
|
||||
def get_from_stdin(info_desc):
|
||||
"""Read information from stdin.
|
||||
|
||||
:param info_desc: A string description of the desired information
|
||||
:raises: InvalidAttribute if there was a problem reading from stdin
|
||||
:returns: the string that was read from stdin
|
||||
"""
|
||||
try:
|
||||
info = sys.stdin.read().strip()
|
||||
except Exception as e:
|
||||
err = _("Cannot get %(desc)s from standard input. Error: %(err)s")
|
||||
raise exc.InvalidAttribute(err % {'desc': info_desc, 'err': e})
|
||||
return info
|
||||
|
||||
|
||||
def handle_json_or_file_arg(json_arg):
|
||||
"""Attempts to read JSON argument from file or string.
|
||||
|
||||
:param json_arg: May be a file name containing the JSON, or
|
||||
a JSON string.
|
||||
:returns: A list or dictionary parsed from JSON.
|
||||
:raises: InvalidAttribute if the argument cannot be parsed.
|
||||
"""
|
||||
|
||||
if os.path.isfile(json_arg):
|
||||
try:
|
||||
with open(json_arg, 'r') as f:
|
||||
json_arg = f.read().strip()
|
||||
except Exception as e:
|
||||
err = _("Cannot get JSON from file '%(file)s'. "
|
||||
"Error: %(err)s") % {'err': e, 'file': json_arg}
|
||||
raise exc.InvalidAttribute(err)
|
||||
try:
|
||||
json_arg = json.loads(json_arg)
|
||||
except ValueError as e:
|
||||
err = (_("For JSON: '%(string)s', error: '%(err)s'") %
|
||||
{'err': e, 'string': json_arg})
|
||||
raise exc.InvalidAttribute(err)
|
||||
|
||||
return json_arg
|
71
iotronicclient/exc.py
Normal file
71
iotronicclient/exc.py
Normal file
@ -0,0 +1,71 @@
|
||||
# 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 iotronicclient.common.apiclient import exceptions
|
||||
from iotronicclient.common.apiclient.exceptions import * # noqa
|
||||
|
||||
|
||||
# NOTE(akurilin): This alias is left here since v.0.1.3 to support backwards
|
||||
# compatibility.
|
||||
InvalidEndpoint = EndpointException
|
||||
CommunicationError = ConnectionRefused
|
||||
HTTPBadRequest = BadRequest
|
||||
HTTPInternalServerError = InternalServerError
|
||||
HTTPNotFound = NotFound
|
||||
HTTPServiceUnavailable = ServiceUnavailable
|
||||
|
||||
|
||||
class AmbiguousAuthSystem(ClientException):
|
||||
"""Could not obtain token and endpoint using provided credentials."""
|
||||
pass
|
||||
|
||||
# Alias for backwards compatibility
|
||||
AmbigiousAuthSystem = AmbiguousAuthSystem
|
||||
|
||||
|
||||
class InvalidAttribute(ClientException):
|
||||
pass
|
||||
|
||||
|
||||
class StateTransitionFailed(ClientException):
|
||||
"""Failed to reach a requested provision state."""
|
||||
|
||||
|
||||
class StateTransitionTimeout(ClientException):
|
||||
"""Timed out while waiting for a requested provision state."""
|
||||
|
||||
|
||||
def from_response(response, message=None, traceback=None, method=None,
|
||||
url=None):
|
||||
"""Return an HttpError instance based on response from httplib/requests."""
|
||||
|
||||
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(jiangfei): These modifications allow SessionClient
|
||||
# to handle faultstring.
|
||||
response.json = lambda: {'error': error_body}
|
||||
|
||||
return exceptions.from_response(response, method=method, url=url)
|
447
iotronicclient/shell.py
Normal file
447
iotronicclient/shell.py
Normal file
@ -0,0 +1,447 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Command-line interface to the OpenStack Bare Metal Provisioning API.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import logging
|
||||
import os
|
||||
import pkgutil
|
||||
import re
|
||||
import sys
|
||||
|
||||
from keystoneauth1.loading import session as kasession
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
import six
|
||||
|
||||
import iotronicclient
|
||||
from iotronicclient.common.apiclient import exceptions
|
||||
from iotronicclient.common import cliutils
|
||||
from iotronicclient.common import http
|
||||
from iotronicclient.common.i18n import _
|
||||
from iotronicclient.common import utils
|
||||
from iotronicclient import exc
|
||||
|
||||
|
||||
LATEST_API_VERSION = ('1', 'latest')
|
||||
|
||||
|
||||
class IotronicShell(object):
|
||||
|
||||
def get_base_parser(self):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='iotronic',
|
||||
description=__doc__.strip(),
|
||||
epilog=_('See "iotronic help COMMAND" '
|
||||
'for help on a specific command.'),
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter,
|
||||
)
|
||||
|
||||
# Register global Keystone args first so their defaults are respected.
|
||||
# See https://bugs.launchpad.net/python-iotronicclient/+bug/1463581
|
||||
kasession.register_argparse_arguments(parser)
|
||||
|
||||
# Global arguments
|
||||
parser.add_argument('-h', '--help',
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--version',
|
||||
action='version',
|
||||
version=iotronicclient.__version__)
|
||||
|
||||
parser.add_argument('--debug',
|
||||
default=bool(cliutils.env('IOTRONICCLIENT_DEBUG')),
|
||||
action='store_true',
|
||||
help=_('Defaults to env[IOTRONICCLIENT_DEBUG]'))
|
||||
|
||||
parser.add_argument('--json',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=_('Print JSON response without formatting.'))
|
||||
|
||||
parser.add_argument('-v', '--verbose',
|
||||
default=False, action="store_true",
|
||||
help=_('Print more verbose output'))
|
||||
|
||||
# for backward compatibility only
|
||||
parser.add_argument('--cert-file',
|
||||
dest='os_cert',
|
||||
help=_('DEPRECATED! Use --os-cert.'))
|
||||
|
||||
# for backward compatibility only
|
||||
parser.add_argument('--key-file',
|
||||
dest='os_key',
|
||||
help=_('DEPRECATED! Use --os-key.'))
|
||||
|
||||
# for backward compatibility only
|
||||
parser.add_argument('--ca-file',
|
||||
dest='os_cacert',
|
||||
help=_('DEPRECATED! Use --os-cacert.'))
|
||||
|
||||
parser.add_argument('--os-username',
|
||||
default=cliutils.env('OS_USERNAME'),
|
||||
help=_('Defaults to env[OS_USERNAME]'))
|
||||
|
||||
parser.add_argument('--os_username',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-password',
|
||||
default=cliutils.env('OS_PASSWORD'),
|
||||
help=_('Defaults to env[OS_PASSWORD]'))
|
||||
|
||||
parser.add_argument('--os_password',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-tenant-id',
|
||||
default=cliutils.env('OS_TENANT_ID'),
|
||||
help=_('Defaults to env[OS_TENANT_ID]'))
|
||||
|
||||
parser.add_argument('--os_tenant_id',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-tenant-name',
|
||||
default=cliutils.env('OS_TENANT_NAME'),
|
||||
help=_('Defaults to env[OS_TENANT_NAME]'))
|
||||
|
||||
parser.add_argument('--os_tenant_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-auth-url',
|
||||
default=cliutils.env('OS_AUTH_URL'),
|
||||
help=_('Defaults to env[OS_AUTH_URL]'))
|
||||
|
||||
parser.add_argument('--os_auth_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-region-name',
|
||||
default=cliutils.env('OS_REGION_NAME'),
|
||||
help=_('Defaults to env[OS_REGION_NAME]'))
|
||||
|
||||
parser.add_argument('--os_region_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-auth-token',
|
||||
default=cliutils.env('OS_AUTH_TOKEN'),
|
||||
help=_('Defaults to env[OS_AUTH_TOKEN]'))
|
||||
|
||||
parser.add_argument('--os_auth_token',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--iotronic-url',
|
||||
default=cliutils.env('IOTRONIC_URL'),
|
||||
help=_('Defaults to env[IOTRONIC_URL]'))
|
||||
|
||||
parser.add_argument('--iotronic_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--iotronic-api-version',
|
||||
default=cliutils.env(
|
||||
'IOTRONIC_API_VERSION', default='1'),
|
||||
help=_('Accepts 1.x (where "x" is microversion) '
|
||||
'or "latest", Defaults to '
|
||||
'env[IOTRONIC_API_VERSION] or 1'))
|
||||
|
||||
parser.add_argument('--iotronic_api_version',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-service-type',
|
||||
default=cliutils.env('OS_SERVICE_TYPE'),
|
||||
help=_('Defaults to env[OS_SERVICE_TYPE] or '
|
||||
'"iot"'))
|
||||
|
||||
parser.add_argument('--os_service_type',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-endpoint',
|
||||
dest='iotronic_url',
|
||||
default=cliutils.env('OS_SERVICE_ENDPOINT'),
|
||||
help=_('Specify an endpoint to use instead of '
|
||||
'retrieving one from the service catalog '
|
||||
'(via authentication). '
|
||||
'Defaults to env[OS_SERVICE_ENDPOINT].'))
|
||||
|
||||
parser.add_argument('--os_endpoint',
|
||||
dest='iotronic_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-endpoint-type',
|
||||
default=cliutils.env('OS_ENDPOINT_TYPE'),
|
||||
help=_('Defaults to env[OS_ENDPOINT_TYPE] or '
|
||||
'"publicURL"'))
|
||||
|
||||
parser.add_argument('--os_endpoint_type',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-user-domain-id',
|
||||
default=cliutils.env('OS_USER_DOMAIN_ID'),
|
||||
help=_('Defaults to env[OS_USER_DOMAIN_ID].'))
|
||||
|
||||
parser.add_argument('--os-user-domain-name',
|
||||
default=cliutils.env('OS_USER_DOMAIN_NAME'),
|
||||
help=_('Defaults to env[OS_USER_DOMAIN_NAME].'))
|
||||
|
||||
parser.add_argument('--os-project-id',
|
||||
default=cliutils.env('OS_PROJECT_ID'),
|
||||
help=_('Another way to specify tenant ID. '
|
||||
'This option is mutually exclusive with '
|
||||
' --os-tenant-id. '
|
||||
'Defaults to env[OS_PROJECT_ID].'))
|
||||
|
||||
parser.add_argument('--os-project-name',
|
||||
default=cliutils.env('OS_PROJECT_NAME'),
|
||||
help=_('Another way to specify tenant name. '
|
||||
'This option is mutually exclusive with '
|
||||
' --os-tenant-name. '
|
||||
'Defaults to env[OS_PROJECT_NAME].'))
|
||||
|
||||
parser.add_argument('--os-project-domain-id',
|
||||
default=cliutils.env('OS_PROJECT_DOMAIN_ID'),
|
||||
help=_('Defaults to env[OS_PROJECT_DOMAIN_ID].'))
|
||||
|
||||
parser.add_argument('--os-project-domain-name',
|
||||
default=cliutils.env('OS_PROJECT_DOMAIN_NAME'),
|
||||
help=_('Defaults to env[OS_PROJECT_DOMAIN_NAME].'))
|
||||
|
||||
msg = _('Maximum number of retries in case of conflict error '
|
||||
'(HTTP 409). Defaults to env[IOTRONIC_MAX_RETRIES] or %d. '
|
||||
'Use 0 to disable retrying.') % http.DEFAULT_MAX_RETRIES
|
||||
parser.add_argument('--max-retries', type=int, help=msg,
|
||||
default=cliutils.env(
|
||||
'IOTRONIC_MAX_RETRIES',
|
||||
default=str(http.DEFAULT_MAX_RETRIES)))
|
||||
|
||||
msg = _('Amount of time (in seconds) between retries '
|
||||
'in case of conflict error (HTTP 409). '
|
||||
'Defaults to env[IOTRONIC_RETRY_INTERVAL] '
|
||||
'or %d.') % http.DEFAULT_RETRY_INTERVAL
|
||||
parser.add_argument('--retry-interval', type=int, help=msg,
|
||||
default=cliutils.env(
|
||||
'IOTRONIC_RETRY_INTERVAL',
|
||||
default=str(http.DEFAULT_RETRY_INTERVAL)))
|
||||
|
||||
return parser
|
||||
|
||||
def get_available_major_versions(self):
|
||||
matcher = re.compile(r"^v[0-9]+$")
|
||||
submodules = pkgutil.iter_modules([os.path.dirname(__file__)])
|
||||
available_versions = [name[1:] for loader, name, ispkg in submodules
|
||||
if matcher.search(name)]
|
||||
|
||||
return available_versions
|
||||
|
||||
def get_subcommand_parser(self, version):
|
||||
parser = self.get_base_parser()
|
||||
|
||||
self.subcommands = {}
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>',
|
||||
dest='subparser_name')
|
||||
try:
|
||||
submodule = importutils.import_versioned_module('iotronicclient',
|
||||
version, 'shell')
|
||||
except ImportError as e:
|
||||
msg = _("Invalid client version '%(version)s'. "
|
||||
"Major part must be one of: '%(major)s'") % {
|
||||
"version": version,
|
||||
"major": ", ".join(self.get_available_major_versions())}
|
||||
raise exceptions.UnsupportedVersion(
|
||||
_('%(message)s, error was: %(error)s') %
|
||||
{'message': msg, 'error': e})
|
||||
submodule.enhance_parser(parser, subparsers, self.subcommands)
|
||||
utils.define_commands_from_module(subparsers, self, self.subcommands)
|
||||
return parser
|
||||
|
||||
def _setup_debugging(self, debug):
|
||||
if debug:
|
||||
logging.basicConfig(
|
||||
format="%(levelname)s (%(module)s:%(lineno)d) %(message)s",
|
||||
level=logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(
|
||||
format="%(levelname)s %(message)s",
|
||||
level=logging.CRITICAL)
|
||||
|
||||
def do_bash_completion(self):
|
||||
"""Prints all of the commands and options for bash-completion."""
|
||||
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')
|
||||
print(' '.join(commands | options))
|
||||
|
||||
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 iotronic_api_version is '1'.
|
||||
# If user not specify the value of api version, not passing
|
||||
# headers at all.
|
||||
os_iotronic_api_version = None
|
||||
elif len(versions) == 2:
|
||||
os_iotronic_api_version = api_version
|
||||
# In the case of '1.0'
|
||||
if versions[1] == 0:
|
||||
os_iotronic_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, os_iotronic_api_version)
|
||||
|
||||
def main(self, argv):
|
||||
# Parse args once to find version
|
||||
parser = self.get_base_parser()
|
||||
(options, args) = parser.parse_known_args(argv)
|
||||
self._setup_debugging(options.debug)
|
||||
|
||||
# build available subcommands based on version
|
||||
(api_major_version, os_iotronic_api_version) = (
|
||||
self._check_version(options.iotronic_api_version))
|
||||
|
||||
subcommand_parser = self.get_subcommand_parser(api_major_version)
|
||||
self.parser = subcommand_parser
|
||||
|
||||
# Handle top-level --help/-h before attempting to parse
|
||||
# a command off the command line
|
||||
if options.help or not argv:
|
||||
self.do_help(options)
|
||||
return 0
|
||||
|
||||
# Parse args again and call whatever callback was selected
|
||||
args = subcommand_parser.parse_args(argv)
|
||||
|
||||
# Short-circuit and deal with these commands right away.
|
||||
if args.func == self.do_help:
|
||||
self.do_help(args)
|
||||
return 0
|
||||
elif args.func == self.do_bash_completion:
|
||||
self.do_bash_completion()
|
||||
return 0
|
||||
|
||||
if not (args.os_auth_token and (args.iotronic_url or args.os_auth_url)
|
||||
):
|
||||
if not args.os_username:
|
||||
raise exc.CommandError(_("You must provide a username via "
|
||||
"either --os-username or via "
|
||||
"env[OS_USERNAME]"))
|
||||
|
||||
if not args.os_password:
|
||||
# No password, If we've got a tty, try prompting for it
|
||||
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
|
||||
# Check for Ctl-D
|
||||
try:
|
||||
args.os_password = getpass.getpass(
|
||||
'OpenStack Password: ')
|
||||
except EOFError:
|
||||
pass
|
||||
# No password because we didn't have a tty or the
|
||||
# user Ctl-D when prompted.
|
||||
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_tenant_id or args.os_tenant_name or
|
||||
args.os_project_id or args.os_project_name):
|
||||
raise exc.CommandError(
|
||||
_("You must provide a project name or"
|
||||
" project id via --os-project-name, --os-project-id,"
|
||||
" env[OS_PROJECT_ID] or env[OS_PROJECT_NAME]. You may"
|
||||
" use os-project and os-tenant interchangeably."))
|
||||
|
||||
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]"))
|
||||
|
||||
if args.max_retries < 0:
|
||||
raise exc.CommandError(_("You must provide value >= 0 for "
|
||||
"--max-retries"))
|
||||
if args.retry_interval < 1:
|
||||
raise exc.CommandError(_("You must provide value >= 1 for "
|
||||
"--retry-interval"))
|
||||
client_args = (
|
||||
'os_auth_token', 'iotronic_url', 'os_username', 'os_password',
|
||||
'os_auth_url', 'os_project_id', 'os_project_name', 'os_tenant_id',
|
||||
'os_tenant_name', 'os_region_name', 'os_user_domain_id',
|
||||
'os_user_domain_name', 'os_project_domain_id',
|
||||
'os_project_domain_name', 'os_service_type', 'os_endpoint_type',
|
||||
'os_cacert', 'os_cert', 'os_key', 'max_retries', 'retry_interval',
|
||||
'timeout', 'insecure'
|
||||
)
|
||||
kwargs = {}
|
||||
for key in client_args:
|
||||
kwargs[key] = getattr(args, key)
|
||||
kwargs['os_iotronic_api_version'] = os_iotronic_api_version
|
||||
client = iotronicclient.client.get_client(api_major_version, **kwargs)
|
||||
|
||||
try:
|
||||
args.func(client, args)
|
||||
except exc.Unauthorized:
|
||||
raise exc.CommandError(_("Invalid OpenStack Identity credentials"))
|
||||
except exc.CommandError as e:
|
||||
subcommand_parser = self.subcommands[args.subparser_name]
|
||||
subcommand_parser.error(e)
|
||||
|
||||
@cliutils.arg('command', metavar='<subcommand>', nargs='?',
|
||||
help=_('Display help for <subcommand>'))
|
||||
def do_help(self, args):
|
||||
"""Display help about this program or one of its subcommands."""
|
||||
if getattr(args, 'command', None):
|
||||
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()
|
||||
|
||||
|
||||
class HelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
super(HelpFormatter, self).start_section(heading.capitalize())
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
IotronicShell().main(sys.argv[1:])
|
||||
except KeyboardInterrupt:
|
||||
print(_("... terminating iotronic client"), file=sys.stderr)
|
||||
return 130
|
||||
except Exception as e:
|
||||
print(encodeutils.safe_encode(six.text_type(e)), file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
0
iotronicclient/v1/__init__.py
Normal file
0
iotronicclient/v1/__init__.py
Normal file
105
iotronicclient/v1/board.py
Normal file
105
iotronicclient/v1/board.py
Normal file
@ -0,0 +1,105 @@
|
||||
# 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 iotronicclient.common import base
|
||||
from iotronicclient.common.i18n import _
|
||||
from iotronicclient.common import utils
|
||||
from iotronicclient import exc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
_DEFAULT_POLL_INTERVAL = 2
|
||||
|
||||
|
||||
class Board(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<Board %s>" % self._info
|
||||
|
||||
|
||||
class BoardManager(base.CreateManager):
|
||||
resource_class = Board
|
||||
_creation_attributes = ['name', 'code', 'type', 'location', 'mobile',
|
||||
'extra']
|
||||
_resource_name = 'boards'
|
||||
|
||||
def list(self, status=None, marker=None, limit=None,
|
||||
detail=False, sort_key=None, sort_dir=None, fields=None,
|
||||
project=None):
|
||||
"""Retrieve a list of boards.
|
||||
|
||||
:param marker: Optional, the UUID of a board, eg the last
|
||||
board 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 boards to return.
|
||||
2) limit == 0, return the entire list of boards.
|
||||
3) limit param is NOT specified (None), the number of items
|
||||
returned respect the maximum imposed by the Iotronic API
|
||||
(see Iotronic's api.max_limit option).
|
||||
|
||||
:param detail: Optional, boolean whether to return detailed information
|
||||
about boards.
|
||||
|
||||
:param sort_key: Optional, field used for sorting.
|
||||
|
||||
:param sort_dir: Optional, direction of sorting, either 'asc' (the
|
||||
default) or 'desc'.
|
||||
|
||||
:param fields: Optional, a list with a specified set of fields
|
||||
of the resource to be returned. Can not be used
|
||||
when 'detail' is set.
|
||||
|
||||
:param project: Optional string value to get
|
||||
only boards of the project.
|
||||
|
||||
:returns: A list of boards.
|
||||
|
||||
"""
|
||||
|
||||
if limit is not None:
|
||||
limit = int(limit)
|
||||
|
||||
if detail and fields:
|
||||
raise exc.InvalidAttribute(_("Can't fetch a subset of fields "
|
||||
"with 'detail' set"))
|
||||
|
||||
filters = utils.common_filters(marker, limit, sort_key, sort_dir,
|
||||
fields)
|
||||
if project is not None:
|
||||
filters.append('project=%s' % project)
|
||||
if status is not None:
|
||||
filters.append('status=%s' % status)
|
||||
|
||||
path = ''
|
||||
if detail:
|
||||
path += 'detail'
|
||||
if filters:
|
||||
path += '?' + '&'.join(filters)
|
||||
|
||||
if limit is None:
|
||||
return self._list(self._path(path), "boards")
|
||||
else:
|
||||
return self._list_pagination(self._path(path), "boards",
|
||||
limit=limit)
|
||||
|
||||
def get(self, board_id, fields=None):
|
||||
return self._get(resource_id=board_id, fields=fields)
|
||||
|
||||
def delete(self, board_id):
|
||||
return self._delete(resource_id=board_id)
|
||||
|
||||
def update(self, board_id, patch, http_method='PATCH'):
|
||||
return self._update(resource_id=board_id, patch=patch,
|
||||
method=http_method)
|
223
iotronicclient/v1/board_shell.py
Normal file
223
iotronicclient/v1/board_shell.py
Normal file
@ -0,0 +1,223 @@
|
||||
# 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 iotronicclient.common.apiclient import exceptions
|
||||
from iotronicclient.common import cliutils
|
||||
from iotronicclient.common.i18n import _
|
||||
from iotronicclient.common import utils
|
||||
from iotronicclient.v1 import resource_fields as res_fields
|
||||
|
||||
|
||||
def _print_board_show(board, fields=None, json=False):
|
||||
if fields is None:
|
||||
fields = res_fields.BOARD_DETAILED_RESOURCE.fields
|
||||
|
||||
data = dict(
|
||||
[(f, getattr(board, f, '')) for f in fields])
|
||||
cliutils.print_dict(data, wrap=72, json_flag=json)
|
||||
|
||||
|
||||
@cliutils.arg(
|
||||
'board',
|
||||
metavar='<id>',
|
||||
help="Name or UUID of the board ")
|
||||
@cliutils.arg(
|
||||
'--fields',
|
||||
nargs='+',
|
||||
dest='fields',
|
||||
metavar='<field>',
|
||||
action='append',
|
||||
default=[],
|
||||
help="One or more board fields. Only these fields will be fetched from "
|
||||
"the server.")
|
||||
def do_board_show(cc, args):
|
||||
"""Show detailed information about a board."""
|
||||
fields = args.fields[0] if args.fields else None
|
||||
utils.check_empty_arg(args.board, '<id>')
|
||||
utils.check_for_invalid_fields(
|
||||
fields, res_fields.BOARD_DETAILED_RESOURCE.fields)
|
||||
board = cc.board.get(args.board, fields=fields)
|
||||
_print_board_show(board, fields=fields, json=args.json)
|
||||
|
||||
|
||||
@cliutils.arg(
|
||||
'--limit',
|
||||
metavar='<limit>',
|
||||
type=int,
|
||||
help='Maximum number of boards to return per request, '
|
||||
'0 for no limit. Default is the maximum number used '
|
||||
'by the Iotronic API Service.')
|
||||
@cliutils.arg(
|
||||
'--marker',
|
||||
metavar='<board>',
|
||||
help='Board UUID (for example, of the last board in the list from '
|
||||
'a previous request). Returns the list of boards after this UUID.')
|
||||
@cliutils.arg(
|
||||
'--sort-key',
|
||||
metavar='<field>',
|
||||
help='Board field that will be used for sorting.')
|
||||
@cliutils.arg(
|
||||
'--status',
|
||||
metavar='<field>',
|
||||
help='Filter by board status ')
|
||||
@cliutils.arg(
|
||||
'--sort-dir',
|
||||
metavar='<direction>',
|
||||
choices=['asc', 'desc'],
|
||||
help='Sort direction: "asc" (the default) or "desc".')
|
||||
@cliutils.arg(
|
||||
'--project',
|
||||
metavar='<project>',
|
||||
help="Project of the list.")
|
||||
@cliutils.arg(
|
||||
'--detail',
|
||||
dest='detail',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="Show detailed information about the boards.")
|
||||
@cliutils.arg(
|
||||
'--fields',
|
||||
nargs='+',
|
||||
dest='fields',
|
||||
metavar='<field>',
|
||||
action='append',
|
||||
default=[],
|
||||
help="One or more board fields. Only these fields will be fetched from "
|
||||
"the server. Can not be used when '--detail' is specified.")
|
||||
def do_board_list(cc, args):
|
||||
"""List the boards which are registered with the Iotronic service."""
|
||||
params = {}
|
||||
|
||||
if args.status:
|
||||
params['status'] = args.status
|
||||
|
||||
if args.project is not None:
|
||||
params['project'] = args.project
|
||||
|
||||
if args.detail:
|
||||
fields = res_fields.BOARD_DETAILED_RESOURCE.fields
|
||||
field_labels = res_fields.BOARD_DETAILED_RESOURCE.labels
|
||||
elif args.fields:
|
||||
utils.check_for_invalid_fields(
|
||||
args.fields[0], res_fields.BOARD_DETAILED_RESOURCE.fields)
|
||||
resource = res_fields.Resource(args.fields[0])
|
||||
fields = resource.fields
|
||||
field_labels = resource.labels
|
||||
else:
|
||||
fields = res_fields.BOARD_RESOURCE.fields
|
||||
field_labels = res_fields.BOARD_RESOURCE.labels
|
||||
|
||||
sort_fields = res_fields.BOARD_DETAILED_RESOURCE.sort_fields
|
||||
sort_field_labels = res_fields.BOARD_DETAILED_RESOURCE.sort_labels
|
||||
|
||||
params.update(utils.common_params_for_list(args,
|
||||
sort_fields,
|
||||
sort_field_labels))
|
||||
|
||||
boards = cc.board.list(**params)
|
||||
cliutils.print_list(boards, fields,
|
||||
field_labels=field_labels,
|
||||
sortby_index=None,
|
||||
json_flag=args.json)
|
||||
|
||||
|
||||
@cliutils.arg(
|
||||
'name',
|
||||
metavar='<name>',
|
||||
help="Name or UUID of the board ")
|
||||
@cliutils.arg(
|
||||
'code',
|
||||
metavar='<code>',
|
||||
help="Codeof the board ")
|
||||
@cliutils.arg(
|
||||
'type',
|
||||
metavar='<type>',
|
||||
help="Type of the board ")
|
||||
@cliutils.arg(
|
||||
'latitude',
|
||||
metavar='<latitude>',
|
||||
help="Latitude of the board ")
|
||||
@cliutils.arg(
|
||||
'longitude',
|
||||
metavar='<longitude>',
|
||||
help="Longitude of the board ")
|
||||
@cliutils.arg(
|
||||
'altitude',
|
||||
metavar='<altitude>',
|
||||
help="Altitude of the board ")
|
||||
@cliutils.arg(
|
||||
'--mobile',
|
||||
dest='mobile',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="Set a mobile board")
|
||||
@cliutils.arg(
|
||||
'-e', '--extra',
|
||||
metavar='<key=value>',
|
||||
action='append',
|
||||
help="Record arbitrary key/value metadata. "
|
||||
"Can be specified multiple times.")
|
||||
def do_board_create(cc, args):
|
||||
"""Register a new board with the Iotronic service."""
|
||||
field_list = ['name', 'code', 'type', 'mobile', 'extra']
|
||||
|
||||
fields = dict((k, v) for (k, v) in vars(args).items()
|
||||
if k in field_list and not (v is None))
|
||||
fields = utils.args_array_to_dict(fields, 'extra')
|
||||
|
||||
fields['location'] = [
|
||||
{'latitude': args.latitude, 'longitude': args.longitude,
|
||||
'altitude': args.altitude}]
|
||||
|
||||
board = cc.board.create(**fields)
|
||||
data = dict([(f, getattr(board, f, '')) for f in
|
||||
res_fields.BOARD_DETAILED_RESOURCE.fields])
|
||||
cliutils.print_dict(data, wrap=72, json_flag=args.json)
|
||||
|
||||
|
||||
@cliutils.arg('board',
|
||||
metavar='<board>',
|
||||
nargs='+',
|
||||
help="Name or UUID of the board.")
|
||||
def do_board_delete(cc, args):
|
||||
"""Unregister board(s) from the Iotronic service.
|
||||
|
||||
Returns errors for any boards that could not be unregistered.
|
||||
"""
|
||||
|
||||
failures = []
|
||||
for n in args.board:
|
||||
try:
|
||||
cc.board.delete(n)
|
||||
print(_('Deleted board %s') % n)
|
||||
except exceptions.ClientException as e:
|
||||
failures.append(_("Failed to delete board %(board)s: %(error)s")
|
||||
% {'board': n, 'error': e})
|
||||
if failures:
|
||||
raise exceptions.ClientException("\n".join(failures))
|
||||
|
||||
|
||||
@cliutils.arg('board', metavar='<board>', help="Name or UUID of the board.")
|
||||
@cliutils.arg(
|
||||
'attributes',
|
||||
metavar='<path=value>',
|
||||
nargs='+',
|
||||
action='append',
|
||||
default=[],
|
||||
help="Values to be changed.")
|
||||
def do_board_update(cc, args):
|
||||
"""Update information about a registered board."""
|
||||
|
||||
patch = {k: v for k, v in (x.split('=') for x in args.attributes[0])}
|
||||
|
||||
board = cc.board.update(args.board, patch)
|
||||
_print_board_show(board, json=args.json)
|
63
iotronicclient/v1/client.py
Normal file
63
iotronicclient/v1/client.py
Normal file
@ -0,0 +1,63 @@
|
||||
# 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.
|
||||
|
||||
from iotronicclient.common import filecache
|
||||
from iotronicclient.common import http
|
||||
from iotronicclient.common.http import DEFAULT_VER
|
||||
from iotronicclient.common.i18n import _
|
||||
from iotronicclient import exc
|
||||
from iotronicclient.v1 import board
|
||||
from iotronicclient.v1 import plugin
|
||||
from iotronicclient.v1 import plugin_injection
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Client for the Iotronic v1 API.
|
||||
|
||||
:param string endpoint: A user-supplied endpoint URL for the iotronic
|
||||
service.
|
||||
:param function token: Provides token for authentication.
|
||||
:param integer timeout: Allows customization of the timeout for client
|
||||
http requests. (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, endpoint=None, *args, **kwargs):
|
||||
"""Initialize a new client for the Iotronic v1 API."""
|
||||
if kwargs.get('os_iotronic_api_version'):
|
||||
kwargs['api_version_select_state'] = "user"
|
||||
else:
|
||||
if not endpoint:
|
||||
raise exc.EndpointException(
|
||||
_("Must provide 'endpoint' if os_iotronic_api_version "
|
||||
"isn't specified"))
|
||||
|
||||
# If the user didn't specify a version, use a cached version if
|
||||
# one has been stored
|
||||
host, netport = http.get_server(endpoint)
|
||||
saved_version = filecache.retrieve_data(host=host, port=netport)
|
||||
if saved_version:
|
||||
kwargs['api_version_select_state'] = "cached"
|
||||
kwargs['os_iotronic_api_version'] = saved_version
|
||||
else:
|
||||
kwargs['api_version_select_state'] = "default"
|
||||
kwargs['os_iotronic_api_version'] = DEFAULT_VER
|
||||
|
||||
self.http_client = http._construct_http_client(
|
||||
endpoint, *args, **kwargs)
|
||||
|
||||
self.board = board.BoardManager(self.http_client)
|
||||
self.plugin = plugin.PluginManager(self.http_client)
|
||||
self.plugin_injection = plugin_injection.InjectionPluginManager(
|
||||
self.http_client)
|
107
iotronicclient/v1/plugin.py
Normal file
107
iotronicclient/v1/plugin.py
Normal file
@ -0,0 +1,107 @@
|
||||
# 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 iotronicclient.common import base
|
||||
from iotronicclient.common.i18n import _
|
||||
from iotronicclient.common import utils
|
||||
from iotronicclient import exc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
_DEFAULT_POLL_INTERVAL = 2
|
||||
|
||||
|
||||
class Plugin(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<Plugin %s>" % self._info
|
||||
|
||||
|
||||
class PluginManager(base.CreateManager):
|
||||
resource_class = Plugin
|
||||
_creation_attributes = ['name', 'code', 'public', 'callable', 'parameters',
|
||||
'extra']
|
||||
_resource_name = 'plugins'
|
||||
|
||||
def list(self, marker=None, limit=None,
|
||||
detail=False, sort_key=None, sort_dir=None, fields=None,
|
||||
with_public=False, all_plugins=False):
|
||||
"""Retrieve a list of plugins.
|
||||
|
||||
:param marker: Optional, the UUID of a plugin, eg the last
|
||||
plugin 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 plugins to return.
|
||||
2) limit == 0, return the entire list of plugins.
|
||||
3) limit param is NOT specified (None), the number of items
|
||||
returned respect the maximum imposed by the Iotronic API
|
||||
(see Iotronic's api.max_limit option).
|
||||
|
||||
:param detail: Optional, boolean whether to return detailed information
|
||||
about plugins.
|
||||
|
||||
:param sort_key: Optional, field used for sorting.
|
||||
|
||||
:param sort_dir: Optional, direction of sorting, either 'asc' (the
|
||||
default) or 'desc'.
|
||||
|
||||
:param fields: Optional, a list with a specified set of fields
|
||||
of the resource to be returned. Can not be used
|
||||
when 'detail' is set.
|
||||
|
||||
:param with_public: Optional boolean value to get also public plugins.
|
||||
|
||||
:param all_plugins: Optional boolean value to get all plugins.
|
||||
|
||||
:returns: A list of plugins.
|
||||
|
||||
"""
|
||||
if limit is not None:
|
||||
limit = int(limit)
|
||||
|
||||
if detail and fields:
|
||||
raise exc.InvalidAttribute(_("Can't fetch a subset of fields "
|
||||
"with 'detail' set"))
|
||||
|
||||
filters = utils.common_filters(marker, limit, sort_key, sort_dir,
|
||||
fields)
|
||||
|
||||
if with_public:
|
||||
filters.append('with_public=true')
|
||||
if all_plugins:
|
||||
filters.append('all_plugins=true')
|
||||
|
||||
path = ''
|
||||
if detail:
|
||||
path += 'detail'
|
||||
|
||||
if filters:
|
||||
path += '?' + '&'.join(filters)
|
||||
|
||||
if limit is None:
|
||||
return self._list(self._path(path), "plugins")
|
||||
else:
|
||||
return self._list_pagination(self._path(path), "plugins",
|
||||
limit=limit)
|
||||
|
||||
def get(self, plugin_id, fields=None):
|
||||
return self._get(resource_id=plugin_id, fields=fields)
|
||||
|
||||
def delete(self, plugin_id):
|
||||
return self._delete(resource_id=plugin_id)
|
||||
|
||||
def update(self, plugin_id, patch, http_method='PATCH'):
|
||||
return self._update(resource_id=plugin_id, patch=patch,
|
||||
method=http_method)
|
103
iotronicclient/v1/plugin_injection.py
Normal file
103
iotronicclient/v1/plugin_injection.py
Normal file
@ -0,0 +1,103 @@
|
||||
# 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 iotronicclient.common import base
|
||||
from iotronicclient.common.i18n import _
|
||||
from iotronicclient import exc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
_DEFAULT_POLL_INTERVAL = 2
|
||||
|
||||
|
||||
class InjectionPlugin(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<InjectionPlugin %s>" % self._info
|
||||
|
||||
|
||||
class InjectionPluginManager(base.Manager):
|
||||
resource_class = InjectionPlugin
|
||||
_resource_name = 'boards'
|
||||
|
||||
def plugin_inject(self, board_ident, plugin_ident, onboot=False):
|
||||
path = "%s/plugins" % board_ident
|
||||
body = {"plugin": plugin_ident,
|
||||
"onboot": onboot}
|
||||
|
||||
return self._update(path, body, method='PUT')
|
||||
|
||||
def plugin_remove(self, board_ident, plugin_ident):
|
||||
path = "%(board)s/plugins/%(plugin)s" % {'board': board_ident,
|
||||
'plugin': plugin_ident}
|
||||
return self._delete(resource_id=path)
|
||||
|
||||
def plugin_action(self, board_ident, plugin_ident, action, params={}):
|
||||
path = "%(board)s/plugins/%(plugin)s" % {'board': board_ident,
|
||||
'plugin': plugin_ident}
|
||||
body = {"action": action,
|
||||
"parameters": params
|
||||
}
|
||||
return self._update(path, body, method='POST')
|
||||
|
||||
def plugins_on_board(self, board_ident, marker=None, limit=None,
|
||||
detail=False, sort_key=None, sort_dir=None,
|
||||
fields=None):
|
||||
"""Retrieve a list of boards.
|
||||
|
||||
:param board_ident: the UUID or name of the board.
|
||||
|
||||
:param marker: Optional, the UUID of a board, eg the last
|
||||
board 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 boards to return.
|
||||
2) limit == 0, return the entire list of boards.
|
||||
3) limit param is NOT specified (None), the number of items
|
||||
returned respect the maximum imposed by the Iotronic API
|
||||
(see Iotronic's api.max_limit option).
|
||||
|
||||
:param detail: Optional, boolean whether to return detailed information
|
||||
about boards.
|
||||
|
||||
:param sort_key: Optional, field used for sorting.
|
||||
|
||||
:param sort_dir: Optional, direction of sorting, either 'asc' (the
|
||||
default) or 'desc'.
|
||||
|
||||
:param fields: Optional, a list with a specified set of fields
|
||||
of the resource to be returned. Can not be used
|
||||
when 'detail' is set.
|
||||
|
||||
:returns: A list of plugins injected on a board.
|
||||
|
||||
"""
|
||||
if limit is not None:
|
||||
limit = int(limit)
|
||||
|
||||
if detail and fields:
|
||||
raise exc.InvalidAttribute(_("Can't fetch a subset of fields "
|
||||
"with 'detail' set"))
|
||||
|
||||
# filters = utils.common_filters(marker, limit, sort_key, sort_dir,
|
||||
# fields)
|
||||
|
||||
path = "%s/plugins" % board_ident
|
||||
|
||||
if limit is None:
|
||||
return self._list(self._path(path), "injections")
|
||||
else:
|
||||
|
||||
return self._list_pagination(self._path(path), "injections",
|
||||
limit=limit)
|
98
iotronicclient/v1/plugin_injection_shell.py
Normal file
98
iotronicclient/v1/plugin_injection_shell.py
Normal file
@ -0,0 +1,98 @@
|
||||
# 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 iotronicclient.common.apiclient import exceptions
|
||||
from iotronicclient.common import cliutils
|
||||
from iotronicclient.common.i18n import _
|
||||
from iotronicclient.v1 import resource_fields as res_fields
|
||||
|
||||
|
||||
def _print_injected(injection, fields=None, json=False):
|
||||
if fields is None:
|
||||
fields = res_fields.PLUGIN_INJECT_RESOURCE.fields
|
||||
|
||||
data = dict(
|
||||
[(f, getattr(injection, f, '')) for f in fields])
|
||||
cliutils.print_dict(data, wrap=72, json_flag=json)
|
||||
|
||||
|
||||
@cliutils.arg('board',
|
||||
metavar='<board>',
|
||||
help="Name or UUID of the board.")
|
||||
@cliutils.arg('plugin',
|
||||
metavar='<plugin>',
|
||||
help="Name or UUID of the plugin.")
|
||||
@cliutils.arg(
|
||||
'--onboot',
|
||||
dest='onboot',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="Start the plugin on boot")
|
||||
def do_plugin_inject(cc, args):
|
||||
onboot = False
|
||||
if args.onboot:
|
||||
onboot = True
|
||||
try:
|
||||
cc.plugin_injection.plugin_inject(args.board, args.plugin, onboot)
|
||||
print(_('Injected plugin %(plugin)s from board %(board)s') % {
|
||||
'board': args.board, 'plugin': args.plugin})
|
||||
except exceptions.ClientException as e:
|
||||
exceptions.ClientException(
|
||||
"Failed to inject plugin on board %(board)s: %(error)s" % {
|
||||
'board': args.board, 'error': e})
|
||||
|
||||
|
||||
@cliutils.arg('board',
|
||||
metavar='<board>',
|
||||
help="Name or UUID of the board.")
|
||||
@cliutils.arg('plugin',
|
||||
metavar='<plugin>',
|
||||
help="Name or UUID of the plugin.")
|
||||
def do_plugin_remove(cc, args):
|
||||
try:
|
||||
cc.plugin_injection.plugin_remove(args.board, args.plugin)
|
||||
print(_('Removed plugin %(plugin)s from board %(board)s') % {
|
||||
'board': args.board, 'plugin': args.plugin})
|
||||
except exceptions.ClientException as e:
|
||||
exceptions.ClientException(
|
||||
"Failed to remove plugin from board %(board)s: %(error)s" % {
|
||||
'board': args.board, 'error': e})
|
||||
|
||||
|
||||
@cliutils.arg('board',
|
||||
metavar='<board>',
|
||||
help="Name or UUID of the board.")
|
||||
@cliutils.arg('plugin',
|
||||
metavar='<plugin>',
|
||||
help="Name or UUID of the plugin.")
|
||||
@cliutils.arg('action',
|
||||
metavar='<action>',
|
||||
help="action of the plugin.")
|
||||
def do_plugin_action(cc, args):
|
||||
result = cc.plugin_injection.plugin_action(args.board, args.plugin,
|
||||
args.action)
|
||||
print(_('%s') % result)
|
||||
|
||||
|
||||
@cliutils.arg(
|
||||
'board',
|
||||
metavar='<id>',
|
||||
help="Name or UUID of the board ")
|
||||
def do_plugins_on_board(cc, args):
|
||||
fields = res_fields.PLUGIN_INJECT_RESOURCE_ON_BOARD.fields
|
||||
field_labels = res_fields.PLUGIN_INJECT_RESOURCE_ON_BOARD.labels
|
||||
"""Show detailed information about a board."""
|
||||
list = cc.plugin_injection.plugins_on_board(args.board)
|
||||
cliutils.print_list(list, fields=fields,
|
||||
field_labels=field_labels,
|
||||
sortby_index=None,
|
||||
json_flag=args.json)
|
228
iotronicclient/v1/plugin_shell.py
Normal file
228
iotronicclient/v1/plugin_shell.py
Normal file
@ -0,0 +1,228 @@
|
||||
# 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 iotronicclient.common.apiclient import exceptions
|
||||
from iotronicclient.common import cliutils
|
||||
from iotronicclient.common.i18n import _
|
||||
from iotronicclient.common import utils
|
||||
from iotronicclient.v1 import resource_fields as res_fields
|
||||
|
||||
|
||||
def _print_plugin_show(plugin, fields=None, json=False):
|
||||
if fields is None:
|
||||
fields = res_fields.PLUGIN_DETAILED_RESOURCE.fields
|
||||
|
||||
data = dict(
|
||||
[(f, getattr(plugin, f, '')) for f in fields])
|
||||
cliutils.print_dict(data, wrap=72, json_flag=json)
|
||||
|
||||
|
||||
@cliutils.arg(
|
||||
'plugin',
|
||||
metavar='<id>',
|
||||
help="Name or UUID of the plugin ")
|
||||
@cliutils.arg(
|
||||
'--fields',
|
||||
nargs='+',
|
||||
dest='fields',
|
||||
metavar='<field>',
|
||||
action='append',
|
||||
default=[],
|
||||
help="One or more plugin fields. Only these fields will be fetched from "
|
||||
"the server.")
|
||||
def do_plugin_show(cc, args):
|
||||
"""Show detailed information about a plugin."""
|
||||
fields = args.fields[0] if args.fields else None
|
||||
utils.check_empty_arg(args.plugin, '<id>')
|
||||
utils.check_for_invalid_fields(
|
||||
fields, res_fields.PLUGIN_DETAILED_RESOURCE.fields)
|
||||
plugin = cc.plugin.get(args.plugin, fields=fields)
|
||||
_print_plugin_show(plugin, fields=fields, json=args.json)
|
||||
|
||||
|
||||
@cliutils.arg(
|
||||
'--limit',
|
||||
metavar='<limit>',
|
||||
type=int,
|
||||
help='Maximum number of plugins to return per request, '
|
||||
'0 for no limit. Default is the maximum number used '
|
||||
'by the Iotronic API Service.')
|
||||
@cliutils.arg(
|
||||
'--marker',
|
||||
metavar='<plugin>',
|
||||
help='Plugin UUID (for example, of the last plugin in the list from '
|
||||
'a previous request). Returns the list of plugins after this UUID.')
|
||||
@cliutils.arg(
|
||||
'--sort-key',
|
||||
metavar='<field>',
|
||||
help='Plugin field that will be used for sorting.')
|
||||
@cliutils.arg(
|
||||
'--sort-dir',
|
||||
metavar='<direction>',
|
||||
choices=['asc', 'desc'],
|
||||
help='Sort direction: "asc" (the default) or "desc".')
|
||||
@cliutils.arg(
|
||||
'--detail',
|
||||
dest='detail',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="Show detailed information about the plugins.")
|
||||
@cliutils.arg(
|
||||
'--with-publics',
|
||||
dest='with_public',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="with public plugins")
|
||||
@cliutils.arg(
|
||||
'--all-plugins',
|
||||
dest='all_plugins',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="all plugins")
|
||||
@cliutils.arg(
|
||||
'--fields',
|
||||
nargs='+',
|
||||
dest='fields',
|
||||
metavar='<field>',
|
||||
action='append',
|
||||
default=[],
|
||||
help="One or more plugin fields. Only these fields will be fetched from "
|
||||
"the server. Can not be used when '--detail' is specified.")
|
||||
def do_plugin_list(cc, args):
|
||||
"""List the plugins which are registered with the Iotronic service."""
|
||||
params = {}
|
||||
|
||||
if args.detail:
|
||||
fields = res_fields.PLUGIN_DETAILED_RESOURCE.fields
|
||||
field_labels = res_fields.PLUGIN_DETAILED_RESOURCE.labels
|
||||
elif args.fields:
|
||||
utils.check_for_invalid_fields(
|
||||
args.fields[0], res_fields.PLUGIN_DETAILED_RESOURCE.fields)
|
||||
resource = res_fields.Resource(args.fields[0])
|
||||
fields = resource.fields
|
||||
field_labels = resource.labels
|
||||
else:
|
||||
fields = res_fields.PLUGIN_RESOURCE.fields
|
||||
field_labels = res_fields.PLUGIN_RESOURCE.labels
|
||||
|
||||
sort_fields = res_fields.PLUGIN_DETAILED_RESOURCE.sort_fields
|
||||
sort_field_labels = res_fields.PLUGIN_DETAILED_RESOURCE.sort_labels
|
||||
|
||||
params.update(utils.common_params_for_list(args,
|
||||
sort_fields,
|
||||
sort_field_labels))
|
||||
|
||||
if args.with_public:
|
||||
params['with_public'] = args.with_public
|
||||
|
||||
if args.all_plugins:
|
||||
params['all_plugins'] = args.all_plugins
|
||||
|
||||
plugins = cc.plugin.list(**params)
|
||||
cliutils.print_list(plugins, fields,
|
||||
field_labels=field_labels,
|
||||
sortby_index=None,
|
||||
json_flag=args.json)
|
||||
|
||||
|
||||
@cliutils.arg(
|
||||
'name',
|
||||
metavar='<name>',
|
||||
help="Name or UUID of the plugin ")
|
||||
@cliutils.arg(
|
||||
'code',
|
||||
metavar='<plugin-file>',
|
||||
help="Code of the plugin")
|
||||
@cliutils.arg(
|
||||
'--callable',
|
||||
dest='callable',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="Set a callable plugin")
|
||||
@cliutils.arg(
|
||||
'--is-plublic',
|
||||
dest='public',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="Set a public plugin")
|
||||
@cliutils.arg(
|
||||
'--params',
|
||||
metavar='<parameters>',
|
||||
help="Parameters file for the plugin")
|
||||
@cliutils.arg(
|
||||
'-e', '--extra',
|
||||
metavar='<key=value>',
|
||||
action='append',
|
||||
help="Record arbitrary key/value metadata. "
|
||||
"Can be specified multiple times.")
|
||||
def do_plugin_create(cc, args):
|
||||
"""Register a new plugin with the Iotronic service."""
|
||||
|
||||
field_list = ['name', 'code', 'callable', 'public', 'extra']
|
||||
|
||||
fields = dict((k, v) for (k, v) in vars(args).items()
|
||||
if k in field_list and not (v is None))
|
||||
|
||||
fields = utils.args_array_to_dict(fields, 'extra')
|
||||
|
||||
fl = fields['code']
|
||||
with open(fl, 'r') as fil:
|
||||
fields['code'] = fil.read()
|
||||
|
||||
if args.params:
|
||||
fields['parameters'] = utils.json_from_file(args.params)
|
||||
|
||||
plugin = cc.plugin.create(**fields)
|
||||
|
||||
data = dict([(f, getattr(plugin, f, '')) for f in
|
||||
res_fields.PLUGIN_DETAILED_RESOURCE.fields])
|
||||
|
||||
cliutils.print_dict(data, wrap=72, json_flag=args.json)
|
||||
|
||||
|
||||
@cliutils.arg('plugin',
|
||||
metavar='<plugin>',
|
||||
nargs='+',
|
||||
help="Name or UUID of the plugin.")
|
||||
def do_plugin_delete(cc, args):
|
||||
"""Unregister plugin(s) from the Iotronic service.
|
||||
|
||||
Returns errors for any plugins that could not be unregistered.
|
||||
"""
|
||||
|
||||
failures = []
|
||||
for n in args.plugin:
|
||||
try:
|
||||
cc.plugin.delete(n)
|
||||
print(_('Deleted plugin %s') % n)
|
||||
except exceptions.ClientException as e:
|
||||
failures.append(_("Failed to delete plugin %(plugin)s: %(error)s")
|
||||
% {'plugin': n, 'error': e})
|
||||
if failures:
|
||||
raise exceptions.ClientException("\n".join(failures))
|
||||
|
||||
|
||||
@cliutils.arg('plugin', metavar='<plugin>', help="Name or UUID of the plugin.")
|
||||
@cliutils.arg(
|
||||
'attributes',
|
||||
metavar='<path=value>',
|
||||
nargs='+',
|
||||
action='append',
|
||||
default=[],
|
||||
help="Values to be changed.")
|
||||
def do_plugin_update(cc, args):
|
||||
"""Update information about a registered plugin."""
|
||||
|
||||
patch = {k: v for k, v in (x.split('=') for x in args.attributes[0])}
|
||||
|
||||
plugin = cc.plugin.update(args.plugin, patch)
|
||||
_print_plugin_show(plugin, json=args.json)
|
208
iotronicclient/v1/resource_fields.py
Normal file
208
iotronicclient/v1/resource_fields.py
Normal file
@ -0,0 +1,208 @@
|
||||
# Copyright 2014 Red Hat, 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 iotronicclient.common.i18n import _
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""Resource class
|
||||
|
||||
This class is used to manage the various fields that a resource (e.g.
|
||||
Chassis, Board, Port) contains. An individual field consists of a
|
||||
'field_id' (key) and a 'label' (value). The caller only provides the
|
||||
'field_ids' when instantiating the object.
|
||||
|
||||
Ordering of the 'field_ids' will be preserved as specified by the caller.
|
||||
|
||||
It also provides the ability to exclude some of these fields when they are
|
||||
being used for sorting.
|
||||
"""
|
||||
|
||||
FIELDS = {
|
||||
'name': 'Name',
|
||||
'project': 'Project',
|
||||
'uuid': 'UUID',
|
||||
'extra': 'Extra',
|
||||
'updated_at': 'Updated At',
|
||||
'id': 'ID',
|
||||
'created_at': 'Created At',
|
||||
'status': 'Status',
|
||||
'code': 'Code',
|
||||
'mobile': 'Mobile',
|
||||
'session': 'Session',
|
||||
'location': 'Location',
|
||||
'owner': 'Owner',
|
||||
'type': 'Type',
|
||||
'callable': 'Callable',
|
||||
'public': 'Public',
|
||||
'onboot': 'On Boot',
|
||||
'board_uuid': 'Board uuid',
|
||||
'plugin_uuid': 'Plugin uuid',
|
||||
'plugin': 'Plugin',
|
||||
'parameters': 'Parameters',
|
||||
|
||||
#
|
||||
# 'address': 'Address',
|
||||
# 'async': 'Async',
|
||||
# 'attach': 'Response is attachment',
|
||||
# 'chassis_uuid': 'Chassis UUID',
|
||||
# 'clean_step': 'Clean Step',
|
||||
# 'console_enabled': 'Console Enabled',
|
||||
# 'description': 'Description',
|
||||
# 'http_methods': 'Supported HTTP methods',
|
||||
# 'inspection_finished_at': 'Inspection Finished At',
|
||||
# 'inspection_started_at': 'Inspection Started At',
|
||||
# 'instance_info': 'Instance Info',
|
||||
# 'instance_uuid': 'Instance UUID',
|
||||
# 'internal_info': 'Internal Info',
|
||||
# 'last_error': 'Last Error',
|
||||
# 'maintenance': 'Maintenance',
|
||||
# 'maintenance_reason': 'Maintenance Reason',
|
||||
# 'mode': 'Mode',
|
||||
# 'power_state': 'Power State',
|
||||
# 'properties': 'Properties',
|
||||
# 'provision_state': 'Provisioning State',
|
||||
# 'provision_updated_at': 'Provision Updated At',
|
||||
# 'raid_config': 'Current RAID configuration',
|
||||
# 'reservation': 'Reservation',
|
||||
# 'resource_class': 'Resource Class',
|
||||
# 'target_power_state': 'Target Power State',
|
||||
# 'target_provision_state': 'Target Provision State',
|
||||
# 'target_raid_config': 'Target RAID configuration',
|
||||
# 'local_link_connection': 'Local Link Connection',
|
||||
# 'pxe_enabled': 'PXE boot enabled',
|
||||
# 'portgroup_uuid': 'Portgroup UUID',
|
||||
# 'boot_interface': 'Boot Interface',
|
||||
# 'console_interface': 'Console Interface',
|
||||
# 'deploy_interface': 'Deploy Interface',
|
||||
# 'inspect_interface': 'Inspect Interface',
|
||||
# 'management_interface': 'Management Interface',
|
||||
# 'network_interface': 'Network Interface',
|
||||
# 'power_interface': 'Power Interface',
|
||||
# 'raid_interface': 'RAID Interface',
|
||||
# 'vendor_interface': 'Vendor Interface',
|
||||
# 'standalone_ports_supported': 'Standalone Ports Supported',
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, field_ids, sort_excluded=None):
|
||||
"""Create a Resource object
|
||||
|
||||
:param field_ids: A list of strings that the Resource object will
|
||||
contain. Each string must match an existing key in
|
||||
FIELDS.
|
||||
:param sort_excluded: Optional. A list of strings that will not be used
|
||||
for sorting. Must be a subset of 'field_ids'.
|
||||
|
||||
:raises: ValueError if sort_excluded contains value not in field_ids
|
||||
"""
|
||||
self._fields = tuple(field_ids)
|
||||
self._labels = tuple([self.FIELDS[x] for x in field_ids])
|
||||
if sort_excluded is None:
|
||||
sort_excluded = []
|
||||
not_existing = set(sort_excluded) - set(field_ids)
|
||||
if not_existing:
|
||||
raise ValueError(
|
||||
_("sort_excluded specified with value not contained in "
|
||||
"field_ids. Unknown value(s): %s") % ','.join(not_existing))
|
||||
self._sort_fields = tuple(
|
||||
[x for x in field_ids if x not in sort_excluded])
|
||||
self._sort_labels = tuple([self.FIELDS[x] for x in self._sort_fields])
|
||||
|
||||
@property
|
||||
def fields(self):
|
||||
return self._fields
|
||||
|
||||
@property
|
||||
def labels(self):
|
||||
return self._labels
|
||||
|
||||
@property
|
||||
def sort_fields(self):
|
||||
return self._sort_fields
|
||||
|
||||
@property
|
||||
def sort_labels(self):
|
||||
return self._sort_labels
|
||||
|
||||
|
||||
# Boards
|
||||
BOARD_DETAILED_RESOURCE = Resource(
|
||||
[
|
||||
'uuid',
|
||||
'name',
|
||||
'type',
|
||||
'status',
|
||||
'code',
|
||||
'session',
|
||||
'mobile',
|
||||
'extra',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'location',
|
||||
'project',
|
||||
'owner',
|
||||
|
||||
],
|
||||
sort_excluded=[
|
||||
'extra', 'location', 'session',
|
||||
])
|
||||
BOARD_RESOURCE = Resource(
|
||||
['uuid',
|
||||
'name',
|
||||
'type',
|
||||
'status',
|
||||
'session',
|
||||
])
|
||||
|
||||
# Plugins
|
||||
PLUGIN_DETAILED_RESOURCE = Resource(
|
||||
['uuid',
|
||||
'name',
|
||||
'owner',
|
||||
'code',
|
||||
'public',
|
||||
'callable',
|
||||
'extra'
|
||||
|
||||
],
|
||||
sort_excluded=[
|
||||
'extra', 'code',
|
||||
])
|
||||
PLUGIN_RESOURCE = Resource(
|
||||
['uuid',
|
||||
'name',
|
||||
'owner',
|
||||
'public',
|
||||
'callable',
|
||||
])
|
||||
|
||||
PLUGIN_INJECT_RESOURCE_ON_BOARD = Resource(
|
||||
[
|
||||
'plugin',
|
||||
'status',
|
||||
'onboot',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])
|
||||
|
||||
PLUGIN_INJECT_RESOURCE = Resource(
|
||||
['board_uuid',
|
||||
'plugin_uuid',
|
||||
'status',
|
||||
'onboot',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
])
|
38
iotronicclient/v1/shell.py
Normal file
38
iotronicclient/v1/shell.py
Normal file
@ -0,0 +1,38 @@
|
||||
# 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 iotronicclient.common import utils
|
||||
from iotronicclient.v1 import board_shell
|
||||
from iotronicclient.v1 import plugin_injection_shell
|
||||
from iotronicclient.v1 import plugin_shell
|
||||
|
||||
COMMAND_MODULES = [
|
||||
board_shell,
|
||||
plugin_shell,
|
||||
plugin_injection_shell,
|
||||
]
|
||||
|
||||
|
||||
def enhance_parser(parser, subparsers, cmd_mapper):
|
||||
"""Enhance parser with API version specific options.
|
||||
|
||||
Take a basic (nonversioned) parser and enhance it with
|
||||
commands and options specific for this version of API.
|
||||
|
||||
:param parser: top level parser
|
||||
:param subparsers: top level parser's subparsers collection
|
||||
where subcommands will go
|
||||
"""
|
||||
for command_module in COMMAND_MODULES:
|
||||
utils.define_commands_from_module(subparsers, command_module,
|
||||
cmd_mapper)
|
48
iotronicclient/v1/utils.py
Normal file
48
iotronicclient/v1/utils.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Copyright 2016 Intel Corporation
|
||||
# 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.
|
||||
|
||||
HTTP_METHODS = ['POST', 'PUT', 'GET', 'DELETE', 'PATCH']
|
||||
|
||||
BOOT_DEVICES = ['pxe', 'disk', 'cdrom', 'bios', 'safe']
|
||||
|
||||
# Polling intervals in seconds.
|
||||
_LONG_ACTION_POLL_INTERVAL = 10
|
||||
_SHORT_ACTION_POLL_INTERVAL = 2
|
||||
# This dict acts as both list of possible provision actions and arguments for
|
||||
# wait_for_provision_state invocation.
|
||||
PROVISION_ACTIONS = {
|
||||
'active': {'expected_state': 'active',
|
||||
'poll_interval': _LONG_ACTION_POLL_INTERVAL},
|
||||
'deleted': {'expected_state': 'available',
|
||||
'poll_interval': _LONG_ACTION_POLL_INTERVAL},
|
||||
'rebuild': {'expected_state': 'active',
|
||||
'poll_interval': _LONG_ACTION_POLL_INTERVAL},
|
||||
'inspect': {'expected_state': 'manageable',
|
||||
# This is suboptimal for in-band inspection, but it's probably
|
||||
# not worth making people wait 10 seconds for OOB inspection
|
||||
'poll_interval': _SHORT_ACTION_POLL_INTERVAL},
|
||||
'provide': {'expected_state': 'available',
|
||||
# This assumes cleaning is in place
|
||||
'poll_interval': _LONG_ACTION_POLL_INTERVAL},
|
||||
'manage': {'expected_state': 'manageable',
|
||||
'poll_interval': _SHORT_ACTION_POLL_INTERVAL},
|
||||
'clean': {'expected_state': 'manageable',
|
||||
'poll_interval': _LONG_ACTION_POLL_INTERVAL},
|
||||
'adopt': {'expected_state': 'active',
|
||||
'poll_interval': _SHORT_ACTION_POLL_INTERVAL},
|
||||
'abort': None, # no support for --wait in abort
|
||||
}
|
||||
|
||||
PROVISION_STATES = list(PROVISION_ACTIONS)
|
0
releasenotes/notes/.placeholder
Normal file
0
releasenotes/notes/.placeholder
Normal file
0
releasenotes/source/_static/.placeholder
Normal file
0
releasenotes/source/_static/.placeholder
Normal file
0
releasenotes/source/_templates/.placeholder
Normal file
0
releasenotes/source/_templates/.placeholder
Normal file
275
releasenotes/source/conf.py
Normal file
275
releasenotes/source/conf.py
Normal file
@ -0,0 +1,275 @@
|
||||
# -*- 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.
|
||||
|
||||
# Glance Release Notes documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue Nov 3 17:40:50 2015.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'oslosphinx',
|
||||
'reno.sphinxext',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'iotronicclient Release Notes'
|
||||
copyright = u'2016, OpenStack Foundation'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = ''
|
||||
# The short X.Y version.
|
||||
version = ''
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
# language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
# html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'GlanceReleaseNotesdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# 'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'GlanceReleaseNotes.tex', u'Glance Release Notes Documentation',
|
||||
u'Glance Developers', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'glancereleasenotes', u'Glance Release Notes Documentation',
|
||||
[u'Glance Developers'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'GlanceReleaseNotes', u'Glance Release Notes Documentation',
|
||||
u'Glance Developers', 'GlanceReleaseNotes',
|
||||
'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/']
|
8
releasenotes/source/index.rst
Normal file
8
releasenotes/source/index.rst
Normal file
@ -0,0 +1,8 @@
|
||||
============================================
|
||||
iotronicclient Release Notes
|
||||
============================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
unreleased
|
5
releasenotes/source/unreleased.rst
Normal file
5
releasenotes/source/unreleased.rst
Normal file
@ -0,0 +1,5 @@
|
||||
==============================
|
||||
Current Series Release Notes
|
||||
==============================
|
||||
|
||||
.. release-notes::
|
17
requirements.txt
Normal file
17
requirements.txt
Normal file
@ -0,0 +1,17 @@
|
||||
# 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.0.0 # Apache-2.0
|
||||
appdirs>=1.3.0 # MIT License
|
||||
dogpile.cache>=0.6.2 # BSD
|
||||
jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT
|
||||
keystoneauth1>=2.18.0 # Apache-2.0
|
||||
osc-lib>=1.2.0 # Apache-2.0
|
||||
oslo.i18n>=2.1.0 # Apache-2.0
|
||||
oslo.serialization>=1.10.0 # Apache-2.0
|
||||
oslo.utils>=3.20.0 # Apache-2.0
|
||||
PrettyTable<0.8,>=0.7.1 # BSD
|
||||
python-openstackclient>=3.3.0 # Apache-2.0
|
||||
PyYAML>=3.10.0 # MIT
|
||||
requests!=2.12.2,!=2.13.0,>=2.10.0 # Apache-2.0
|
||||
six>=1.9.0 # MIT
|
37
setup.cfg
Normal file
37
setup.cfg
Normal file
@ -0,0 +1,37 @@
|
||||
[metadata]
|
||||
name = python-iotronicclient
|
||||
summary = Iotronic Client
|
||||
description-file =
|
||||
README.rst
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
home-page = http://www.openstack.org/
|
||||
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 :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.3
|
||||
Programming Language :: Python :: 3.4
|
||||
|
||||
[files]
|
||||
packages =
|
||||
iotronicclient
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
iotronic = iotronicclient.shell:main
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
all_files = 1
|
||||
|
||||
[pbr]
|
||||
autodoc_index_modules = True
|
||||
warnerrors = True
|
29
setup.py
Normal file
29
setup.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
||||
import setuptools
|
||||
|
||||
# In python < 2.7.4, a lazy loading of package `pbr` will break
|
||||
# setuptools if some other modules registered functions in `atexit`.
|
||||
# solution from: http://bugs.python.org/issue15881#msg170215
|
||||
try:
|
||||
import multiprocessing # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr>=1.8'],
|
||||
pbr=True)
|
17
test-requirements.txt
Normal file
17
test-requirements.txt
Normal file
@ -0,0 +1,17 @@
|
||||
# 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>=0.12.0,!=0.13.0,<0.14 # Apache-2.0
|
||||
|
||||
coverage>=4.0 # Apache-2.0
|
||||
python-subunit>=0.0.18 # Apache-2.0/BSD
|
||||
sphinx>=1.5.1 # BSD
|
||||
oslosphinx>=4.7.0 # Apache-2.0
|
||||
oslotest>=1.10.0 # Apache-2.0
|
||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
testscenarios>=0.4 # Apache-2.0/BSD
|
||||
testtools>=1.4.0 # MIT
|
||||
|
||||
# releasenotes
|
||||
reno>=1.8.0 # Apache-2.0
|
41
tox.ini
Normal file
41
tox.ini
Normal file
@ -0,0 +1,41 @@
|
||||
[tox]
|
||||
minversion = 2.0
|
||||
envlist = py35,py34,py27,pypy,pep8
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
|
||||
setenv =
|
||||
VIRTUAL_ENV={envdir}
|
||||
PYTHONWARNINGS=default::DeprecationWarning
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
commands =
|
||||
find . -type f -name "*.pyc" -delete
|
||||
|
||||
[testenv:pep8]
|
||||
commands = flake8 {posargs}
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands = python setup.py test --coverage --testr-args='{posargs}'
|
||||
|
||||
[testenv:docs]
|
||||
commands = python setup.py build_sphinx
|
||||
|
||||
[testenv:releasenotes]
|
||||
commands =
|
||||
sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
||||
|
||||
[testenv:debug]
|
||||
commands = oslo_debug_helper {posargs}
|
||||
|
||||
[flake8]
|
||||
# E123, E125 skipped as they are invalid PEP-8.
|
||||
|
||||
show-source = True
|
||||
ignore = E123,E125
|
||||
builtins = _
|
||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
|
Loading…
x
Reference in New Issue
Block a user