Glare client code
Taken from https://github.com/dshakhray/python-glareclient Change-Id: If0e7e5cd0e39281f725df21308a18ed6caa8a009
This commit is contained in:

committed by
Mike Fedosin

parent
70f051e6a4
commit
6005eb8b3b
7
.coveragerc
Normal file
7
.coveragerc
Normal file
@@ -0,0 +1,7 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = glareclient
|
||||
omit = glareclient/openstack/*
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
.coverage
|
||||
subunit.log
|
||||
.venv
|
||||
*,cover
|
||||
cover
|
||||
*.pyc
|
||||
.idea
|
||||
*.sw?
|
||||
*~
|
||||
AUTHORS
|
||||
build
|
||||
dist
|
||||
python_glareclient.egg-info
|
||||
ChangeLog
|
||||
run_tests.err.log
|
||||
.testrepository
|
||||
.tox
|
||||
doc/source/api
|
||||
doc/build
|
||||
*.egg
|
||||
.eggs/*
|
||||
glareclient/versioninfo
|
||||
# Files created by releasenotes build
|
||||
releasenotes/build
|
4
.testr.conf
Normal file
4
.testr.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
[DEFAULT]
|
||||
test_command=${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./glareclient/tests/unit} $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
16
CONTRIBUTING.rst
Normal file
16
CONTRIBUTING.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
If you would like to contribute to the development of OpenStack,
|
||||
you must follow the steps documented at:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
Once those steps have been completed, changes to OpenStack
|
||||
should be submitted for review via the Gerrit tool, following
|
||||
the workflow documented at:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
Pull requests submitted through GitHub will be ignored.
|
||||
|
||||
Bugs should be filed on Launchpad, not GitHub:
|
||||
|
||||
https://bugs.launchpad.net/python-glareclient
|
12
HACKING.rst
Normal file
12
HACKING.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
Glare Style Commandments
|
||||
========================
|
||||
|
||||
- Step 1: Read the OpenStack Style Commandments
|
||||
http://docs.openstack.org/developer/hacking/
|
||||
- Step 2: Read on
|
||||
|
||||
|
||||
Glare Specific Commandments
|
||||
---------------------------
|
||||
|
||||
None so far
|
175
LICENSE
Normal file
175
LICENSE
Normal file
@@ -0,0 +1,175 @@
|
||||
|
||||
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.
|
4
README.rst
Normal file
4
README.rst
Normal file
@@ -0,0 +1,4 @@
|
||||
Python bindings to the Glare Artifact Repository
|
||||
================================================
|
||||
|
||||
|
126
doc/source/conf.py
Normal file
126
doc/source/conf.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# Copyright 2015 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.
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'..', '..')))
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", ".."))
|
||||
|
||||
|
||||
def gen_ref(ver, title, names):
|
||||
refdir = os.path.join(BASE_DIR, "ref")
|
||||
pkg = "glareclient"
|
||||
if ver:
|
||||
pkg = "%s.%s" % (pkg, ver)
|
||||
refdir = os.path.join(refdir, ver)
|
||||
if not os.path.exists(refdir):
|
||||
os.makedirs(refdir)
|
||||
idxpath = os.path.join(refdir, "index.rst")
|
||||
with open(idxpath, "w") as idx:
|
||||
idx.write(("%(title)s\n"
|
||||
"%(signs)s\n"
|
||||
"\n"
|
||||
".. toctree::\n"
|
||||
" :maxdepth: 1\n"
|
||||
"\n") % {"title": title, "signs": "=" * len(title)})
|
||||
for name in names:
|
||||
idx.write(" %s\n" % name)
|
||||
rstpath = os.path.join(refdir, "%s.rst" % name)
|
||||
with open(rstpath, "w") as rst:
|
||||
rst.write(("%(title)s\n"
|
||||
"%(signs)s\n"
|
||||
"\n"
|
||||
".. automodule:: %(pkg)s.%(name)s\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n"
|
||||
" :noindex:\n")
|
||||
% {"title": name.capitalize(),
|
||||
"signs": "=" * len(name),
|
||||
"pkg": pkg, "name": name})
|
||||
|
||||
gen_ref(None, "API", ["client", "exc"])
|
||||
gen_ref("v1", "Glare Artifact Repository Version 1 Client Reference",
|
||||
["client", "artifacts"])
|
||||
|
||||
# -- 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', '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
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'python-glareclient'
|
||||
copyright = u'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'
|
||||
|
||||
# Grouping the document tree for man pages.
|
||||
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
|
||||
|
||||
man_pages = [
|
||||
('man/glare', 'glare', u'Client for Glare Artifact Repository',
|
||||
[u'OpenStack Foundation'], 1),
|
||||
]
|
||||
# -- 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 = 'nature'
|
||||
|
||||
# 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'
|
||||
),
|
||||
]
|
27
doc/source/index.rst
Normal file
27
doc/source/index.rst
Normal file
@@ -0,0 +1,27 @@
|
||||
Python Bindings for the Glare Artifact Repository
|
||||
=================================================
|
||||
|
||||
This is a client for the Glare Artifact Repository. There's :doc:`a Python API <ref/index>` (the :mod:`glareclient` module) and a :doc:`command-line script<man/glare>` (installed as :program:`glare`).
|
||||
|
||||
Python API
|
||||
----------
|
||||
|
||||
|
||||
Python API Reference
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
Command-line Tool Reference
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
man/glare
|
||||
|
||||
Command-line Tool
|
||||
-----------------
|
||||
|
||||
|
||||
Release Notes
|
||||
=============
|
||||
|
42
doc/source/man/glare.rst
Normal file
42
doc/source/man/glare.rst
Normal file
@@ -0,0 +1,42 @@
|
||||
=============================
|
||||
:program:`glare` CLI man page
|
||||
=============================
|
||||
|
||||
.. program:: glare
|
||||
.. highlight:: bash
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
|
||||
:program:`glare` [options] <command> [command-options]
|
||||
|
||||
:program:`glare help`
|
||||
|
||||
:program:`glare help` <command>
|
||||
|
||||
|
||||
DESCRIPTION
|
||||
===========
|
||||
|
||||
|
||||
OPTIONS
|
||||
=======
|
||||
|
||||
To get a list of available commands and options run::
|
||||
|
||||
glare help
|
||||
|
||||
To get usage and options of a command::
|
||||
|
||||
glare help <command>
|
||||
|
||||
|
||||
EXAMPLES
|
||||
========
|
||||
|
||||
|
||||
BUGS
|
||||
====
|
||||
|
||||
Glare client is hosted in Launchpad so you can view current bugs at
|
||||
https://bugs.launchpad.net/python-glareclient/.
|
31
glareclient/__init__.py
Normal file
31
glareclient/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
# NOTE(bcwaldon): this try/except block is needed to run setup.py due to
|
||||
# its need to import local code before installing required dependencies
|
||||
try:
|
||||
import glareclient.client
|
||||
Client = glareclient.client.Client
|
||||
except ImportError:
|
||||
import warnings
|
||||
warnings.warn("Could not import glareclient.client", ImportWarning)
|
||||
|
||||
import pbr.version
|
||||
|
||||
version_info = pbr.version.VersionInfo('python-glareclient')
|
||||
|
||||
try:
|
||||
__version__ = version_info.version_string()
|
||||
except AttributeError:
|
||||
__version__ = None
|
31
glareclient/_i18n.py
Normal file
31
glareclient/_i18n.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright 2012 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.
|
||||
import oslo_i18n as i18n
|
||||
|
||||
|
||||
_translators = i18n.TranslatorFactory(domain='glareclient')
|
||||
|
||||
# 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
|
61
glareclient/client.py
Normal file
61
glareclient/client.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Copyright 2012 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.
|
||||
|
||||
import warnings
|
||||
|
||||
from glareclient.common import utils
|
||||
|
||||
|
||||
def Client(version=None, endpoint=None, session=None, *args, **kwargs):
|
||||
"""Client for the Glare Artifact Repository.
|
||||
|
||||
Generic client for the Glare Artifact Repository. See version classes
|
||||
for specific details.
|
||||
|
||||
:param string version: The version of API to use.
|
||||
:param session: A keystoneauth1 session that should be used for transport.
|
||||
:type session: keystoneauth1.session.Session
|
||||
"""
|
||||
# FIXME(jamielennox): Add a deprecation warning if no session is passed.
|
||||
# Leaving it as an option until we can ensure nothing break when we switch.
|
||||
if session:
|
||||
if endpoint:
|
||||
kwargs.setdefault('endpoint_override', endpoint)
|
||||
|
||||
if not version:
|
||||
__, version = utils.strip_version(endpoint)
|
||||
|
||||
if not version:
|
||||
msg = ("You must provide a client version when using session")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
else:
|
||||
if version is not None:
|
||||
warnings.warn(("`version` keyword is being deprecated. Please pass"
|
||||
" the version as part of the URL. "
|
||||
"http://$HOST:$PORT/v$VERSION_NUMBER"),
|
||||
DeprecationWarning)
|
||||
|
||||
endpoint, url_version = utils.strip_version(endpoint)
|
||||
version = version or url_version
|
||||
|
||||
if not version:
|
||||
msg = ("Please provide either the version or an url with the form "
|
||||
"http://$HOST:$PORT/v$VERSION_NUMBER")
|
||||
raise RuntimeError(msg)
|
||||
|
||||
module = utils.import_versioned_module(int(version), 'client')
|
||||
client_class = getattr(module, 'Client')
|
||||
return client_class(endpoint, *args, session=session, **kwargs)
|
0
glareclient/common/__init__.py
Normal file
0
glareclient/common/__init__.py
Normal file
35
glareclient/common/base.py
Normal file
35
glareclient/common/base.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Copyright 2012 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.
|
||||
|
||||
DEPRECATED post v.0.12.0. Use 'glareclient.openstack.common.apiclient.base'
|
||||
instead of this module."
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from glareclient.openstack.common.apiclient import base
|
||||
|
||||
|
||||
warnings.warn("The 'glareclient.common.base' module is deprecated post "
|
||||
"v.0.12.0. Use 'glareclient.openstack.common.apiclient.base' "
|
||||
"instead of this one.", DeprecationWarning)
|
||||
|
||||
|
||||
getid = base.getid
|
||||
Manager = base.ManagerWithFind
|
||||
Resource = base.Resource
|
3
glareclient/common/exceptions.py
Normal file
3
glareclient/common/exceptions.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# This is here for compatibility purposes. Once all known OpenStack clients
|
||||
# are updated to use glareclient.exc, this file should be removed
|
||||
from glareclient.exc import * # noqa
|
351
glareclient/common/http.py
Normal file
351
glareclient/common/http.py
Normal file
@@ -0,0 +1,351 @@
|
||||
# Copyright 2012 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.
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from keystoneauth1 import adapter
|
||||
from keystoneauth1 import exceptions as ksa_exc
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import netutils
|
||||
import requests
|
||||
import six
|
||||
import warnings
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
|
||||
from glareclient.common import utils
|
||||
from glareclient import exc
|
||||
|
||||
osprofiler_web = importutils.try_import("osprofiler.web")
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
USER_AGENT = 'python-glareclient'
|
||||
CHUNKSIZE = 1024 * 64 # 64kB
|
||||
|
||||
|
||||
def encode_headers(headers):
|
||||
"""Encodes headers.
|
||||
|
||||
Note: This should be used right before
|
||||
sending anything out.
|
||||
|
||||
:param headers: Headers to encode
|
||||
:returns: Dictionary with encoded headers'
|
||||
names and values
|
||||
"""
|
||||
return dict((encodeutils.safe_encode(h), encodeutils.safe_encode(v))
|
||||
for h, v in six.iteritems(headers) if v is not None)
|
||||
|
||||
|
||||
class _BaseHTTPClient(object):
|
||||
|
||||
@staticmethod
|
||||
def _chunk_body(body):
|
||||
chunk = body
|
||||
while chunk:
|
||||
chunk = body.read(CHUNKSIZE)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
|
||||
def _set_common_request_kwargs(self, headers, kwargs):
|
||||
"""Handle the common parameters used to send the request."""
|
||||
|
||||
# Default Content-Type is octet-stream
|
||||
content_type = headers.get('Content-Type', 'application/octet-stream')
|
||||
|
||||
# NOTE(jamielennox): remove this later. Managers should pass json= if
|
||||
# they want to send json data.
|
||||
data = kwargs.pop("data", None)
|
||||
if data is not None and not isinstance(data, six.string_types):
|
||||
try:
|
||||
data = json.dumps(data)
|
||||
content_type = 'application/json'
|
||||
except TypeError:
|
||||
# Here we assume it's
|
||||
# a file-like object
|
||||
# and we'll chunk it
|
||||
data = self._chunk_body(data)
|
||||
|
||||
headers['Content-Type'] = content_type
|
||||
kwargs['stream'] = content_type == 'application/octet-stream'
|
||||
return data
|
||||
|
||||
def _handle_response(self, resp):
|
||||
# log request-id for each api cal
|
||||
request_id = resp.headers.get('x-openstack-request-id')
|
||||
if request_id:
|
||||
LOG.debug('%(method)s call to glare-api for '
|
||||
'%(url)s used request id '
|
||||
'%(response_request_id)s',
|
||||
{'method': resp.request.method,
|
||||
'url': resp.url,
|
||||
'response_request_id': request_id})
|
||||
|
||||
if not resp.ok:
|
||||
LOG.debug("Request returned failure status %s.", resp.status_code)
|
||||
raise exc.from_response(resp, resp.content)
|
||||
elif (resp.status_code == requests.codes.MULTIPLE_CHOICES and
|
||||
resp.request.path_url != '/versions'):
|
||||
# NOTE(flaper87): Eventually, we'll remove the check on `versions`
|
||||
# which is a bug (1491350) on the server.
|
||||
raise exc.from_response(resp)
|
||||
|
||||
content_type = resp.headers.get('Content-Type')
|
||||
|
||||
if not content_type:
|
||||
body_iter = six.StringIO(resp.text)
|
||||
try:
|
||||
body_iter = json.loads(''.join([c for c in body_iter]))
|
||||
except ValueError:
|
||||
body_iter = None
|
||||
elif content_type.startswith('application/json'):
|
||||
# Let's use requests json method, it should take care of
|
||||
# response encoding
|
||||
body_iter = resp.json()
|
||||
else:
|
||||
# Do not read all response in memory when downloading a blob.
|
||||
body_iter = _close_after_stream(resp, CHUNKSIZE)
|
||||
return resp, body_iter
|
||||
|
||||
|
||||
class HTTPClient(_BaseHTTPClient):
|
||||
|
||||
def __init__(self, endpoint, **kwargs):
|
||||
self.endpoint = endpoint
|
||||
self.identity_headers = kwargs.get('identity_headers')
|
||||
self.auth_token = kwargs.get('token')
|
||||
self.language_header = kwargs.get('language_header')
|
||||
self.last_request_id = None
|
||||
if self.identity_headers:
|
||||
if self.identity_headers.get('X-Auth-Token'):
|
||||
self.auth_token = self.identity_headers.get('X-Auth-Token')
|
||||
del self.identity_headers['X-Auth-Token']
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.headers["User-Agent"] = USER_AGENT
|
||||
|
||||
if self.language_header:
|
||||
self.session.headers["Accept-Language"] = self.language_header
|
||||
|
||||
self.timeout = float(kwargs.get('timeout', 600))
|
||||
|
||||
if self.endpoint.startswith("https"):
|
||||
compression = kwargs.get('ssl_compression', True)
|
||||
|
||||
if compression is False:
|
||||
# Note: This is not seen by default. (python must be
|
||||
# run with -Wd)
|
||||
warnings.warn('The "ssl_compression" argument has been '
|
||||
'deprecated.', DeprecationWarning)
|
||||
|
||||
if kwargs.get('insecure', False) is True:
|
||||
self.session.verify = False
|
||||
else:
|
||||
if kwargs.get('cacert', None) is not '':
|
||||
self.session.verify = kwargs.get('cacert', True)
|
||||
|
||||
self.session.cert = (kwargs.get('cert_file'),
|
||||
kwargs.get('key_file'))
|
||||
|
||||
@staticmethod
|
||||
def parse_endpoint(endpoint):
|
||||
return netutils.urlsplit(endpoint)
|
||||
|
||||
def log_curl_request(self, method, url, headers, data, kwargs):
|
||||
curl = ['curl -g -i -X %s' % method]
|
||||
|
||||
headers = copy.deepcopy(headers)
|
||||
headers.update(self.session.headers)
|
||||
|
||||
for (key, value) in six.iteritems(headers):
|
||||
header = '-H \'%s: %s\'' % utils.safe_header(key, value)
|
||||
curl.append(header)
|
||||
|
||||
if not self.session.verify:
|
||||
curl.append('-k')
|
||||
else:
|
||||
if isinstance(self.session.verify, six.string_types):
|
||||
curl.append(' --cacert %s' % self.session.verify)
|
||||
|
||||
if self.session.cert:
|
||||
curl.append(' --cert %s --key %s' % self.session.cert)
|
||||
|
||||
if data and isinstance(data, six.string_types):
|
||||
curl.append('-d \'%s\'' % data)
|
||||
|
||||
curl.append(url)
|
||||
|
||||
msg = ' '.join([encodeutils.safe_decode(item, errors='ignore')
|
||||
for item in curl])
|
||||
LOG.debug(msg)
|
||||
|
||||
@staticmethod
|
||||
def log_http_response(resp):
|
||||
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
|
||||
dump = ['\nHTTP/%.1f %s %s' % status]
|
||||
headers = resp.headers.items()
|
||||
dump.extend(['%s: %s' % utils.safe_header(k, v) for k, v in headers])
|
||||
dump.append('')
|
||||
content_type = resp.headers.get('Content-Type')
|
||||
|
||||
if content_type != 'application/octet-stream':
|
||||
dump.extend([resp.text, ''])
|
||||
LOG.debug('\n'.join([encodeutils.safe_decode(x, errors='ignore')
|
||||
for x in dump]))
|
||||
|
||||
def _request(self, method, url, **kwargs):
|
||||
"""Send an http request with the specified characteristics.
|
||||
|
||||
Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
|
||||
as setting headers and error handling.
|
||||
"""
|
||||
# Copy the kwargs so we can reuse the original in case of redirects
|
||||
headers = copy.deepcopy(kwargs.pop('headers', {}))
|
||||
|
||||
if self.identity_headers:
|
||||
for k, v in six.iteritems(self.identity_headers):
|
||||
headers.setdefault(k, v)
|
||||
|
||||
data = self._set_common_request_kwargs(headers, kwargs)
|
||||
|
||||
# add identity header to the request
|
||||
if not headers.get('X-Auth-Token'):
|
||||
headers['X-Auth-Token'] = self.auth_token
|
||||
|
||||
if osprofiler_web:
|
||||
headers.update(osprofiler_web.get_trace_id_headers())
|
||||
|
||||
# Note(flaper87): Before letting headers / url fly,
|
||||
# they should be encoded otherwise httplib will
|
||||
# complain.
|
||||
headers = encode_headers(headers)
|
||||
|
||||
if self.endpoint.endswith("/") or url.startswith("/"):
|
||||
conn_url = "%s%s" % (self.endpoint, url)
|
||||
else:
|
||||
conn_url = "%s/%s" % (self.endpoint, url)
|
||||
self.log_curl_request(method, conn_url, headers, data, kwargs)
|
||||
|
||||
try:
|
||||
resp = self.session.request(method,
|
||||
conn_url,
|
||||
data=data,
|
||||
headers=headers,
|
||||
**kwargs)
|
||||
except requests.exceptions.Timeout as e:
|
||||
message = ("Error communicating with %(url)s: %(e)s" %
|
||||
dict(url=conn_url, e=e))
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
message = ("Error finding address for %(url)s: %(e)s" %
|
||||
dict(url=conn_url, e=e))
|
||||
raise exc.CommunicationError(message=message)
|
||||
except socket.gaierror as e:
|
||||
message = "Error finding address for %s: %s" % (
|
||||
self.endpoint_hostname, e)
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
except (socket.error, socket.timeout) as e:
|
||||
endpoint = self.endpoint
|
||||
message = ("Error communicating with %(endpoint)s %(e)s" %
|
||||
{'endpoint': endpoint, 'e': e})
|
||||
raise exc.CommunicationError(message=message)
|
||||
|
||||
self.last_request_id = resp.headers.get('x-openstack-request-id')
|
||||
resp, body_iter = self._handle_response(resp)
|
||||
self.log_http_response(resp)
|
||||
return resp, body_iter
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
return self._request('HEAD', url, **kwargs)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._request('GET', url, **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._request('POST', url, **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._request('PUT', url, **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self._request('PATCH', url, **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._request('DELETE', url, **kwargs)
|
||||
|
||||
|
||||
def _close_after_stream(response, chunk_size):
|
||||
"""Iterate over the content and ensure the response is closed after."""
|
||||
# Yield each chunk in the response body
|
||||
for chunk in response.iter_content(chunk_size=chunk_size):
|
||||
yield chunk
|
||||
# Once we're done streaming the body, ensure everything is closed.
|
||||
# This will return the connection to the HTTPConnectionPool in urllib3
|
||||
# and ideally reduce the number of HTTPConnectionPool full warnings.
|
||||
response.close()
|
||||
|
||||
|
||||
class SessionClient(adapter.Adapter, _BaseHTTPClient):
|
||||
|
||||
def __init__(self, session, **kwargs):
|
||||
kwargs.setdefault('user_agent', USER_AGENT)
|
||||
kwargs.setdefault('service_type', 'artifact')
|
||||
self.last_request_id = None
|
||||
super(SessionClient, self).__init__(session, **kwargs)
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
headers = encode_headers(kwargs.pop('headers', {}))
|
||||
kwargs['raise_exc'] = False
|
||||
data = self._set_common_request_kwargs(headers, kwargs)
|
||||
|
||||
try:
|
||||
resp = super(SessionClient, self).request(url,
|
||||
method,
|
||||
headers=headers,
|
||||
data=data,
|
||||
**kwargs)
|
||||
except ksa_exc.ConnectTimeout as e:
|
||||
conn_url = self.get_endpoint(auth=kwargs.get('auth'))
|
||||
conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/'))
|
||||
message = ("Error communicating with %(url)s %(e)s" %
|
||||
dict(url=conn_url, e=e))
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
except ksa_exc.ConnectFailure as e:
|
||||
conn_url = self.get_endpoint(auth=kwargs.get('auth'))
|
||||
conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/'))
|
||||
message = ("Error finding address for %(url)s: %(e)s" %
|
||||
dict(url=conn_url, e=e))
|
||||
raise exc.CommunicationError(message=message)
|
||||
|
||||
self.last_request_id = resp.headers.get('x-openstack-request-id')
|
||||
return self._handle_response(resp)
|
||||
|
||||
|
||||
def get_http_client(endpoint=None, session=None, **kwargs):
|
||||
if session:
|
||||
return SessionClient(session, **kwargs)
|
||||
elif endpoint:
|
||||
return HTTPClient(endpoint, **kwargs)
|
||||
else:
|
||||
raise AttributeError('Constructing a client must contain either an '
|
||||
'endpoint or a session')
|
347
glareclient/common/https.py
Normal file
347
glareclient/common/https.py
Normal file
@@ -0,0 +1,347 @@
|
||||
# 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.
|
||||
|
||||
import socket
|
||||
import ssl
|
||||
import struct
|
||||
|
||||
import OpenSSL
|
||||
from requests import adapters
|
||||
from requests import compat
|
||||
try:
|
||||
from requests.packages.urllib3 import connectionpool
|
||||
from requests.packages.urllib3 import poolmanager
|
||||
except ImportError:
|
||||
from urllib3 import connectionpool
|
||||
from urllib3 import poolmanager
|
||||
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
import six
|
||||
# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
|
||||
from six.moves import range
|
||||
|
||||
try:
|
||||
from eventlet import patcher
|
||||
# Handle case where we are running in a monkey patched environment
|
||||
if patcher.is_monkey_patched('socket'):
|
||||
from eventlet.green.httplib import HTTPSConnection
|
||||
from eventlet.green.OpenSSL.SSL import GreenConnection as Connection
|
||||
else:
|
||||
raise ImportError
|
||||
except ImportError:
|
||||
from OpenSSL import SSL
|
||||
from six.moves import http_client
|
||||
HTTPSConnection = http_client.HTTPSConnection
|
||||
Connection = SSL.Connection
|
||||
|
||||
|
||||
from glareclient import exc
|
||||
|
||||
|
||||
def verify_callback(host=None):
|
||||
"""Provide wrapper for do_verify_callback.
|
||||
|
||||
We use a partial around the 'real' verify_callback function
|
||||
so that we can stash the host value without holding a
|
||||
reference on the VerifiedHTTPSConnection.
|
||||
"""
|
||||
def wrapper(connection, x509, errnum,
|
||||
depth, preverify_ok, host=host):
|
||||
return do_verify_callback(connection, x509, errnum,
|
||||
depth, preverify_ok, host=host)
|
||||
return wrapper
|
||||
|
||||
|
||||
def do_verify_callback(connection, x509, errnum,
|
||||
depth, preverify_ok, host=None):
|
||||
"""Verify the server's SSL certificate.
|
||||
|
||||
This is a standalone function rather than a method to avoid
|
||||
issues around closing sockets if a reference is held on
|
||||
a VerifiedHTTPSConnection by the callback function.
|
||||
"""
|
||||
if x509.has_expired():
|
||||
msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
|
||||
raise exc.SSLCertificateError(msg)
|
||||
|
||||
if depth == 0 and preverify_ok:
|
||||
# We verify that the host matches against the last
|
||||
# certificate in the chain
|
||||
return host_matches_cert(host, x509)
|
||||
else:
|
||||
# Pass through OpenSSL's default result
|
||||
return preverify_ok
|
||||
|
||||
|
||||
def host_matches_cert(host, x509):
|
||||
"""Verify the certificate identifies the host.
|
||||
|
||||
Verify that the x509 certificate we have received
|
||||
from 'host' correctly identifies the server we are
|
||||
connecting to, ie that the certificate's Common Name
|
||||
or a Subject Alternative Name matches 'host'.
|
||||
"""
|
||||
def check_match(name):
|
||||
# Directly match the name
|
||||
if name == host:
|
||||
return True
|
||||
|
||||
# Support single wildcard matching
|
||||
if name.startswith('*.') and host.find('.') > 0:
|
||||
if name[2:] == host.split('.', 1)[1]:
|
||||
return True
|
||||
|
||||
common_name = x509.get_subject().commonName
|
||||
|
||||
# First see if we can match the CN
|
||||
if check_match(common_name):
|
||||
return True
|
||||
# Also try Subject Alternative Names for a match
|
||||
san_list = None
|
||||
for i in range(x509.get_extension_count()):
|
||||
ext = x509.get_extension(i)
|
||||
if ext.get_short_name() == b'subjectAltName':
|
||||
san_list = str(ext)
|
||||
for san in ''.join(san_list.split()).split(','):
|
||||
if san.startswith('DNS:'):
|
||||
if check_match(san.split(':', 1)[1]):
|
||||
return True
|
||||
|
||||
# Server certificate does not match host
|
||||
msg = ('Host "%s" does not match x509 certificate contents: '
|
||||
'CommonName "%s"' % (host, common_name))
|
||||
if san_list is not None:
|
||||
msg = msg + ', subjectAltName "%s"' % san_list
|
||||
raise exc.SSLCertificateError(msg)
|
||||
|
||||
|
||||
def to_bytes(s):
|
||||
if isinstance(s, six.string_types):
|
||||
return six.b(s)
|
||||
else:
|
||||
return s
|
||||
|
||||
|
||||
class HTTPSAdapter(adapters.HTTPAdapter):
|
||||
"""This adapter will be used just when ssl compression should be disabled.
|
||||
|
||||
The init method overwrites the default https pool by setting
|
||||
glareclient's one.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
classes_by_scheme = poolmanager.pool_classes_by_scheme
|
||||
classes_by_scheme["glare+https"] = HTTPSConnectionPool
|
||||
super(HTTPSAdapter, self).__init__(*args, **kwargs)
|
||||
|
||||
def request_url(self, request, proxies):
|
||||
# NOTE(flaper87): Make sure the url is encoded, otherwise
|
||||
# python's standard httplib will fail with a TypeError.
|
||||
url = super(HTTPSAdapter, self).request_url(request, proxies)
|
||||
if six.PY2:
|
||||
url = encodeutils.safe_encode(url)
|
||||
return url
|
||||
|
||||
def _create_glare_httpsconnectionpool(self, url):
|
||||
kw = self.poolmanager.connection_pool_kw
|
||||
# Parse the url to get the scheme, host, and port
|
||||
parsed = compat.urlparse(url)
|
||||
# If there is no port specified, we should use the standard HTTPS port
|
||||
port = parsed.port or 443
|
||||
host = parsed.netloc.rsplit(':', 1)[0]
|
||||
pool = HTTPSConnectionPool(host, port, **kw)
|
||||
|
||||
with self.poolmanager.pools.lock:
|
||||
self.poolmanager.pools[(parsed.scheme, host, port)] = pool
|
||||
|
||||
return pool
|
||||
|
||||
def get_connection(self, url, proxies=None):
|
||||
try:
|
||||
return super(HTTPSAdapter, self).get_connection(url, proxies)
|
||||
except KeyError:
|
||||
# NOTE(sigamvirus24): This works around modifying a module global
|
||||
# which fixes bug #1396550
|
||||
# The scheme is most likely glare+https but check anyway
|
||||
if not url.startswith('glare+https://'):
|
||||
raise
|
||||
|
||||
return self._create_glare_httpsconnectionpool(url)
|
||||
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
super(HTTPSAdapter, self).cert_verify(conn, url, verify, cert)
|
||||
conn.ca_certs = verify[0]
|
||||
conn.insecure = verify[1]
|
||||
|
||||
|
||||
class HTTPSConnectionPool(connectionpool.HTTPSConnectionPool):
|
||||
"""A replacement for the default HTTPSConnectionPool.
|
||||
|
||||
HTTPSConnectionPool will be instantiated when a new
|
||||
connection is requested to the HTTPSAdapter. This
|
||||
implementation overwrites the _new_conn method and
|
||||
returns an instances of glareclient's VerifiedHTTPSConnection
|
||||
which handles no compression.
|
||||
|
||||
ssl_compression is hard-coded to False because this will
|
||||
be used just when the user sets --no-ssl-compression.
|
||||
"""
|
||||
|
||||
scheme = 'glare+https'
|
||||
|
||||
def _new_conn(self):
|
||||
self.num_connections += 1
|
||||
return VerifiedHTTPSConnection(host=self.host,
|
||||
port=self.port,
|
||||
key_file=self.key_file,
|
||||
cert_file=self.cert_file,
|
||||
cacert=self.ca_certs,
|
||||
insecure=self.insecure,
|
||||
ssl_compression=False)
|
||||
|
||||
|
||||
class OpenSSLConnectionDelegator(object):
|
||||
"""An OpenSSL.SSL.Connection delegator.
|
||||
|
||||
Supplies an additional 'makefile' method which httplib requires
|
||||
and is not present in OpenSSL.SSL.Connection.
|
||||
|
||||
Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
|
||||
a delegator must be used.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.connection = Connection(*args, **kwargs)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.connection, name)
|
||||
|
||||
def makefile(self, *args, **kwargs):
|
||||
return socket._fileobject(self.connection, *args, **kwargs)
|
||||
|
||||
|
||||
class VerifiedHTTPSConnection(HTTPSConnection):
|
||||
"""Extended OpenSSL HTTPSConnection for enhanced SSL support.
|
||||
|
||||
Note: Much of this functionality can eventually be replaced
|
||||
with native Python 3.3 code.
|
||||
"""
|
||||
# Restrict the set of client supported cipher suites
|
||||
CIPHERS = 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:'\
|
||||
'eCDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:'\
|
||||
'RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS'
|
||||
|
||||
def __init__(self, host, port=None, key_file=None, cert_file=None,
|
||||
cacert=None, timeout=None, insecure=False,
|
||||
ssl_compression=True):
|
||||
# List of exceptions reported by Python3 instead of
|
||||
# SSLConfigurationError
|
||||
if six.PY3:
|
||||
excp_lst = (TypeError, FileNotFoundError, ssl.SSLError)
|
||||
else:
|
||||
# NOTE(jamespage)
|
||||
# Accommodate changes in behaviour for pep-0467, introduced
|
||||
# in python 2.7.9.
|
||||
# https://github.com/python/peps/blob/master/pep-0476.txt
|
||||
excp_lst = (TypeError, IOError, ssl.SSLError)
|
||||
try:
|
||||
HTTPSConnection.__init__(self, host, port,
|
||||
key_file=key_file,
|
||||
cert_file=cert_file)
|
||||
self.key_file = key_file
|
||||
self.cert_file = cert_file
|
||||
self.timeout = timeout
|
||||
self.insecure = insecure
|
||||
# NOTE(flaper87): `is_verified` is needed for
|
||||
# requests' urllib3. If insecure is True then
|
||||
# the request is not `verified`, hence `not insecure`
|
||||
self.is_verified = not insecure
|
||||
self.ssl_compression = ssl_compression
|
||||
self.cacert = None if cacert is None else str(cacert)
|
||||
self.set_context()
|
||||
# ssl exceptions are reported in various form in Python 3
|
||||
# so to be compatible, we report the same kind as under
|
||||
# Python2
|
||||
except excp_lst as e:
|
||||
raise exc.SSLConfigurationError(str(e))
|
||||
|
||||
def set_context(self):
|
||||
"""Set up the OpenSSL context."""
|
||||
self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
|
||||
self.context.set_cipher_list(self.CIPHERS)
|
||||
|
||||
if self.ssl_compression is False:
|
||||
self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
|
||||
|
||||
if self.insecure is not True:
|
||||
self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
|
||||
verify_callback(host=self.host))
|
||||
else:
|
||||
self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
|
||||
lambda *args: True)
|
||||
|
||||
if self.cert_file:
|
||||
try:
|
||||
self.context.use_certificate_file(self.cert_file)
|
||||
except Exception as e:
|
||||
msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
|
||||
raise exc.SSLConfigurationError(msg)
|
||||
if self.key_file is None:
|
||||
# We support having key and cert in same file
|
||||
try:
|
||||
self.context.use_privatekey_file(self.cert_file)
|
||||
except Exception as e:
|
||||
msg = ('No key file specified and unable to load key '
|
||||
'from "%s" %s' % (self.cert_file, e))
|
||||
raise exc.SSLConfigurationError(msg)
|
||||
|
||||
if self.key_file:
|
||||
try:
|
||||
self.context.use_privatekey_file(self.key_file)
|
||||
except Exception as e:
|
||||
msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
|
||||
raise exc.SSLConfigurationError(msg)
|
||||
|
||||
if self.cacert:
|
||||
try:
|
||||
self.context.load_verify_locations(to_bytes(self.cacert))
|
||||
except Exception as e:
|
||||
msg = 'Unable to load CA from "%s" %s' % (self.cacert, e)
|
||||
raise exc.SSLConfigurationError(msg)
|
||||
else:
|
||||
self.context.set_default_verify_paths()
|
||||
|
||||
def connect(self):
|
||||
"""Connect to an SSL port using the OpenSSL library.
|
||||
|
||||
This method also applies per-connection parameters to the connection.
|
||||
"""
|
||||
result = socket.getaddrinfo(self.host, self.port, 0,
|
||||
socket.SOCK_STREAM)
|
||||
if result:
|
||||
socket_family = result[0][0]
|
||||
if socket_family == socket.AF_INET6:
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
else:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
else:
|
||||
# If due to some reason the address lookup fails - we still connect
|
||||
# to IPv4 socket. This retains the older behavior.
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
if self.timeout is not None:
|
||||
# '0' microseconds
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
|
||||
struct.pack('LL', self.timeout, 0))
|
||||
self.sock = OpenSSLConnectionDelegator(self.context, sock)
|
||||
self.sock.connect((self.host, self.port))
|
101
glareclient/common/progressbar.py
Normal file
101
glareclient/common/progressbar.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# 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.
|
||||
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
|
||||
class _ProgressBarBase(object):
|
||||
"""A progress bar provider for a wrapped obect.
|
||||
|
||||
Base abstract class used by specific class wrapper to show
|
||||
a progress bar when the wrapped object are consumed.
|
||||
|
||||
:param wrapped: Object to wrap that hold data to be consumed.
|
||||
:param totalsize: The total size of the data in the wrapped object.
|
||||
|
||||
:note: The progress will be displayed only if sys.stdout is a tty.
|
||||
"""
|
||||
|
||||
def __init__(self, wrapped, totalsize):
|
||||
self._wrapped = wrapped
|
||||
self._totalsize = float(totalsize)
|
||||
self._show_progress = sys.stdout.isatty()
|
||||
self._percent = 0
|
||||
|
||||
def _display_progress_bar(self, size_read):
|
||||
if self._show_progress:
|
||||
if self._totalsize == 0:
|
||||
self._totalsize = size_read
|
||||
self._percent += size_read / self._totalsize
|
||||
# Output something like this: [==========> ] 49%
|
||||
sys.stdout.write('\r[{0:<30}] {1:.0%}'.format(
|
||||
'=' * int(round(self._percent * 29)) + '>', self._percent
|
||||
))
|
||||
sys.stdout.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# Forward other attribute access to the wrapped object.
|
||||
return getattr(self._wrapped, attr)
|
||||
|
||||
|
||||
class VerboseFileWrapper(_ProgressBarBase):
|
||||
"""A file wrapper with a progress bar.
|
||||
|
||||
The file wrapper shows and advances a progress bar whenever the
|
||||
wrapped file's read method is called.
|
||||
"""
|
||||
|
||||
def read(self, *args, **kwargs):
|
||||
data = self._wrapped.read(*args, **kwargs)
|
||||
if data:
|
||||
self._display_progress_bar(len(data))
|
||||
else:
|
||||
if self._show_progress:
|
||||
# Break to a new line from the progress bar for incoming
|
||||
# output.
|
||||
sys.stdout.write('\n')
|
||||
return data
|
||||
|
||||
|
||||
class VerboseIteratorWrapper(_ProgressBarBase):
|
||||
"""An iterator wrapper with a progress bar.
|
||||
|
||||
The iterator wrapper shows and advances a progress bar whenever the
|
||||
wrapped data is consumed from the iterator.
|
||||
|
||||
:note: Use only with iterator that yield strings.
|
||||
"""
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
try:
|
||||
data = six.next(self._wrapped)
|
||||
# NOTE(mouad): Assuming that data is a string b/c otherwise calling
|
||||
# len function will not make any sense.
|
||||
self._display_progress_bar(len(data))
|
||||
return data
|
||||
except StopIteration:
|
||||
if self._show_progress:
|
||||
# Break to a new line from the progress bar for incoming
|
||||
# output.
|
||||
sys.stdout.write('\n')
|
||||
raise
|
||||
|
||||
# In Python 3, __next__() has replaced next().
|
||||
__next__ = next
|
280
glareclient/common/utils.py
Normal file
280
glareclient/common/utils.py
Normal file
@@ -0,0 +1,280 @@
|
||||
# Copyright 2012 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.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import errno
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
import sys
|
||||
|
||||
if os.name == 'nt':
|
||||
import msvcrt
|
||||
else:
|
||||
msvcrt = None
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
|
||||
SENSITIVE_HEADERS = ('X-Auth-Token', )
|
||||
|
||||
|
||||
# Decorator for cli-args
|
||||
def arg(*args, **kwargs):
|
||||
def _decorator(func):
|
||||
# Because of the semantics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
|
||||
return func
|
||||
return _decorator
|
||||
|
||||
|
||||
def env(*vars, **kwargs):
|
||||
"""Search for the first defined of possibly many env vars.
|
||||
|
||||
Returns the first environment variable defined in vars, or
|
||||
returns the default defined in kwargs.
|
||||
"""
|
||||
for v in vars:
|
||||
value = os.environ.get(v, None)
|
||||
if value:
|
||||
return value
|
||||
return kwargs.get('default', '')
|
||||
|
||||
|
||||
def import_versioned_module(version, submodule=None):
|
||||
module = 'glareclient.v%s' % version
|
||||
if submodule:
|
||||
module = '.'.join((module, submodule))
|
||||
return importutils.import_module(module)
|
||||
|
||||
|
||||
def exit(msg='', exit_code=1):
|
||||
if msg:
|
||||
print_err(msg)
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
def print_err(msg):
|
||||
print(encodeutils.safe_decode(msg), file=sys.stderr)
|
||||
|
||||
|
||||
def strip_version(endpoint):
|
||||
"""Strip version from the last component of endpoint if present."""
|
||||
# NOTE(flaper87): This shouldn't be necessary if
|
||||
# we make endpoint the first argument. However, we
|
||||
# can't do that just yet because we need to keep
|
||||
# backwards compatibility.
|
||||
if not isinstance(endpoint, six.string_types):
|
||||
raise ValueError("Expected endpoint")
|
||||
|
||||
version = None
|
||||
# Get rid of trailing '/' if present
|
||||
endpoint = endpoint.rstrip('/')
|
||||
url_parts = urlparse.urlparse(endpoint)
|
||||
(scheme, netloc, path, __, __, __) = url_parts
|
||||
path = path.lstrip('/')
|
||||
# regex to match 'v1' or 'v2.0' etc
|
||||
if re.match('v\d+\.?\d*', path):
|
||||
version = float(path.lstrip('v'))
|
||||
endpoint = scheme + '://' + netloc
|
||||
return endpoint, version
|
||||
|
||||
|
||||
def integrity_iter(iter, checksum):
|
||||
"""Check blob integrity.
|
||||
|
||||
:raises: IOError
|
||||
"""
|
||||
md5sum = hashlib.md5()
|
||||
for chunk in iter:
|
||||
yield chunk
|
||||
if isinstance(chunk, six.string_types):
|
||||
chunk = six.b(chunk)
|
||||
md5sum.update(chunk)
|
||||
md5sum = md5sum.hexdigest()
|
||||
if md5sum != checksum:
|
||||
raise IOError(errno.EPIPE,
|
||||
'Corrupt blob download. Checksum was %s expected %s' %
|
||||
(md5sum, checksum))
|
||||
|
||||
|
||||
def safe_header(name, value):
|
||||
if value is not None and name in SENSITIVE_HEADERS:
|
||||
h = hashlib.sha1(value)
|
||||
d = h.hexdigest()
|
||||
return name, "{SHA1}%s" % d
|
||||
else:
|
||||
return name, value
|
||||
|
||||
|
||||
def endpoint_version_from_url(endpoint, default_version=None):
|
||||
if endpoint:
|
||||
endpoint, version = strip_version(endpoint)
|
||||
return endpoint, version or default_version
|
||||
else:
|
||||
return None, default_version
|
||||
|
||||
|
||||
def debug_enabled(argv):
|
||||
if bool(env('GLARECLIENT_DEBUG')) is True:
|
||||
return True
|
||||
if '--debug' in argv or '-d' in argv:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class IterableWithLength(object):
|
||||
def __init__(self, iterable, length):
|
||||
self.iterable = iterable
|
||||
self.length = length
|
||||
|
||||
def __iter__(self):
|
||||
try:
|
||||
for chunk in self.iterable:
|
||||
yield chunk
|
||||
finally:
|
||||
self.iterable.close()
|
||||
|
||||
def next(self):
|
||||
return next(self.iterable)
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
|
||||
def get_item_properties(item, fields, mixed_case_fields=None, formatters=None):
|
||||
"""Return a tuple containing the item properties.
|
||||
|
||||
:param item: a single item resource (e.g. Server, Project, etc)
|
||||
:param fields: tuple of strings with the desired field names
|
||||
:param mixed_case_fields: tuple of field names to preserve case
|
||||
:param formatters: dictionary mapping field names to callables
|
||||
to format the values
|
||||
"""
|
||||
if mixed_case_fields is None:
|
||||
mixed_case_fields = []
|
||||
if formatters is None:
|
||||
formatters = {}
|
||||
|
||||
row = []
|
||||
|
||||
for field in fields:
|
||||
if field in mixed_case_fields:
|
||||
field_name = field.replace(' ', '_')
|
||||
else:
|
||||
field_name = field.lower().replace(' ', '_')
|
||||
data = item[field_name]
|
||||
if field in formatters:
|
||||
row.append(formatters[field](data))
|
||||
else:
|
||||
row.append(data)
|
||||
return tuple(row)
|
||||
|
||||
|
||||
def make_size_human_readable(size):
|
||||
suffix = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB']
|
||||
base = 1024.0
|
||||
index = 0
|
||||
|
||||
if size is None:
|
||||
size = 0
|
||||
while size >= base:
|
||||
index = index + 1
|
||||
size = size / base
|
||||
|
||||
padded = '%.1f' % size
|
||||
stripped = padded.rstrip('0').rstrip('.')
|
||||
|
||||
return '%s%s' % (stripped, suffix[index])
|
||||
|
||||
|
||||
def save_blob(data, path):
|
||||
"""Save a blob to the specified path.
|
||||
|
||||
:param data: blob of the artifact
|
||||
:param path: path to save the blob to
|
||||
"""
|
||||
if path is None:
|
||||
blob = getattr(sys.stdout, 'buffer',
|
||||
sys.stdout)
|
||||
else:
|
||||
blob = open(path, 'wb')
|
||||
try:
|
||||
for chunk in data:
|
||||
blob.write(chunk)
|
||||
finally:
|
||||
if path is not None:
|
||||
blob.close()
|
||||
|
||||
|
||||
def get_data_file(blob):
|
||||
if blob:
|
||||
return open(blob, 'rb')
|
||||
else:
|
||||
# distinguish cases where:
|
||||
# (1) stdin is not valid (as in cron jobs):
|
||||
# glare ... <&-
|
||||
# (2) blob is provided through standard input:
|
||||
# glare ... < /tmp/file or cat /tmp/file | glare ...
|
||||
# (3) no blob provided:
|
||||
# glare ...
|
||||
try:
|
||||
os.fstat(0)
|
||||
except OSError:
|
||||
# (1) stdin is not valid (closed...)
|
||||
return None
|
||||
if not sys.stdin.isatty():
|
||||
# (2) blob data is provided through standard input
|
||||
blob_data = sys.stdin
|
||||
if hasattr(sys.stdin, 'buffer'):
|
||||
blob_data = sys.stdin.buffer
|
||||
if msvcrt:
|
||||
msvcrt.setmode(blob_data.fileno(), os.O_BINARY)
|
||||
return blob_data
|
||||
else:
|
||||
# (3) no blob data provided
|
||||
return None
|
||||
|
||||
|
||||
def get_file_size(file_obj):
|
||||
"""Analyze file-like object and attempt to determine its size.
|
||||
|
||||
:param file_obj: file-like object.
|
||||
:retval The file's size or None if it cannot be determined.
|
||||
"""
|
||||
if (hasattr(file_obj, 'seek') and hasattr(file_obj, 'tell') and
|
||||
(six.PY2 or six.PY3 and file_obj.seekable())):
|
||||
try:
|
||||
curr = file_obj.tell()
|
||||
file_obj.seek(0, os.SEEK_END)
|
||||
size = file_obj.tell()
|
||||
file_obj.seek(curr)
|
||||
return size
|
||||
except IOError as e:
|
||||
if e.errno == errno.ESPIPE:
|
||||
# Illegal seek. This means the file object
|
||||
# is a pipe (e.g. the user is trying
|
||||
# to pipe blob to the client,
|
||||
# echo testdata | bin/glare add blah...), or
|
||||
# that file object is empty, or that a file-like
|
||||
# object which doesn't support 'seek/tell' has
|
||||
# been supplied.
|
||||
return
|
||||
else:
|
||||
raise
|
205
glareclient/exc.py
Normal file
205
glareclient/exc.py
Normal file
@@ -0,0 +1,205 @@
|
||||
# Copyright 2012 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.
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
|
||||
class BaseException(Exception):
|
||||
"""An error occurred."""
|
||||
def __init__(self, message=None):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return self.message or self.__class__.__doc__
|
||||
|
||||
|
||||
class CommandError(BaseException):
|
||||
"""Invalid usage of CLI."""
|
||||
|
||||
|
||||
class InvalidEndpoint(BaseException):
|
||||
"""The provided endpoint is invalid."""
|
||||
|
||||
|
||||
class CommunicationError(BaseException):
|
||||
"""Unable to communicate with server."""
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""DEPRECATED!"""
|
||||
|
||||
|
||||
class HTTPException(ClientException):
|
||||
"""Base exception for all HTTP-derived exceptions."""
|
||||
code = 'N/A'
|
||||
|
||||
def __init__(self, details=None):
|
||||
self.details = details or self.__class__.__name__
|
||||
|
||||
def __str__(self):
|
||||
return "%s (HTTP %s)" % (self.details, self.code)
|
||||
|
||||
|
||||
class HTTPMultipleChoices(HTTPException):
|
||||
code = 300
|
||||
|
||||
def __str__(self):
|
||||
self.details = ("Requested version of OpenStack Glare API is not "
|
||||
"available.")
|
||||
return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code,
|
||||
self.details)
|
||||
|
||||
|
||||
class BadRequest(HTTPException):
|
||||
"""DEPRECATED!"""
|
||||
code = 400
|
||||
|
||||
|
||||
class HTTPBadRequest(BadRequest):
|
||||
pass
|
||||
|
||||
|
||||
class Unauthorized(HTTPException):
|
||||
"""DEPRECATED!"""
|
||||
code = 401
|
||||
|
||||
|
||||
class HTTPUnauthorized(Unauthorized):
|
||||
pass
|
||||
|
||||
|
||||
class Forbidden(HTTPException):
|
||||
"""DEPRECATED!"""
|
||||
code = 403
|
||||
|
||||
|
||||
class HTTPForbidden(Forbidden):
|
||||
pass
|
||||
|
||||
|
||||
class NotFound(HTTPException):
|
||||
"""DEPRECATED!"""
|
||||
code = 404
|
||||
|
||||
|
||||
class HTTPNotFound(NotFound):
|
||||
pass
|
||||
|
||||
|
||||
class HTTPMethodNotAllowed(HTTPException):
|
||||
code = 405
|
||||
|
||||
|
||||
class Conflict(HTTPException):
|
||||
"""DEPRECATED!"""
|
||||
code = 409
|
||||
|
||||
|
||||
class HTTPConflict(Conflict):
|
||||
pass
|
||||
|
||||
|
||||
class OverLimit(HTTPException):
|
||||
"""DEPRECATED!"""
|
||||
code = 413
|
||||
|
||||
|
||||
class HTTPOverLimit(OverLimit):
|
||||
pass
|
||||
|
||||
|
||||
class HTTPInternalServerError(HTTPException):
|
||||
code = 500
|
||||
|
||||
|
||||
class HTTPNotImplemented(HTTPException):
|
||||
code = 501
|
||||
|
||||
|
||||
class HTTPBadGateway(HTTPException):
|
||||
code = 502
|
||||
|
||||
|
||||
class ServiceUnavailable(HTTPException):
|
||||
"""DEPRECATED!"""
|
||||
code = 503
|
||||
|
||||
|
||||
class HTTPServiceUnavailable(ServiceUnavailable):
|
||||
pass
|
||||
|
||||
|
||||
# NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception
|
||||
# classes
|
||||
_code_map = {}
|
||||
for obj_name in dir(sys.modules[__name__]):
|
||||
if obj_name.startswith('HTTP'):
|
||||
obj = getattr(sys.modules[__name__], obj_name)
|
||||
_code_map[obj.code] = obj
|
||||
|
||||
|
||||
def from_response(response, body=None):
|
||||
"""Return an instance of an HTTPException based on httplib response."""
|
||||
cls = _code_map.get(response.status_code, HTTPException)
|
||||
if body and 'json' in response.headers['content-type']:
|
||||
# Iterate over the nested objects and retrieve the "message" attribute.
|
||||
messages = [response.json().get('error').get('message')]
|
||||
# Join all of the messages together nicely and filter out any objects
|
||||
# that don't have a "message" attr.
|
||||
details = '\n'.join(i for i in messages if i is not None)
|
||||
return cls(details=details)
|
||||
elif body and 'html' in response.headers['content-type']:
|
||||
# Split the lines, strip whitespace and inline HTML from the response.
|
||||
details = [re.sub(r'<.+?>', '', i.strip())
|
||||
for i in response.text.splitlines()]
|
||||
details = [i for i in details if i]
|
||||
# Remove duplicates from the list.
|
||||
details_seen = set()
|
||||
details_temp = []
|
||||
for i in details:
|
||||
if i not in details_seen:
|
||||
details_temp.append(i)
|
||||
details_seen.add(i)
|
||||
# Return joined string separated by colons.
|
||||
details = ': '.join(details_temp)
|
||||
return cls(details=details)
|
||||
elif body:
|
||||
if six.PY3:
|
||||
body = body.decode('utf-8')
|
||||
details = body.replace('\n\n', '\n')
|
||||
return cls(details=details)
|
||||
|
||||
return cls()
|
||||
|
||||
|
||||
class NoTokenLookupException(Exception):
|
||||
"""DEPRECATED!"""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(Exception):
|
||||
"""DEPRECATED!"""
|
||||
pass
|
||||
|
||||
|
||||
class SSLConfigurationError(BaseException):
|
||||
pass
|
||||
|
||||
|
||||
class SSLCertificateError(BaseException):
|
||||
pass
|
0
glareclient/osc/__init__.py
Normal file
0
glareclient/osc/__init__.py
Normal file
56
glareclient/osc/plugin.py
Normal file
56
glareclient/osc/plugin.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from osc_lib import utils
|
||||
|
||||
from glareclient._i18n import _
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_API_VERSION = "1"
|
||||
API_VERSION_OPTION = "os_artifact_api_version"
|
||||
API_NAME = "artifact"
|
||||
API_VERSIONS = {
|
||||
'1': 'glareclient.v1.client.Client',
|
||||
}
|
||||
|
||||
|
||||
def make_client(instance):
|
||||
"""Returns an artifact service client"""
|
||||
glare_client = utils.get_client_class(
|
||||
API_NAME,
|
||||
instance._api_version[API_NAME],
|
||||
API_VERSIONS)
|
||||
LOG.debug("Instantiating glare client: {0}".format(
|
||||
glare_client))
|
||||
|
||||
client = glare_client(
|
||||
instance.get_configuration().get('glare_url'),
|
||||
region_name=instance._region_name,
|
||||
session=instance.session,
|
||||
service_type='artifact',
|
||||
)
|
||||
return client
|
||||
|
||||
|
||||
def build_option_parser(parser):
|
||||
"""Hook to add global options"""
|
||||
parser.add_argument(
|
||||
'--os-artifact-api-version',
|
||||
metavar='<artifact-api-version>',
|
||||
default=utils.env('OS_ARTIFACT_API_VERSION'),
|
||||
help=_('Artifact API version, default=%s '
|
||||
'(Env: OS_ARTIFACT_API_VERSION)') % DEFAULT_API_VERSION,
|
||||
)
|
||||
return parser
|
38
glareclient/osc/v1/__init__.py
Normal file
38
glareclient/osc/v1/__init__.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# Copyright (c) 2016 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
class TypeMapperAction(argparse.Action):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
setattr(namespace, self.dest, self._type_name_mapper(values))
|
||||
|
||||
@staticmethod
|
||||
def _type_name_mapper(type_name):
|
||||
if type_name in ['images', 'image']:
|
||||
return 'images'
|
||||
elif type_name in ['heat-templates', 'heat-template',
|
||||
'heat_templates', 'heat_template']:
|
||||
return 'heat_templates'
|
||||
elif type_name in ['heat-environments', 'heat-environment',
|
||||
'heat_environments', 'heat_environment']:
|
||||
return 'heat_environments'
|
||||
elif type_name in ['tosca-templates', 'tosca-template',
|
||||
'tosca_templates', 'tosca_template']:
|
||||
return 'tosca_templates'
|
||||
elif type_name in ['murano-packages', 'murano-package',
|
||||
'murano_packages', 'murano_package']:
|
||||
return 'tosca_templates'
|
||||
return type_name
|
344
glareclient/osc/v1/artifacts.py
Normal file
344
glareclient/osc/v1/artifacts.py
Normal file
@@ -0,0 +1,344 @@
|
||||
# Copyright (c) 2016 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from osc_lib.command import command
|
||||
|
||||
from glareclient.common import utils as glare_utils
|
||||
from glareclient.osc.v1 import TypeMapperAction
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ListArtifacts(command.Lister):
|
||||
"""List of artifacts"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListArtifacts, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'type_name',
|
||||
metavar='<TYPE_NAME>',
|
||||
action=TypeMapperAction,
|
||||
help='Name of artifact type.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--limit',
|
||||
default=20,
|
||||
metavar='<LIMIT>',
|
||||
help='Maximum number of artifacts to get.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--page-size',
|
||||
default=20,
|
||||
metavar='<SIZE>',
|
||||
help='Number of artifacts to request in each paginated request.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--filter',
|
||||
default=[],
|
||||
action='append',
|
||||
metavar='<KEY=VALUE>',
|
||||
help='Filtering artifact list by a user-defined property.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--sort',
|
||||
default='name:asc',
|
||||
metavar='<key>[:<direction>]',
|
||||
help='Comma-separated list of sort keys and directions in the '
|
||||
'form of <key>[:<asc|desc>].',
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.debug('take_action({0})'.format(parsed_args))
|
||||
client = self.app.client_manager.artifact
|
||||
params = {'limit': parsed_args.limit,
|
||||
'filters': [f.split('=', 1) for f in parsed_args.filter],
|
||||
'sort': parsed_args.sort,
|
||||
'page_size': parsed_args.page_size}
|
||||
|
||||
data = client.artifacts.list(type_name=parsed_args.type_name,
|
||||
**params)
|
||||
|
||||
columns = ('id', 'name', 'version', 'owner', 'visibility', 'status')
|
||||
column_headers = [c.capitalize() for c in columns]
|
||||
table = []
|
||||
for af in data:
|
||||
table.append(glare_utils.get_item_properties(af, columns))
|
||||
return (column_headers,
|
||||
table)
|
||||
|
||||
|
||||
class ShowArtifact(command.ShowOne):
|
||||
"""Show details artifact"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ShowArtifact, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'type_name',
|
||||
metavar='<TYPE_NAME>',
|
||||
action=TypeMapperAction,
|
||||
help='Name of artifact type.',
|
||||
),
|
||||
parser.add_argument(
|
||||
'id',
|
||||
metavar='<ID>',
|
||||
help='ID of the artifact to update',
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.debug('take_action({0})'.format(parsed_args))
|
||||
client = self.app.client_manager.artifact
|
||||
data = client.artifacts.get(parsed_args.id,
|
||||
type_name=parsed_args.type_name)
|
||||
return self.dict2columns(data)
|
||||
|
||||
|
||||
class CreateArtifact(command.ShowOne):
|
||||
"""Create a new artifact"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(CreateArtifact, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'type_name',
|
||||
metavar='<TYPE_NAME>',
|
||||
action=TypeMapperAction,
|
||||
help='Name of artifact type.',
|
||||
),
|
||||
parser.add_argument(
|
||||
'name',
|
||||
default='',
|
||||
metavar='<NAME>',
|
||||
help='Name of the artifact.',
|
||||
),
|
||||
parser.add_argument(
|
||||
'--artifact-version',
|
||||
default='0.0.0',
|
||||
metavar='<VERSION>',
|
||||
help='Version of the artifact.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--property',
|
||||
metavar='<key=value>',
|
||||
action='append',
|
||||
default=[],
|
||||
help='Artifact property.'
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.debug('take_action({0})'.format(parsed_args))
|
||||
|
||||
prop = {}
|
||||
for datum in parsed_args.property:
|
||||
key, value = datum.split('=', 1)
|
||||
prop[key] = value
|
||||
|
||||
client = self.app.client_manager.artifact
|
||||
data = client.artifacts.create(parsed_args.name,
|
||||
type_name=parsed_args.type_name,
|
||||
version=parsed_args.artifact_version,
|
||||
**prop)
|
||||
return self.dict2columns(data)
|
||||
|
||||
|
||||
class UpdateArtifact(command.ShowOne):
|
||||
"""Update the properties of the artifact"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(UpdateArtifact, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'type_name',
|
||||
metavar='<TYPE_NAME>',
|
||||
action=TypeMapperAction,
|
||||
help='Name of artifact type.',
|
||||
),
|
||||
parser.add_argument(
|
||||
'id',
|
||||
metavar='<ID>',
|
||||
help='ID of the artifact to update',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--name',
|
||||
metavar='<NAME>',
|
||||
help='Name of the artifact',
|
||||
),
|
||||
parser.add_argument(
|
||||
'--remove-property',
|
||||
metavar='<key=value>',
|
||||
action='append',
|
||||
default=[],
|
||||
help='Property that will be removed.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--property',
|
||||
metavar='<key=value>',
|
||||
action='append',
|
||||
default=[],
|
||||
help='Update property values.'
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.debug('take_action({0})'.format(parsed_args))
|
||||
|
||||
remove_props = {}
|
||||
for datum in parsed_args.remove_property:
|
||||
key, value = datum.split('=', 1)
|
||||
remove_props[key] = value
|
||||
|
||||
prop = {}
|
||||
for datum in parsed_args.property:
|
||||
key, value = datum.split('=', 1)
|
||||
prop[key] = value
|
||||
|
||||
client = self.app.client_manager.artifact
|
||||
data = client.artifacts.update(parsed_args.id,
|
||||
type_name=parsed_args.type_name,
|
||||
remove_props=remove_props,
|
||||
**prop)
|
||||
return self.dict2columns(data)
|
||||
|
||||
|
||||
class DeleteArtifact(command.ShowOne):
|
||||
"""Delete the artifact"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(DeleteArtifact, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'type_name',
|
||||
metavar='<TYPE_NAME>',
|
||||
action=TypeMapperAction,
|
||||
help='Name of artifact type.',
|
||||
),
|
||||
parser.add_argument(
|
||||
'id',
|
||||
metavar='<ID>',
|
||||
help='ID of the artifact to update',
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.debug('take_action({0})'.format(parsed_args))
|
||||
client = self.app.client_manager.artifact
|
||||
data = client.artifacts.delete(parsed_args.id,
|
||||
type_name=parsed_args.type_name)
|
||||
return self.dict2columns(data)
|
||||
|
||||
|
||||
class ActivateArtifact(command.ShowOne):
|
||||
"""Activate the artifact"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ActivateArtifact, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'type_name',
|
||||
metavar='<TYPE_NAME>',
|
||||
action=TypeMapperAction,
|
||||
help='Name of artifact type.',
|
||||
),
|
||||
parser.add_argument(
|
||||
'id',
|
||||
metavar='<ID>',
|
||||
help='ID of the artifact to update',
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.debug('take_action({0})'.format(parsed_args))
|
||||
client = self.app.client_manager.artifact
|
||||
data = client.artifacts.active(parsed_args.id,
|
||||
type_name=parsed_args.type_name)
|
||||
return self.dict2columns(data)
|
||||
|
||||
|
||||
class DeactivateArtifact(command.ShowOne):
|
||||
"""Deactivate the artifact"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(DeactivateArtifact, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'type_name',
|
||||
metavar='<TYPE_NAME>',
|
||||
action=TypeMapperAction,
|
||||
help='Name of artifact type.',
|
||||
),
|
||||
parser.add_argument(
|
||||
'id',
|
||||
metavar='<ID>',
|
||||
help='ID of the artifact to update',
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.debug('take_action({0})'.format(parsed_args))
|
||||
client = self.app.client_manager.artifact
|
||||
data = client.artifacts.deactivete(parsed_args.id,
|
||||
type_name=parsed_args.type_name)
|
||||
return self.dict2columns(data)
|
||||
|
||||
|
||||
class ReactivateArtifact(command.ShowOne):
|
||||
"""Reactivate the artifact"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ReactivateArtifact, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'type_name',
|
||||
metavar='<TYPE_NAME>',
|
||||
action=TypeMapperAction,
|
||||
help='Name of artifact type.',
|
||||
),
|
||||
parser.add_argument(
|
||||
'id',
|
||||
metavar='<ID>',
|
||||
help='ID of the artifact to update',
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.debug('take_action({0})'.format(parsed_args))
|
||||
client = self.app.client_manager.artifact
|
||||
data = client.artifacts.reactivete(parsed_args.id,
|
||||
type_name=parsed_args.type_name)
|
||||
return self.dict2columns(data)
|
||||
|
||||
|
||||
class PublishArtifact(command.ShowOne):
|
||||
"""Publish the artifact"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(PublishArtifact, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'type_name',
|
||||
metavar='<TYPE_NAME>',
|
||||
action=TypeMapperAction,
|
||||
help='Name of artifact type.',
|
||||
),
|
||||
parser.add_argument(
|
||||
'id',
|
||||
metavar='<ID>',
|
||||
help='ID of the artifact to update',
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.debug('take_action({0})'.format(parsed_args))
|
||||
client = self.app.client_manager.artifact
|
||||
data = client.artifacts.publish(parsed_args.id,
|
||||
type_name=parsed_args.type_name)
|
||||
return self.dict2columns(data)
|
158
glareclient/osc/v1/blobs.py
Normal file
158
glareclient/osc/v1/blobs.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# Copyright (c) 2016 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from osc_lib.command import command
|
||||
|
||||
from glareclient.common import progressbar
|
||||
from glareclient.common import utils
|
||||
from glareclient.osc.v1 import TypeMapperAction
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _default_blob_property(type_name):
|
||||
if type_name == 'images':
|
||||
return 'image'
|
||||
elif type_name == 'murano_packages':
|
||||
return 'package'
|
||||
elif type_name in ('heat_templates', 'tosca_templates'):
|
||||
return 'template'
|
||||
elif type_name == 'heat_environments':
|
||||
return 'environment'
|
||||
utils.exit('Unknown artifact type. Please specify --blob-property.')
|
||||
|
||||
|
||||
class UploadBlob(command.ShowOne):
|
||||
"""Upload blob"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(UploadBlob, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'type_name',
|
||||
metavar='<TYPE_NAME>',
|
||||
action=TypeMapperAction,
|
||||
help='Name of artifact type.',
|
||||
),
|
||||
parser.add_argument(
|
||||
'id',
|
||||
metavar='<ID>',
|
||||
help='ID of the artifact to update',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--blob',
|
||||
metavar='<TYPE_NAME>',
|
||||
help='Local file that contains blob to be uploaded.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--blob-property',
|
||||
metavar='<BLOB_PROPERTY>',
|
||||
help='Name of the blob field.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--progress',
|
||||
help='Show download progress bar.'
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.debug('take_action({0})'.format(parsed_args))
|
||||
client = self.app.client_manager.artifact
|
||||
|
||||
if not parsed_args.blob_property:
|
||||
parsed_args.blob_property = _default_blob_property(
|
||||
parsed_args.type_name)
|
||||
|
||||
blob = utils.get_data_file(parsed_args.blob)
|
||||
if parsed_args.progress:
|
||||
file_size = utils.get_file_size(blob)
|
||||
if file_size is not None:
|
||||
blob = progressbar.VerboseFileWrapper(blob, file_size)
|
||||
|
||||
client.artifacts.upload_blob(parsed_args.id,
|
||||
parsed_args.blob_property,
|
||||
blob,
|
||||
type_name=parsed_args.type_name)
|
||||
|
||||
data = client.artifacts.get(parsed_args.id,
|
||||
type_name=parsed_args.type_name)
|
||||
|
||||
size = data[parsed_args.blob_property].pop('size', None)
|
||||
data_to_display = {'blob_property': parsed_args.blob_property,
|
||||
'id': parsed_args.id,
|
||||
'name': data['name'],
|
||||
'version': data['version'],
|
||||
'status': data['status'],
|
||||
'size': utils.make_size_human_readable(size)}
|
||||
data_to_display.update(data[parsed_args.blob_property])
|
||||
return self.dict2columns(data_to_display)
|
||||
|
||||
|
||||
class DownloadBlob(command.Command):
|
||||
"""Download blob"""
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(DownloadBlob, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'type_name',
|
||||
metavar='<TYPE_NAME>',
|
||||
action=TypeMapperAction,
|
||||
help='Name of artifact type.',
|
||||
),
|
||||
parser.add_argument(
|
||||
'id',
|
||||
metavar='<ID>',
|
||||
help='ID of the artifact to update',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--progress',
|
||||
default=False,
|
||||
help='Show download progress bar.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--file',
|
||||
metavar='<FILE>',
|
||||
help='Local file to save downloaded blob to. '
|
||||
'If this is not specified and there is no redirection '
|
||||
'the blob will not be saved.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--blob-property',
|
||||
metavar='<BLOB_PROPERTY>',
|
||||
default=None,
|
||||
help='Name of the blob field.'
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
LOG.debug('take_action({0})'.format(parsed_args))
|
||||
client = self.app.client_manager.artifact
|
||||
if not parsed_args.blob_property:
|
||||
parsed_args.blob_property = _default_blob_property(
|
||||
parsed_args.type_name)
|
||||
data = client.artifacts.download_blob(parsed_args.id,
|
||||
parsed_args.blob_property,
|
||||
type_name=parsed_args.type_name)
|
||||
if parsed_args.progress:
|
||||
data = progressbar.VerboseIteratorWrapper(data, len(data))
|
||||
if not (sys.stdout.isatty() and parsed_args.file is None):
|
||||
utils.save_blob(data, parsed_args.file)
|
||||
return self.dict2columns(())
|
||||
else:
|
||||
msg = ('No redirection or local file specified for downloaded '
|
||||
'blob. Please specify a local file with --file to save '
|
||||
'downloaded blob or redirect output to another source.')
|
||||
utils.exit(msg)
|
0
glareclient/tests/__init__.py
Normal file
0
glareclient/tests/__init__.py
Normal file
0
glareclient/tests/unit/__init__.py
Normal file
0
glareclient/tests/unit/__init__.py
Normal file
46
glareclient/tests/unit/base.py
Normal file
46
glareclient/tests/unit/base.py
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
# Copyright 2011 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.
|
||||
|
||||
import os
|
||||
|
||||
import fixtures
|
||||
import six
|
||||
import testtools
|
||||
|
||||
|
||||
class TestCaseShell(testtools.TestCase):
|
||||
TEST_REQUEST_BASE = {
|
||||
'verify': True,
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestCaseShell, self).setUp()
|
||||
if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
|
||||
os.environ.get('OS_STDOUT_CAPTURE') == '1'):
|
||||
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
|
||||
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
|
||||
if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
|
||||
os.environ.get('OS_STDERR_CAPTURE') == '1'):
|
||||
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
|
||||
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
|
||||
|
||||
|
||||
class TestAdditionalAsserts(testtools.TestCase):
|
||||
|
||||
def check_dict_is_subset(self, dict1, dict2):
|
||||
# There is an assert for this in Python 2.7 but not 2.6
|
||||
self.assertTrue(all(k in dict2 and dict2[k] == v for k, v
|
||||
in six.iteritems(dict1)))
|
0
glareclient/tests/unit/osc/__init__.py
Normal file
0
glareclient/tests/unit/osc/__init__.py
Normal file
34
glareclient/tests/unit/osc/test_plugin.py
Normal file
34
glareclient/tests/unit/osc/test_plugin.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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 mock
|
||||
|
||||
from glareclient.osc import plugin
|
||||
from glareclient.tests.unit import base
|
||||
|
||||
|
||||
class TestArtifactPlugin(base.TestCaseShell):
|
||||
|
||||
@mock.patch("glareclient.v1.client.Client")
|
||||
def test_make_client(self, p_client):
|
||||
|
||||
instance = mock.Mock()
|
||||
instance._api_version = {"artifact": '1'}
|
||||
instance._region_name = 'glare_region'
|
||||
instance.session = 'glare_session'
|
||||
|
||||
plugin.make_client(instance)
|
||||
p_client.assert_called_with(
|
||||
mock.ANY,
|
||||
region_name='glare_region',
|
||||
session='glare_session',
|
||||
service_type='artifact')
|
55
glareclient/tests/unit/test_client.py
Normal file
55
glareclient/tests/unit/test_client.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# 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.
|
||||
|
||||
import testtools
|
||||
|
||||
from glareclient import client
|
||||
from glareclient import v1
|
||||
|
||||
|
||||
class ClientTest(testtools.TestCase):
|
||||
|
||||
def test_no_endpoint_error(self):
|
||||
self.assertRaises(ValueError, client.Client, None)
|
||||
|
||||
def test_endpoint(self):
|
||||
gc = client.Client('1', "http://example.com")
|
||||
self.assertEqual("http://example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
||||
|
||||
def test_versioned_endpoint(self):
|
||||
gc = client.Client('1', "http://example.com/v1")
|
||||
self.assertEqual("http://example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
||||
|
||||
def test_versioned_endpoint_with_minor_revision(self):
|
||||
gc = client.Client('1', "http://example.com/v1.0")
|
||||
self.assertEqual("http://example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
||||
|
||||
def test_endpoint_with_version_hostname(self):
|
||||
gc = client.Client('1', "http://v1.example.com")
|
||||
self.assertEqual("http://v1.example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
||||
|
||||
def test_versioned_endpoint_with_version_hostname_v1(self):
|
||||
gc = client.Client(endpoint="http://v2.example.com/v1")
|
||||
self.assertEqual("http://v2.example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
||||
|
||||
def test_versioned_endpoint_with_minor_revision_and_version_hostname(self):
|
||||
gc = client.Client(endpoint="http://v1.example.com/v1.1")
|
||||
self.assertEqual("http://v1.example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
411
glareclient/tests/unit/test_http.py
Normal file
411
glareclient/tests/unit/test_http.py
Normal file
@@ -0,0 +1,411 @@
|
||||
# Copyright 2012 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.
|
||||
import functools
|
||||
import json
|
||||
|
||||
from keystoneauth1 import session
|
||||
from keystoneauth1 import token_endpoint
|
||||
|
||||
import mock
|
||||
import requests
|
||||
from requests_mock.contrib import fixture
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
from testscenarios import load_tests_apply_scenarios as load_tests # noqa
|
||||
import testtools
|
||||
from testtools import matchers
|
||||
import types
|
||||
|
||||
import glareclient
|
||||
from glareclient.common import http
|
||||
from glareclient.tests import utils
|
||||
|
||||
|
||||
def original_only(f):
|
||||
@functools.wraps(f)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if not hasattr(self.client, 'log_curl_request'):
|
||||
self.skipTest('Skip logging tests for session client')
|
||||
|
||||
return f(self, *args, **kwargs)
|
||||
|
||||
|
||||
class TestClient(testtools.TestCase):
|
||||
|
||||
scenarios = [
|
||||
('httpclient', {'create_client': '_create_http_client'}),
|
||||
('session', {'create_client': '_create_session_client'})
|
||||
]
|
||||
|
||||
def _create_http_client(self):
|
||||
return http.HTTPClient(self.endpoint, token=self.token)
|
||||
|
||||
def _create_session_client(self):
|
||||
auth = token_endpoint.Token(self.endpoint, self.token)
|
||||
sess = session.Session(auth=auth)
|
||||
return http.SessionClient(sess)
|
||||
|
||||
def setUp(self):
|
||||
super(TestClient, self).setUp()
|
||||
self.mock = self.useFixture(fixture.Fixture())
|
||||
|
||||
self.endpoint = 'http://example.com:9292'
|
||||
self.ssl_endpoint = 'https://example.com:9292'
|
||||
self.token = u'abc123'
|
||||
|
||||
self.client = getattr(self, self.create_client)()
|
||||
|
||||
def test_identity_headers_and_token(self):
|
||||
identity_headers = {
|
||||
'X-Auth-Token': 'auth_token',
|
||||
'X-User-Id': 'user',
|
||||
'X-Tenant-Id': 'tenant',
|
||||
'X-Roles': 'roles',
|
||||
'X-Identity-Status': 'Confirmed',
|
||||
'X-Service-Catalog': 'service_catalog',
|
||||
}
|
||||
# with token
|
||||
kwargs = {'token': u'fake-token',
|
||||
'identity_headers': identity_headers}
|
||||
http_client_object = http.HTTPClient(self.endpoint, **kwargs)
|
||||
self.assertEqual('auth_token', http_client_object.auth_token)
|
||||
self.assertTrue(http_client_object.identity_headers.
|
||||
get('X-Auth-Token') is None)
|
||||
|
||||
def test_identity_headers_and_no_token_in_header(self):
|
||||
identity_headers = {
|
||||
'X-User-Id': 'user',
|
||||
'X-Tenant-Id': 'tenant',
|
||||
'X-Roles': 'roles',
|
||||
'X-Identity-Status': 'Confirmed',
|
||||
'X-Service-Catalog': 'service_catalog',
|
||||
}
|
||||
# without X-Auth-Token in identity headers
|
||||
kwargs = {'token': u'fake-token',
|
||||
'identity_headers': identity_headers}
|
||||
http_client_object = http.HTTPClient(self.endpoint, **kwargs)
|
||||
self.assertEqual(u'fake-token', http_client_object.auth_token)
|
||||
self.assertTrue(http_client_object.identity_headers.
|
||||
get('X-Auth-Token') is None)
|
||||
|
||||
def test_identity_headers_and_no_token_in_session_header(self):
|
||||
# Tests that if token or X-Auth-Token are not provided in the kwargs
|
||||
# when creating the http client, the session headers don't contain
|
||||
# the X-Auth-Token key.
|
||||
identity_headers = {
|
||||
'X-User-Id': 'user',
|
||||
'X-Tenant-Id': 'tenant',
|
||||
'X-Roles': 'roles',
|
||||
'X-Identity-Status': 'Confirmed',
|
||||
'X-Service-Catalog': 'service_catalog',
|
||||
}
|
||||
kwargs = {'identity_headers': identity_headers}
|
||||
http_client_object = http.HTTPClient(self.endpoint, **kwargs)
|
||||
self.assertIsNone(http_client_object.auth_token)
|
||||
self.assertNotIn('X-Auth-Token', http_client_object.session.headers)
|
||||
|
||||
def test_identity_headers_are_passed(self):
|
||||
# Tests that if token or X-Auth-Token are not provided in the kwargs
|
||||
# when creating the http client, the session headers don't contain
|
||||
# the X-Auth-Token key.
|
||||
identity_headers = {
|
||||
'X-User-Id': b'user',
|
||||
'X-Tenant-Id': b'tenant',
|
||||
'X-Roles': b'roles',
|
||||
'X-Identity-Status': b'Confirmed',
|
||||
'X-Service-Catalog': b'service_catalog',
|
||||
}
|
||||
kwargs = {'identity_headers': identity_headers}
|
||||
http_client = http.HTTPClient(self.endpoint, **kwargs)
|
||||
|
||||
path = '/artifactsmy-image'
|
||||
self.mock.get(self.endpoint + path)
|
||||
http_client.get(path)
|
||||
|
||||
headers = self.mock.last_request.headers
|
||||
for k, v in six.iteritems(identity_headers):
|
||||
self.assertEqual(v, headers[k])
|
||||
|
||||
def test_language_header_passed(self):
|
||||
kwargs = {'language_header': 'nb_NO'}
|
||||
http_client = http.HTTPClient(self.endpoint, **kwargs)
|
||||
|
||||
path = '/v2/images/my-image'
|
||||
self.mock.get(self.endpoint + path)
|
||||
http_client.get(path)
|
||||
|
||||
headers = self.mock.last_request.headers
|
||||
self.assertEqual(kwargs['language_header'], headers['Accept-Language'])
|
||||
|
||||
def test_language_header_not_passed_no_language(self):
|
||||
kwargs = {}
|
||||
http_client = http.HTTPClient(self.endpoint, **kwargs)
|
||||
|
||||
path = '/v2/images/my-image'
|
||||
self.mock.get(self.endpoint + path)
|
||||
http_client.get(path)
|
||||
|
||||
headers = self.mock.last_request.headers
|
||||
self.assertTrue('Accept-Language' not in headers)
|
||||
|
||||
def test_connection_timeout(self):
|
||||
"""Verify a InvalidEndpoint is received if connection times out."""
|
||||
def cb(request, context):
|
||||
raise requests.exceptions.Timeout
|
||||
|
||||
path = '/v1/images'
|
||||
self.mock.get(self.endpoint + path, text=cb)
|
||||
comm_err = self.assertRaises(glareclient.exc.InvalidEndpoint,
|
||||
self.client.get,
|
||||
'/v1/images')
|
||||
self.assertIn(self.endpoint, comm_err.message)
|
||||
|
||||
def test_connection_refused(self):
|
||||
"""Verify a CommunicationError is received if connection is refused.
|
||||
|
||||
The error should list the host and port that refused the connection.
|
||||
"""
|
||||
def cb(request, context):
|
||||
raise requests.exceptions.ConnectionError()
|
||||
|
||||
path = '/artifacts/?limit=20'
|
||||
self.mock.get(self.endpoint + path, text=cb)
|
||||
|
||||
comm_err = self.assertRaises(glareclient.exc.CommunicationError,
|
||||
self.client.get,
|
||||
'/artifacts/?limit=20')
|
||||
|
||||
self.assertIn(self.endpoint, comm_err.message)
|
||||
|
||||
def test_http_encoding(self):
|
||||
path = '/artifacts/'
|
||||
text = 'Ok'
|
||||
self.mock.get(self.endpoint + path, text=text,
|
||||
headers={"Content-Type": "text/plain"})
|
||||
|
||||
headers = {"test": u'ni\xf1o'}
|
||||
resp, body = self.client.get(path, headers=headers)
|
||||
self.assertEqual(text, resp.text)
|
||||
|
||||
def test_request_id(self):
|
||||
path = '/artifacts/'
|
||||
self.mock.get(self.endpoint + path,
|
||||
headers={"x-openstack-request-id": "req-aaa"})
|
||||
|
||||
self.client.get(path)
|
||||
self.assertEqual(self.client.last_request_id, 'req-aaa')
|
||||
|
||||
def test_headers_encoding(self):
|
||||
value = u'ni\xf1o'
|
||||
headers = {"test": value, "none-val": None}
|
||||
encoded = http.encode_headers(headers)
|
||||
self.assertEqual(b"ni\xc3\xb1o", encoded[b"test"])
|
||||
self.assertNotIn("none-val", encoded)
|
||||
|
||||
def test_raw_request(self):
|
||||
"""Verify the path being used for HTTP requests reflects accurately."""
|
||||
headers = {"Content-Type": "text/plain"}
|
||||
text = 'Ok'
|
||||
path = '/artifacts/'
|
||||
|
||||
self.mock.get(self.endpoint + path, text=text, headers=headers)
|
||||
|
||||
resp, body = self.client.get('/artifacts/', headers=headers)
|
||||
self.assertEqual(headers, resp.headers)
|
||||
self.assertEqual(text, resp.text)
|
||||
|
||||
def test_parse_endpoint(self):
|
||||
endpoint = 'http://example.com:9292'
|
||||
test_client = http.HTTPClient(endpoint, token=u'adc123')
|
||||
actual = test_client.parse_endpoint(endpoint)
|
||||
expected = parse.SplitResult(scheme='http',
|
||||
netloc='example.com:9292', path='',
|
||||
query='', fragment='')
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_get_connections_kwargs_http(self):
|
||||
endpoint = 'http://example.com:9292'
|
||||
test_client = http.HTTPClient(endpoint, token=u'adc123')
|
||||
self.assertEqual(600.0, test_client.timeout)
|
||||
|
||||
def test__chunk_body_exact_size_chunk(self):
|
||||
test_client = http._BaseHTTPClient()
|
||||
bytestring = b'x' * http.CHUNKSIZE
|
||||
data = six.BytesIO(bytestring)
|
||||
chunk = list(test_client._chunk_body(data))
|
||||
self.assertEqual(1, len(chunk))
|
||||
self.assertEqual([bytestring], chunk)
|
||||
|
||||
def test_http_chunked_request(self):
|
||||
text = "Ok"
|
||||
data = six.StringIO(text)
|
||||
path = '/artifacts'
|
||||
self.mock.post(self.endpoint + path, text=text)
|
||||
|
||||
headers = {"test": u'chunked_request'}
|
||||
resp, body = self.client.post(path, headers=headers, data=data)
|
||||
self.assertIsInstance(self.mock.last_request.body, types.GeneratorType)
|
||||
self.assertEqual(text, resp.text)
|
||||
|
||||
def test_http_json(self):
|
||||
data = {"test": "json_request"}
|
||||
path = '/artifacts'
|
||||
text = 'OK'
|
||||
self.mock.post(self.endpoint + path, text=text)
|
||||
|
||||
headers = {"test": u'chunked_request'}
|
||||
resp, body = self.client.post(path, headers=headers, data=data)
|
||||
|
||||
self.assertEqual(text, resp.text)
|
||||
self.assertIsInstance(self.mock.last_request.body, six.string_types)
|
||||
self.assertEqual(data, json.loads(self.mock.last_request.body))
|
||||
|
||||
def test_http_chunked_response(self):
|
||||
data = "TEST"
|
||||
path = '/artifacts'
|
||||
self.mock.get(self.endpoint + path, body=six.StringIO(data),
|
||||
headers={"Content-Type": "application/octet-stream"})
|
||||
|
||||
resp, body = self.client.get(path)
|
||||
self.assertIsInstance(body, types.GeneratorType)
|
||||
self.assertEqual([data], list(body))
|
||||
|
||||
@original_only
|
||||
def test_log_http_response_with_non_ascii_char(self):
|
||||
try:
|
||||
response = 'Ok'
|
||||
headers = {"Content-Type": "text/plain",
|
||||
"test": "value1\xa5\xa6"}
|
||||
fake = utils.FakeResponse(headers, six.StringIO(response))
|
||||
self.client.log_http_response(fake)
|
||||
except UnicodeDecodeError as e:
|
||||
self.fail("Unexpected UnicodeDecodeError exception '%s'" % e)
|
||||
|
||||
@original_only
|
||||
def test_log_curl_request_with_non_ascii_char(self):
|
||||
try:
|
||||
headers = {'header1': 'value1\xa5\xa6'}
|
||||
body = 'examplebody\xa5\xa6'
|
||||
self.client.log_curl_request('GET', '/api/v1/\xa5', headers, body,
|
||||
None)
|
||||
except UnicodeDecodeError as e:
|
||||
self.fail("Unexpected UnicodeDecodeError exception '%s'" % e)
|
||||
|
||||
@original_only
|
||||
@mock.patch('glareclient.common.http.LOG.debug')
|
||||
def test_log_curl_request_with_body_and_header(self, mock_log):
|
||||
hd_name = 'header1'
|
||||
hd_val = 'value1'
|
||||
headers = {hd_name: hd_val}
|
||||
body = 'examplebody'
|
||||
self.client.log_curl_request('GET', '/api/v1/', headers, body, None)
|
||||
self.assertTrue(mock_log.called, 'LOG.debug never called')
|
||||
self.assertTrue(mock_log.call_args[0],
|
||||
'LOG.debug called with no arguments')
|
||||
hd_regex = ".*\s-H\s+'\s*%s\s*:\s*%s\s*'.*" % (hd_name, hd_val)
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.MatchesRegex(hd_regex),
|
||||
'header not found in curl command')
|
||||
body_regex = ".*\s-d\s+'%s'\s.*" % body
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.MatchesRegex(body_regex),
|
||||
'body not found in curl command')
|
||||
|
||||
def _test_log_curl_request_with_certs(self, mock_log, key, cert, cacert):
|
||||
headers = {'header1': 'value1'}
|
||||
http_client_object = http.HTTPClient(self.ssl_endpoint, key_file=key,
|
||||
cert_file=cert, cacert=cacert,
|
||||
token='fake-token')
|
||||
http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
|
||||
None)
|
||||
self.assertTrue(mock_log.called, 'LOG.debug never called')
|
||||
self.assertTrue(mock_log.call_args[0],
|
||||
'LOG.debug called with no arguments')
|
||||
|
||||
needles = {'key': key, 'cert': cert, 'cacert': cacert}
|
||||
for option, value in six.iteritems(needles):
|
||||
if value:
|
||||
regex = ".*\s--%s\s+('%s'|%s).*" % (option, value, value)
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.MatchesRegex(regex),
|
||||
'no --%s option in curl command' % option)
|
||||
else:
|
||||
regex = ".*\s--%s\s+.*" % option
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.Not(matchers.MatchesRegex(regex)),
|
||||
'unexpected --%s option in curl command' %
|
||||
option)
|
||||
|
||||
@mock.patch('glareclient.common.http.LOG.debug')
|
||||
def test_log_curl_request_with_all_certs(self, mock_log):
|
||||
self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1',
|
||||
'cacert2')
|
||||
|
||||
@mock.patch('glareclient.common.http.LOG.debug')
|
||||
def test_log_curl_request_with_some_certs(self, mock_log):
|
||||
self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1', None)
|
||||
|
||||
@mock.patch('glareclient.common.http.LOG.debug')
|
||||
def test_log_curl_request_with_insecure_param(self, mock_log):
|
||||
headers = {'header1': 'value1'}
|
||||
http_client_object = http.HTTPClient(self.ssl_endpoint, insecure=True,
|
||||
token='fake-token')
|
||||
http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
|
||||
None)
|
||||
self.assertTrue(mock_log.called, 'LOG.debug never called')
|
||||
self.assertTrue(mock_log.call_args[0],
|
||||
'LOG.debug called with no arguments')
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.MatchesRegex('.*\s-k\s.*'),
|
||||
'no -k option in curl command')
|
||||
|
||||
@mock.patch('glareclient.common.http.LOG.debug')
|
||||
def test_log_curl_request_with_token_header(self, mock_log):
|
||||
fake_token = 'fake-token'
|
||||
headers = {'X-Auth-Token': fake_token}
|
||||
http_client_object = http.HTTPClient(self.endpoint,
|
||||
identity_headers=headers)
|
||||
http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
|
||||
None)
|
||||
self.assertTrue(mock_log.called, 'LOG.debug never called')
|
||||
self.assertTrue(mock_log.call_args[0],
|
||||
'LOG.debug called with no arguments')
|
||||
token_regex = '.*%s.*' % fake_token
|
||||
self.assertThat(mock_log.call_args[0][0],
|
||||
matchers.Not(matchers.MatchesRegex(token_regex)),
|
||||
'token found in LOG.debug parameter')
|
||||
|
||||
def test_expired_token_has_changed(self):
|
||||
# instantiate client with some token
|
||||
fake_token = b'fake-token'
|
||||
http_client = http.HTTPClient(self.endpoint,
|
||||
token=fake_token)
|
||||
path = '/artifacts'
|
||||
self.mock.get(self.endpoint + path)
|
||||
http_client.get(path)
|
||||
headers = self.mock.last_request.headers
|
||||
self.assertEqual(fake_token, headers['X-Auth-Token'])
|
||||
# refresh the token
|
||||
refreshed_token = b'refreshed-token'
|
||||
http_client.auth_token = refreshed_token
|
||||
http_client.get(path)
|
||||
headers = self.mock.last_request.headers
|
||||
self.assertEqual(refreshed_token, headers['X-Auth-Token'])
|
||||
# regression check for bug 1448080
|
||||
unicode_token = u'ni\xf1o'
|
||||
http_client.auth_token = unicode_token
|
||||
http_client.get(path)
|
||||
headers = self.mock.last_request.headers
|
||||
self.assertEqual(b'ni\xc3\xb1o', headers['X-Auth-Token'])
|
75
glareclient/tests/unit/test_progressbar.py
Normal file
75
glareclient/tests/unit/test_progressbar.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# 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.
|
||||
|
||||
import sys
|
||||
|
||||
import six
|
||||
import testtools
|
||||
|
||||
from glareclient.common import progressbar
|
||||
from glareclient.tests import utils as test_utils
|
||||
|
||||
|
||||
class TestProgressBarWrapper(testtools.TestCase):
|
||||
|
||||
def test_iter_iterator_display_progress_bar(self):
|
||||
size = 100
|
||||
iterator = iter('X' * 100)
|
||||
saved_stdout = sys.stdout
|
||||
try:
|
||||
sys.stdout = output = test_utils.FakeTTYStdout()
|
||||
# Consume iterator.
|
||||
data = list(progressbar.VerboseIteratorWrapper(iterator, size))
|
||||
self.assertEqual(['X'] * 100, data)
|
||||
self.assertEqual(
|
||||
'[%s>] 100%%\n' % ('=' * 29),
|
||||
output.getvalue()
|
||||
)
|
||||
finally:
|
||||
sys.stdout = saved_stdout
|
||||
|
||||
def test_iter_file_display_progress_bar(self):
|
||||
size = 98304
|
||||
file_obj = six.StringIO('X' * size)
|
||||
saved_stdout = sys.stdout
|
||||
try:
|
||||
sys.stdout = output = test_utils.FakeTTYStdout()
|
||||
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
|
||||
chunksize = 1024
|
||||
chunk = file_obj.read(chunksize)
|
||||
while chunk:
|
||||
chunk = file_obj.read(chunksize)
|
||||
self.assertEqual(
|
||||
'[%s>] 100%%\n' % ('=' * 29),
|
||||
output.getvalue()
|
||||
)
|
||||
finally:
|
||||
sys.stdout = saved_stdout
|
||||
|
||||
def test_iter_file_no_tty(self):
|
||||
size = 98304
|
||||
file_obj = six.StringIO('X' * size)
|
||||
saved_stdout = sys.stdout
|
||||
try:
|
||||
sys.stdout = output = test_utils.FakeNoTTYStdout()
|
||||
file_obj = progressbar.VerboseFileWrapper(file_obj, size)
|
||||
chunksize = 1024
|
||||
chunk = file_obj.read(chunksize)
|
||||
while chunk:
|
||||
chunk = file_obj.read(chunksize)
|
||||
# If stdout is not a tty progress bar should do nothing.
|
||||
self.assertEqual('', output.getvalue())
|
||||
finally:
|
||||
sys.stdout = saved_stdout
|
226
glareclient/tests/unit/test_ssl.py
Normal file
226
glareclient/tests/unit/test_ssl.py
Normal file
@@ -0,0 +1,226 @@
|
||||
# Copyright 2012 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.
|
||||
|
||||
import os
|
||||
|
||||
import mock
|
||||
import six
|
||||
import ssl
|
||||
import testtools
|
||||
import threading
|
||||
|
||||
from glareclient import Client
|
||||
from glareclient import exc
|
||||
from glareclient import v1
|
||||
|
||||
if six.PY3 is True:
|
||||
import socketserver
|
||||
else:
|
||||
import SocketServer as socketserver
|
||||
|
||||
|
||||
TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'var'))
|
||||
|
||||
|
||||
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
self.request.recv(1024)
|
||||
response = b'somebytes'
|
||||
self.request.sendall(response)
|
||||
|
||||
|
||||
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
def get_request(self):
|
||||
key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
|
||||
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
|
||||
(_sock, addr) = socketserver.TCPServer.get_request(self)
|
||||
sock = ssl.wrap_socket(_sock,
|
||||
certfile=cert_file,
|
||||
keyfile=key_file,
|
||||
ca_certs=cacert,
|
||||
server_side=True,
|
||||
cert_reqs=ssl.CERT_REQUIRED)
|
||||
return sock, addr
|
||||
|
||||
|
||||
class TestHTTPSVerifyCert(testtools.TestCase):
|
||||
"""Check 'requests' based ssl verification occurs.
|
||||
|
||||
The requests library performs SSL certificate validation,
|
||||
however there is still a need to check that the glare
|
||||
client is properly integrated with requests so that
|
||||
cert validation actually happens.
|
||||
"""
|
||||
def setUp(self):
|
||||
# Rather than spinning up a new process, we create
|
||||
# a thread to perform client/server interaction.
|
||||
# This should run more quickly.
|
||||
super(TestHTTPSVerifyCert, self).setUp()
|
||||
server = ThreadedTCPServer(('127.0.0.1', 0),
|
||||
ThreadedTCPRequestHandler)
|
||||
__, self.port = server.server_address
|
||||
server_thread = threading.Thread(target=server.serve_forever)
|
||||
server_thread.daemon = True
|
||||
server_thread.start()
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_cert_verification(self, __):
|
||||
"""v1 regression test for bug 115260."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
|
||||
try:
|
||||
client = v1.Client(url,
|
||||
insecure=False,
|
||||
ssl_compression=True)
|
||||
client.artifacts.get('123', type_name='sample_artifact')
|
||||
self.fail('No SSL exception has been raised')
|
||||
except exc.CommunicationError as e:
|
||||
if 'certificate verify failed' not in e.message:
|
||||
self.fail('No certificate failure message is received')
|
||||
except Exception:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_cert_verification_no_compression(self, __):
|
||||
"""v1 regression test for bug 115260."""
|
||||
# Legacy test. Verify 'no compression' has no effect
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
|
||||
try:
|
||||
client = v1.Client(url,
|
||||
insecure=False,
|
||||
ssl_compression=False)
|
||||
client.artifacts.get('123', type_name='sample_artifact')
|
||||
self.fail('No SSL exception has been raised')
|
||||
except exc.CommunicationError as e:
|
||||
if 'certificate verify failed' not in e.message:
|
||||
self.fail('No certificate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_valid_cert_verification(self, __):
|
||||
"""Test absence of SSL key file."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
|
||||
|
||||
try:
|
||||
gc = Client('1', url,
|
||||
insecure=False,
|
||||
ssl_compression=True,
|
||||
cacert=cacert)
|
||||
gc.artifacts.get('123', type_name='sample_artifact')
|
||||
except exc.CommunicationError as e:
|
||||
if 'certificate verify failed' in e.message:
|
||||
self.fail('Certificate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_valid_cert_verification_no_compression(self, __):
|
||||
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
|
||||
|
||||
try:
|
||||
gc = Client('1', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cacert=cacert)
|
||||
gc.artifacts.get('123', type_name='sample_artifact')
|
||||
except exc.CommunicationError as e:
|
||||
if 'certificate verify failed' in e.message:
|
||||
self.fail('Certificate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_valid_cert_no_key(self, __):
|
||||
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
|
||||
|
||||
try:
|
||||
gc = Client('1', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cert_file=cert_file,
|
||||
cacert=cacert)
|
||||
gc.artifacts.get('123', type_name='sample_artifact')
|
||||
except exc.CommunicationError as e:
|
||||
if ('PEM lib' not in e.message):
|
||||
self.fail('No appropriate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_bad_cert(self, __):
|
||||
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
cert_file = os.path.join(TEST_VAR_DIR, 'badcert.crt')
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
|
||||
|
||||
try:
|
||||
gc = Client('1', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cert_file=cert_file,
|
||||
cacert=cacert)
|
||||
gc.artifacts.get('123', type_name='sample_artifact')
|
||||
except exc.CommunicationError as e:
|
||||
# NOTE(dsariel)
|
||||
# starting from python 2.7.8 the way to handle loading private
|
||||
# keys into the SSL_CTX was changed and error message become
|
||||
# similar to the one in 3.X
|
||||
if (six.PY2 and 'PrivateKey' not in e.message and
|
||||
'PEM lib' not in e.message or
|
||||
six.PY3 and 'PEM lib' not in e.message):
|
||||
self.fail('No appropriate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v1_requests_bad_ca(self, __):
|
||||
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
cacert = os.path.join(TEST_VAR_DIR, 'badca.crt')
|
||||
|
||||
try:
|
||||
gc = Client('1', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cacert=cacert)
|
||||
gc.artifacts.get('123', type_name='sample_artifact')
|
||||
except exc.CommunicationError as e:
|
||||
# NOTE(dsariel)
|
||||
# starting from python 2.7.8 the way of handling x509 certificates
|
||||
# was changed (github.com/python/peps/blob/master/pep-0476.txt#L28)
|
||||
# and error message become similar to the one in 3.X
|
||||
if (six.PY2 and 'certificate' not in e.message and
|
||||
'No such file' not in e.message or
|
||||
six.PY3 and 'No such file' not in e.message):
|
||||
self.fail('No appropriate failure message is received')
|
||||
except Exception as e:
|
||||
self.fail('Unexpected exception has been raised')
|
83
glareclient/tests/unit/test_utils.py
Normal file
83
glareclient/tests/unit/test_utils.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# Copyright 2012 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.
|
||||
|
||||
|
||||
import mock
|
||||
from oslo_utils import encodeutils
|
||||
import six
|
||||
import testtools
|
||||
|
||||
from glareclient.common import utils
|
||||
|
||||
|
||||
class TestUtils(testtools.TestCase):
|
||||
|
||||
def test_make_size_human_readable(self):
|
||||
self.assertEqual("106B", utils.make_size_human_readable(106))
|
||||
self.assertEqual("1000kB", utils.make_size_human_readable(1024000))
|
||||
self.assertEqual("1MB", utils.make_size_human_readable(1048576))
|
||||
self.assertEqual("1.4GB", utils.make_size_human_readable(1476395008))
|
||||
self.assertEqual("9.3MB", utils.make_size_human_readable(9761280))
|
||||
self.assertEqual("0B", utils.make_size_human_readable(None))
|
||||
|
||||
def test_get_new_file_size(self):
|
||||
size = 98304
|
||||
file_obj = six.StringIO('X' * size)
|
||||
try:
|
||||
self.assertEqual(size, utils.get_file_size(file_obj))
|
||||
# Check that get_file_size didn't change original file position.
|
||||
self.assertEqual(0, file_obj.tell())
|
||||
finally:
|
||||
file_obj.close()
|
||||
|
||||
def test_get_consumed_file_size(self):
|
||||
size, consumed = 98304, 304
|
||||
file_obj = six.StringIO('X' * size)
|
||||
file_obj.seek(consumed)
|
||||
try:
|
||||
self.assertEqual(size, utils.get_file_size(file_obj))
|
||||
# Check that get_file_size didn't change original file position.
|
||||
self.assertEqual(consumed, file_obj.tell())
|
||||
finally:
|
||||
file_obj.close()
|
||||
|
||||
def test_iterable_closes(self):
|
||||
# Regression test for bug 1461678.
|
||||
def _iterate(i):
|
||||
for chunk in i:
|
||||
raise(IOError)
|
||||
|
||||
data = six.moves.StringIO('somestring')
|
||||
data.close = mock.Mock()
|
||||
i = utils.IterableWithLength(data, 10)
|
||||
self.assertRaises(IOError, _iterate, i)
|
||||
data.close.assert_called_with()
|
||||
|
||||
def test_safe_header(self):
|
||||
self.assertEqual(('somekey', 'somevalue'),
|
||||
utils.safe_header('somekey', 'somevalue'))
|
||||
self.assertEqual(('somekey', None),
|
||||
utils.safe_header('somekey', None))
|
||||
|
||||
for sensitive_header in utils.SENSITIVE_HEADERS:
|
||||
(name, value) = utils.safe_header(
|
||||
sensitive_header,
|
||||
encodeutils.safe_encode('somestring'))
|
||||
self.assertEqual(sensitive_header, name)
|
||||
self.assertTrue(value.startswith("{SHA1}"))
|
||||
|
||||
(name, value) = utils.safe_header(sensitive_header, None)
|
||||
self.assertEqual(sensitive_header, name)
|
||||
self.assertIsNone(value)
|
0
glareclient/tests/unit/v1/__init__.py
Normal file
0
glareclient/tests/unit/v1/__init__.py
Normal file
59
glareclient/tests/unit/v1/fixtures.py
Normal file
59
glareclient/tests/unit/v1/fixtures.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Copyright 2016 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.
|
||||
|
||||
data_fixtures = {
|
||||
'/artifacts/sample_artifact?limit=20': {
|
||||
'GET': (
|
||||
# headers
|
||||
{},
|
||||
# response
|
||||
{'sample_artifact': [
|
||||
{
|
||||
'name': 'art1',
|
||||
'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
|
||||
'version': '0.0.0'
|
||||
},
|
||||
{
|
||||
'name': 'art2',
|
||||
'id': 'db721fb0-5b85-4738-9401-f161d541de5e',
|
||||
'version': '0.0.0'
|
||||
},
|
||||
{
|
||||
'name': 'art3',
|
||||
'id': 'e4f027d2-bff3-4084-a2ba-f31cb5e3067f',
|
||||
'version': '0.0.0'
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/artifacts/sample_artifact&limit=2': {
|
||||
'GET': (
|
||||
{},
|
||||
{'sample_artifact': [
|
||||
{
|
||||
'name': 'art1',
|
||||
'id': '3a4560a1-e585-443e-9b39-553b46ec92d1',
|
||||
'version': '0.0.0'
|
||||
},
|
||||
{
|
||||
'name': 'art2',
|
||||
'id': 'e4f027d2-bff3-4084-a2ba-f31cb5e3067f',
|
||||
'version': '0.0.0'
|
||||
}],
|
||||
'next': '/artifacts/sample_artifact?'
|
||||
'marker=e1090471-1d12-4935-a8d8-a9351266ece8&limit=2'},
|
||||
),
|
||||
},
|
||||
}
|
39
glareclient/tests/unit/v1/test_artifacts.py
Normal file
39
glareclient/tests/unit/v1/test_artifacts.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# Copyright 2016 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.
|
||||
|
||||
import testtools
|
||||
|
||||
from glareclient.tests.unit.v1 import fixtures
|
||||
from glareclient.tests import utils
|
||||
from glareclient.v1 import artifacts
|
||||
|
||||
|
||||
class TestController(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestController, self).setUp()
|
||||
self.api = utils.FakeAPI(fixtures.data_fixtures)
|
||||
self.controller = artifacts.Controller(self.api)
|
||||
|
||||
def test_list_artifacts(self):
|
||||
artifacts = list(self.controller.list(type_name='sample_artifact'))
|
||||
self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1',
|
||||
artifacts[0]['id'])
|
||||
self.assertEqual('art1', artifacts[0]['name'])
|
||||
self.assertEqual('db721fb0-5b85-4738-9401-f161d541de5e',
|
||||
artifacts[1]['id'])
|
||||
self.assertEqual('art2', artifacts[1]['name'])
|
||||
self.assertEqual('e4f027d2-bff3-4084-a2ba-f31cb5e3067f',
|
||||
artifacts[2]['id'])
|
||||
self.assertEqual('art3', artifacts[2]['name'])
|
0
glareclient/tests/unit/var/badcert.crt
Normal file
0
glareclient/tests/unit/var/badcert.crt
Normal file
34
glareclient/tests/unit/var/ca.crt
Normal file
34
glareclient/tests/unit/var/ca.crt
Normal file
@@ -0,0 +1,34 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIF7jCCA9YCCQDbl9qx7iIeJDANBgkqhkiG9w0BAQUFADCBuDEZMBcGA1UEChMQ
|
||||
T3BlbnN0YWNrIENBIE9yZzEaMBgGA1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAh
|
||||
BgkqhkiG9w0BCQEWFGFkbWluQGNhLmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0
|
||||
ZSBDQTELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3Rh
|
||||
Y2sgVGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTIxMTE2MTI1MDE2WhcN
|
||||
NDAwNDAzMTI1MDE2WjCBuDEZMBcGA1UEChMQT3BlbnN0YWNrIENBIE9yZzEaMBgG
|
||||
A1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAhBgkqhkiG9w0BCQEWFGFkbWluQGNh
|
||||
LmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0ZSBDQTELMAkGA1UECBMCQ0ExCzAJ
|
||||
BgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3RhY2sgVGVzdCBDZXJ0aWZpY2F0ZSBB
|
||||
dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC94cpBjwj2
|
||||
MD0w5j1Jlcy8Ljmk3r7CRaoV5vhWUrAWpT7Thxr/Ti0qAfZZRSIVpvBM0RlseH0Q
|
||||
toUJixuYMoNRPUQ74r/TRoO8HfjQDJfnXtWg2L7DRP8p4Zgj3vByBUCU+rKsbI/H
|
||||
Nssl/AronADbZXCoL5hJRN8euMYZGrt/Gh1ZotKE5gQlEjylDFlA3s3pn+ABLgzf
|
||||
7L7iufwV3zLdPRHCb6Ve8YvUmKfI6gy+WwTRhNhLz4Nj0uBthnj6QhnRXtxkNT7A
|
||||
aAStqKH6TtYRnk2Owh8ITFbtLQ0/MSV8jHAxMXx9AloBhEKxv3cIpgLH6lOCnj//
|
||||
Ql+H6/QWtmTUHzP1kBfMhTQnWTfR92QTcgEMiZ7a07VyVtLh+kp/G5IUqpM6Pyz/
|
||||
O6QDs7FF69bTpws7Ce916PPrGFZ9Gqvo/P0jXge8kYqO+a8QnTRldAxdUzPJCK9+
|
||||
Dyi2LWeHf8nPFYdwW9Ov6Jw1CKDYxjJg6KIwnrMPa2eUdPB6/OKkqr9/KemOoKQu
|
||||
4KSaYadFZbaJwt7JPZaHy6TpkGxW7Af8RqGrW6a6nWEFcfO2POuHcAHWL5LiRmni
|
||||
unm60DBF3b3itDTqCvER3mZE9pN8dqtxdpB8SUX8eq0UJJK2K8mJQS+oE9crbqYb
|
||||
1kQbYjhhPLlvOQru+/m/abqZrC04u2OtYQIDAQABMA0GCSqGSIb3DQEBBQUAA4IC
|
||||
AQA8wGVBbzfpQ3eYpchiHyHF9N5LIhr6Bt4jYDKLz8DIbElLtoOlgH/v7hLGJ7wu
|
||||
R9OteonwQ1qr9umMmnp61bKXOEBJLBJbGKEt0MNLmmX89+M/h3rdMVZEz/Hht/xK
|
||||
Xm4di8pjkHfmdhqsbiFW81lAt9W1r74lnH7wQHr9ueALGKDx0hi8pAZ27itgQVHL
|
||||
eA1erhw0kjr9BqWpDIskVwePcD7pFoZ48GQlST0uIEq5U+1AWq7AbOABsqODygKi
|
||||
Ri5pmTasNFT7nEX3ti4VN214MNy0JnPzTRNWR2rD0I30AebM3KkzTprbLVfnGkm4
|
||||
7hOPV+Wc8EjgbbrUAIp2YpOfO/9nbgljTOUsqfjqxzvHx/09XOo2M6NIE5UiHqIq
|
||||
TXN7CeGIhBoYbvBAH2QvtveFXv41IYL4zFFXo4wTBSzCCOUGeDDv0U4hhsNaCkDQ
|
||||
G2TcubNA4g/FAtqLvPj/6VbIIgFE/1/6acsT+W0O+kkVAb7ej2dpI7J+jKXDXuiA
|
||||
PDCMn9dVQ7oAcaQvVdvvRphLdIZ9wHgqKhxKsMwzIMExuDKL0lWe/3sueFyol6nv
|
||||
xRCSgzr5MqSObbO3EnWgcUocBvlPyYLnTM2T8C5wh3BGnJXqJSRETggNn8PXBVIm
|
||||
+c5o+Ic0mYu4v8P1ZSozFdgf+HLriVPwzJU5dHvvTEu7sw==
|
||||
-----END CERTIFICATE-----
|
66
glareclient/tests/unit/var/certificate.crt
Normal file
66
glareclient/tests/unit/var/certificate.crt
Normal file
@@ -0,0 +1,66 @@
|
||||
# Certificate:
|
||||
# Data:
|
||||
# Version: 3 (0x2)
|
||||
# Serial Number: 1 (0x1)
|
||||
# Signature Algorithm: sha1WithRSAEncryption
|
||||
# Issuer: O=Openstack CA Org, OU=Openstack Test CA/emailAddress=admin@ca.example.com,
|
||||
# L=State CA, ST=CA, C=AU, CN=Openstack Test Certificate Authority
|
||||
# Validity
|
||||
# Not Before: Nov 16 12:50:19 2012 GMT
|
||||
# Not After : Apr 3 12:50:19 2040 GMT
|
||||
# Subject: O=Openstack Test Org, OU=Openstack Test Unit/emailAddress=admin@example.com,
|
||||
# L=State1, ST=CA, C=US, CN=0.0.0.0
|
||||
# Subject Public Key Info:
|
||||
# Public Key Algorithm: rsaEncryption
|
||||
# RSA Public Key: (4096 bit)
|
||||
# Modulus (4096 bit):
|
||||
# 00:d4:bb:3a:c4:a0:06:54:31:23:5d:b0:78:5a:be:
|
||||
# 45:44:ae:a1:89:86:11:d8:ca:a8:33:b0:4f:f3:e1:
|
||||
# .
|
||||
# .
|
||||
# .
|
||||
# Exponent: 65537 (0x10001)
|
||||
# X509v3 extensions:
|
||||
# X509v3 Subject Alternative Name:
|
||||
# DNS:alt1.example.com, DNS:alt2.example.com
|
||||
# Signature Algorithm: sha1WithRSAEncryption
|
||||
# 2c:fc:5c:87:24:bd:4a:fa:40:d2:2e:35:a4:2a:f3:1c:b3:67:
|
||||
# b0:e4:8a:cd:67:6b:55:50:d4:cb:dd:2d:26:a5:15:62:90:a3:
|
||||
# .
|
||||
# .
|
||||
# .
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
tdc5rw==
|
||||
-----END CERTIFICATE-----
|
35
glareclient/tests/unit/var/expired-cert.crt
Normal file
35
glareclient/tests/unit/var/expired-cert.crt
Normal file
@@ -0,0 +1,35 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
l3+dFHnWayuCdqcFwIl3m8i8FupFihz9+A==
|
||||
-----END CERTIFICATE-----
|
51
glareclient/tests/unit/var/privatekey.key
Normal file
51
glareclient/tests/unit/var/privatekey.key
Normal file
@@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
61
glareclient/tests/unit/var/wildcard-certificate.crt
Normal file
61
glareclient/tests/unit/var/wildcard-certificate.crt
Normal file
@@ -0,0 +1,61 @@
|
||||
#Certificate:
|
||||
# Data:
|
||||
# Version: 1 (0x0)
|
||||
# Serial Number: 13493453254446411258 (0xbb42603e589dedfa)
|
||||
# Signature Algorithm: sha1WithRSAEncryption
|
||||
# Issuer: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=*.pong.example.com/emailAddress=admin@example.com
|
||||
# Validity
|
||||
# Not Before: Aug 21 17:29:18 2013 GMT
|
||||
# Not After : Jul 28 17:29:18 2113 GMT
|
||||
# Subject: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=*.pong.example.com/emailAddress=admin@example.com
|
||||
# Subject Public Key Info:
|
||||
# Public Key Algorithm: rsaEncryption
|
||||
# Public-Key: (4096 bit)
|
||||
# Modulus:
|
||||
# 00:d4:bb:3a:c4:a0:06:54:31:23:5d:b0:78:5a:be:
|
||||
# 45:44:ae:a1:89:86:11:d8:ca:a8:33:b0:4f:f3:e1:
|
||||
# 46:1e:85:a3:2a:9c:a4:e0:c2:14:34:4f:91:df:dc:
|
||||
# .
|
||||
# .
|
||||
# .
|
||||
# Exponent: 65537 (0x10001)
|
||||
# Signature Algorithm: sha1WithRSAEncryption
|
||||
# 9f:cc:08:5d:19:ee:54:31:a3:57:d7:3c:89:89:c0:69:41:dd:
|
||||
# 46:f8:73:68:ec:46:b9:fa:f5:df:f6:d9:58:35:d8:53:94:88:
|
||||
# bd:36:a6:23:9e:0c:0d:89:62:35:91:49:b6:14:f4:43:69:3c:
|
||||
# .
|
||||
# .
|
||||
# .
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
54
glareclient/tests/unit/var/wildcard-san-certificate.crt
Normal file
54
glareclient/tests/unit/var/wildcard-san-certificate.crt
Normal file
@@ -0,0 +1,54 @@
|
||||
#Certificate:
|
||||
# Data:
|
||||
# Version: 3 (0x2)
|
||||
# Serial Number: 11990626514780340979 (0xa66743493fdcc2f3)
|
||||
# Signature Algorithm: sha1WithRSAEncryption
|
||||
# Issuer: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=0.0.0.0
|
||||
# Validity
|
||||
# Not Before: Dec 10 15:31:22 2013 GMT
|
||||
# Not After : Nov 16 15:31:22 2113 GMT
|
||||
# Subject: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=0.0.0.0
|
||||
# Subject Public Key Info:
|
||||
# Public Key Algorithm: rsaEncryption
|
||||
# Public-Key: (2048 bit)
|
||||
# Modulus:
|
||||
# 00:ca:6b:07:73:53:24:45:74:05:a5:2a:27:bd:3e:
|
||||
# .
|
||||
# .
|
||||
# .
|
||||
# Exponent: 65537 (0x10001)
|
||||
# X509v3 extensions:
|
||||
# X509v3 Key Usage:
|
||||
# Key Encipherment, Data Encipherment
|
||||
# X509v3 Extended Key Usage:
|
||||
# TLS Web Server Authentication
|
||||
# X509v3 Subject Alternative Name:
|
||||
# DNS:foo.example.net, DNS:*.example.com
|
||||
# Signature Algorithm: sha1WithRSAEncryption
|
||||
# 7e:41:69:da:f4:3c:06:d6:83:c6:f2:db:df:37:f1:ac:fa:f5:
|
||||
# .
|
||||
# .
|
||||
# .
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDxDCCAqygAwIBAgIJAKZnQ0k/3MLzMA0GCSqGSIb3DQEBBQUAMHgxCzAJBgNV
|
||||
BAYTAlVTMQswCQYDVQQIEwJDQTEPMA0GA1UEBxMGU3RhdGUxMRswGQYDVQQKExJP
|
||||
cGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsTE09wZW5zdGFjayBUZXN0IFVuaXQx
|
||||
EDAOBgNVBAMTBzAuMC4wLjAwIBcNMTMxMjEwMTUzMTIyWhgPMjExMzExMTYxNTMx
|
||||
MjJaMHgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEPMA0GA1UEBxMGU3RhdGUx
|
||||
MRswGQYDVQQKExJPcGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsTE09wZW5zdGFj
|
||||
ayBUZXN0IFVuaXQxEDAOBgNVBAMTBzAuMC4wLjAwggEiMA0GCSqGSIb3DQEBAQUA
|
||||
A4IBDwAwggEKAoIBAQDKawdzUyRFdAWlKie9Pn10j7frffN+z1gEMluK2CtDEwv9
|
||||
kbD4uS/Kz4dujfTx03mdyNfiMVlOM+YJm/qeLLSdJyFyvZ9Y3WmJ+vT2RGlMMhLd
|
||||
/wEnMRrTYLL39pwI6z+gyw+4D78Pyv/OXy02IA6WtVEefYSx1vmVngb3pL+iBzhO
|
||||
8CZXNI6lqrFhh+Hr4iMkYMtY1vTnwezAL6p64E/ZAFNPYCEJlacESTLQ4VZYniHc
|
||||
QTgnE1czlI1vxlIk1KDXAzUGeeopZecRih9qlTxtOpklqEciQEE+sHtPcvyvdRE9
|
||||
Bdyx5rNSALLIcXs0ViJE1RPlw3fjdBoDIOygqvX1AgMBAAGjTzBNMAsGA1UdDwQE
|
||||
AwIEMDATBgNVHSUEDDAKBggrBgEFBQcDATApBgNVHREEIjAggg9mb28uZXhhbXBs
|
||||
ZS5uZXSCDSouZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEFBQADggEBAH5Badr0PAbW
|
||||
g8by29838az69Raul5IkpZQ5V3O1NaNNWxvmF1q8zFFqqGK5ktXJAwGiwnYEBb30
|
||||
Zfrr+eFIEERzBthSJkWlP8NG+2ooMyg50femp+asAvW+KYYefJW8KaXTsznMsAFy
|
||||
z1agcWVYVZ4H9PwunEYn/rM1krLEe4Cagsw5nmf8VqZg+hHtw930q8cRzgDsZdfA
|
||||
jVK6dWdmzmLCUTL1GKCeNriDw1jIeFvNufC+Q3orH7xBx4VL+NV5ORWdNY/B8q1b
|
||||
mFHdzbuZX6v39+2ww6aZqG2orfxUocc/5Ox6fXqenKPI3moeHS6Ktesq7sEQSJ6H
|
||||
QZFsTuT/124=
|
||||
-----END CERTIFICATE-----
|
214
glareclient/tests/utils.py
Normal file
214
glareclient/tests/utils.py
Normal file
@@ -0,0 +1,214 @@
|
||||
# Copyright 2012 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.
|
||||
|
||||
import copy
|
||||
import json
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
import testtools
|
||||
|
||||
|
||||
class FakeAPI(object):
|
||||
def __init__(self, fixtures):
|
||||
self.fixtures = fixtures
|
||||
self.calls = []
|
||||
|
||||
def _request(self, method, url, headers=None, data=None,
|
||||
content_length=None):
|
||||
call = build_call_record(method, sort_url_by_query_keys(url),
|
||||
headers or {}, data)
|
||||
if content_length is not None:
|
||||
call = tuple(list(call) + [content_length])
|
||||
self.calls.append(call)
|
||||
|
||||
fixture = self.fixtures[sort_url_by_query_keys(url)][method]
|
||||
|
||||
data = fixture[1]
|
||||
if isinstance(fixture[1], six.string_types):
|
||||
try:
|
||||
data = json.loads(fixture[1])
|
||||
except ValueError:
|
||||
data = six.StringIO(fixture[1])
|
||||
|
||||
return FakeResponse(fixture[0], fixture[1]), data
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self._request('GET', *args, **kwargs)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
return self._request('POST', *args, **kwargs)
|
||||
|
||||
def put(self, *args, **kwargs):
|
||||
return self._request('PUT', *args, **kwargs)
|
||||
|
||||
def patch(self, *args, **kwargs):
|
||||
return self._request('PATCH', *args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
return self._request('DELETE', *args, **kwargs)
|
||||
|
||||
def head(self, *args, **kwargs):
|
||||
return self._request('HEAD', *args, **kwargs)
|
||||
|
||||
|
||||
class RawRequest(object):
|
||||
def __init__(self, headers, body=None,
|
||||
version=1.0, status=200, reason="Ok"):
|
||||
"""A crafted request object used for testing.
|
||||
|
||||
:param headers: dict representing HTTP response headers
|
||||
:param body: file-like object
|
||||
:param version: HTTP Version
|
||||
:param status: Response status code
|
||||
:param reason: Status code related message.
|
||||
"""
|
||||
self.body = body
|
||||
self.status = status
|
||||
self.reason = reason
|
||||
self.version = version
|
||||
self.headers = headers
|
||||
|
||||
def getheaders(self):
|
||||
return copy.deepcopy(self.headers).items()
|
||||
|
||||
def getheader(self, key, default):
|
||||
return self.headers.get(key, default)
|
||||
|
||||
def read(self, amt):
|
||||
return self.body.read(amt)
|
||||
|
||||
|
||||
class FakeResponse(object):
|
||||
def __init__(self, headers=None, body=None,
|
||||
version=1.0, status_code=200, reason="Ok"):
|
||||
"""A crafted response object used for testing.
|
||||
|
||||
:param headers: dict representing HTTP response headers
|
||||
:param body: file-like object
|
||||
:param version: HTTP Version
|
||||
:param status: Response status code
|
||||
:param reason: Status code related message.
|
||||
"""
|
||||
self.body = body
|
||||
self.reason = reason
|
||||
self.version = version
|
||||
self.headers = headers
|
||||
self.status_code = status_code
|
||||
self.raw = RawRequest(headers, body=body, reason=reason,
|
||||
version=version, status=status_code)
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self.status_code
|
||||
|
||||
@property
|
||||
def ok(self):
|
||||
return (self.status_code < 400 or
|
||||
self.status_code >= 600)
|
||||
|
||||
def read(self, amt):
|
||||
return self.body.read(amt)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
if hasattr(self.body, "read"):
|
||||
return self.body.read()
|
||||
return self.body
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
if isinstance(self.content, six.binary_type):
|
||||
return self.content.decode('utf-8')
|
||||
|
||||
return self.content
|
||||
|
||||
def json(self, **kwargs):
|
||||
return self.body and json.loads(self.text) or ""
|
||||
|
||||
def iter_content(self, chunk_size=1, decode_unicode=False):
|
||||
while True:
|
||||
chunk = self.raw.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
|
||||
def release_conn(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class TestCase(testtools.TestCase):
|
||||
TEST_REQUEST_BASE = {
|
||||
'config': {'danger_mode': False},
|
||||
'verify': True}
|
||||
|
||||
|
||||
class FakeTTYStdout(six.StringIO):
|
||||
"""A Fake stdout that try to emulate a TTY device as much as possible."""
|
||||
|
||||
def isatty(self):
|
||||
return True
|
||||
|
||||
def write(self, data):
|
||||
# When a CR (carriage return) is found reset file.
|
||||
if data.startswith('\r'):
|
||||
self.seek(0)
|
||||
data = data[1:]
|
||||
return six.StringIO.write(self, data)
|
||||
|
||||
|
||||
class FakeNoTTYStdout(FakeTTYStdout):
|
||||
"""A Fake stdout that is not a TTY device."""
|
||||
|
||||
def isatty(self):
|
||||
return False
|
||||
|
||||
|
||||
def sort_url_by_query_keys(url):
|
||||
"""A helper function which sorts the keys of the query string of a url.
|
||||
|
||||
For example, an input of '/v2/tasks?sort_key=id&sort_dir=asc&limit=10'
|
||||
returns '/v2/tasks?limit=10&sort_dir=asc&sort_key=id'. This is to
|
||||
prevent non-deterministic ordering of the query string causing
|
||||
problems with unit tests.
|
||||
:param url: url which will be ordered by query keys
|
||||
:returns url: url with ordered query keys
|
||||
"""
|
||||
parsed = urlparse.urlparse(url)
|
||||
queries = urlparse.parse_qsl(parsed.query, True)
|
||||
sorted_query = sorted(queries, key=lambda x: x[0])
|
||||
|
||||
encoded_sorted_query = urlparse.urlencode(sorted_query, True)
|
||||
|
||||
url_parts = (parsed.scheme, parsed.netloc, parsed.path,
|
||||
parsed.params, encoded_sorted_query,
|
||||
parsed.fragment)
|
||||
|
||||
return urlparse.urlunparse(url_parts)
|
||||
|
||||
|
||||
def build_call_record(method, url, headers, data):
|
||||
"""Key the request body be ordered if it's a dict type."""
|
||||
if isinstance(data, dict):
|
||||
data = sorted(data.items())
|
||||
if isinstance(data, six.string_types):
|
||||
try:
|
||||
data = json.loads(data)
|
||||
except ValueError:
|
||||
return (method, url, headers or {}, data)
|
||||
data = [sorted(d.items()) for d in data]
|
||||
return (method, url, headers or {}, data)
|
15
glareclient/v1/__init__.py
Normal file
15
glareclient/v1/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) 2015 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from glareclient.v1.client import Client # noqa
|
223
glareclient/v1/artifacts.py
Normal file
223
glareclient/v1/artifacts.py
Normal file
@@ -0,0 +1,223 @@
|
||||
# Copyright (c) 2016 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from glareclient.common import utils
|
||||
from glareclient import exc
|
||||
from oslo_utils import encodeutils
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self, http_client, type_name=None):
|
||||
self.http_client = http_client
|
||||
self.type_name = type_name
|
||||
self.default_page_size = 20
|
||||
self.sort_dir_values = ('asc', 'desc')
|
||||
|
||||
def _check_type_name(self, type_name):
|
||||
"""Check that type name and type versions were specified"""
|
||||
type_name = type_name or self.type_name
|
||||
if type_name is None:
|
||||
msg = "Type name must be specified"
|
||||
raise exc.HTTPBadRequest(msg)
|
||||
return type_name
|
||||
|
||||
def _validate_sort_param(self, sort):
|
||||
"""Validates sorting argument for invalid keys and directions values.
|
||||
|
||||
:param sort: comma-separated list of sort keys with optional <:dir>
|
||||
after each key
|
||||
"""
|
||||
for sort_param in sort.strip().split(','):
|
||||
key, _sep, dir = sort_param.partition(':')
|
||||
if dir and dir not in self.sort_dir_values:
|
||||
msg = ('Invalid sort direction: %(sort_dir)s.'
|
||||
' It must be one of the following: %(available)s.'
|
||||
) % {'sort_dir': dir,
|
||||
'available': ', '.join(self.sort_dir_values)}
|
||||
raise exc.HTTPBadRequest(msg)
|
||||
return sort
|
||||
|
||||
def create(self, name, version='0.0.0', type_name=None, **kwargs):
|
||||
"""Create an artifact of given type and version.
|
||||
|
||||
:param name: name of creating artifact.
|
||||
:param version: semver string describing an artifact version
|
||||
"""
|
||||
type_name = self._check_type_name(type_name)
|
||||
kwargs.update({'name': name, 'version': version})
|
||||
url = '/artifacts/%s' % type_name
|
||||
resp, body = self.http_client.post(url, data=kwargs)
|
||||
return body
|
||||
|
||||
def update(self, artifact_id, type_name=None, remove_props=None,
|
||||
**kwargs):
|
||||
"""Update attributes of an artifact.
|
||||
|
||||
:param artifact_id: ID of the artifact to modify.
|
||||
:param remove_props: List of property names to remove
|
||||
:param \*\*kwargs: Artifact attribute names and their new values.
|
||||
"""
|
||||
type_name = self._check_type_name(type_name)
|
||||
url = '/artifacts/%s/%s' % (type_name, artifact_id)
|
||||
hdrs = {'Content-Type': 'application/json-patch+json'}
|
||||
changes = []
|
||||
if remove_props:
|
||||
for prop_name in remove_props:
|
||||
if prop_name not in kwargs:
|
||||
changes.append({'op': 'remove',
|
||||
'path': '/%s' % prop_name})
|
||||
for prop_name in kwargs:
|
||||
changes.append({'op': 'add', 'path': '/%s' % prop_name,
|
||||
'value': kwargs[prop_name]})
|
||||
resp, body = self.http_client.patch(url, headers=hdrs, data=changes)
|
||||
return body
|
||||
|
||||
def get(self, artifact_id, type_name=None):
|
||||
"""Get information about an artifact.
|
||||
|
||||
:param artifact_id: ID of the artifact to get.
|
||||
|
||||
"""
|
||||
type_name = self._check_type_name(type_name)
|
||||
url = '/artifacts/%s/%s' % (type_name, artifact_id)
|
||||
resp, body = self.http_client.get(url)
|
||||
return body
|
||||
|
||||
def list(self, type_name=None, **kwargs):
|
||||
"""Retrieve a listing of artifacts objects.
|
||||
|
||||
:param page_size: Number of artifacts to request in each
|
||||
paginated request.
|
||||
:returns: generator over list of artifacts.
|
||||
"""
|
||||
type_name = self._check_type_name(type_name)
|
||||
|
||||
limit = kwargs.get('limit')
|
||||
page_size = kwargs.get('page_size') or self.default_page_size
|
||||
|
||||
def paginate(url, page_size, limit=None):
|
||||
next_url = url
|
||||
|
||||
while True:
|
||||
if limit and page_size > limit:
|
||||
next_url = next_url.replace("limit=%s" % page_size,
|
||||
"limit=%s" % limit)
|
||||
|
||||
resp, body = self.http_client.get(next_url)
|
||||
for artifact in body[type_name]:
|
||||
yield artifact
|
||||
|
||||
if limit:
|
||||
limit -= 1
|
||||
if limit <= 0:
|
||||
raise StopIteration
|
||||
|
||||
try:
|
||||
next_url = body['next']
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
filters = kwargs.get('filters', [])
|
||||
filters.append(('limit', page_size))
|
||||
|
||||
url_params = []
|
||||
for param, items in filters:
|
||||
values = [items] if not isinstance(items, list) else items
|
||||
for value in values:
|
||||
if isinstance(value, six.string_types):
|
||||
value = encodeutils.safe_encode(value)
|
||||
url_params.append({param: value})
|
||||
|
||||
url = '/artifacts/%s?' % type_name
|
||||
|
||||
for param in url_params:
|
||||
url = '%s&%s' % (url, parse.urlencode(param))
|
||||
|
||||
if 'sort' in kwargs:
|
||||
url = '%s&sort=%s' % (url, self._validate_sort_param(
|
||||
kwargs['sort']))
|
||||
|
||||
for artifact in paginate(url, page_size, limit):
|
||||
yield artifact
|
||||
|
||||
def active(self, artifact_id, type_name=None):
|
||||
"""Set artifact status to 'active'.
|
||||
|
||||
:param artifact_id: ID of the artifact to get.
|
||||
"""
|
||||
return self.update(artifact_id, type_name,
|
||||
status='active')
|
||||
|
||||
def deactivate(self, artifact_id, type_name=None):
|
||||
"""Set artifact status to 'deactivated'.
|
||||
|
||||
:param artifact_id: ID of the artifact to get.
|
||||
"""
|
||||
return self.update(artifact_id, type_name,
|
||||
status='deactivated')
|
||||
|
||||
def reactivate(self, artifact_id, type_name=None):
|
||||
"""Set artifact status to 'active'.
|
||||
|
||||
:param artifact_id: ID of the artifact to get.
|
||||
"""
|
||||
return self.update(artifact_id, type_name,
|
||||
status='active')
|
||||
|
||||
def publish(self, artifact_id, type_name=None):
|
||||
"""Set artifact visibility to 'public'.
|
||||
|
||||
:param artifact_id: ID of the artifact to get.
|
||||
"""
|
||||
return self.update(artifact_id, type_name,
|
||||
visibility='public')
|
||||
|
||||
def delete(self, artifact_id, type_name=None):
|
||||
"""Delete an artifact and all its data.
|
||||
|
||||
:param artifact_id: ID of the artifact to delete.
|
||||
"""
|
||||
type_name = self._check_type_name(type_name)
|
||||
url = '/artifacts/%s/%s' % (type_name, artifact_id)
|
||||
self.http_client.delete(url)
|
||||
|
||||
def upload_blob(self, artifact_id, blob_property, data, type_name=None):
|
||||
"""Upload blob data.
|
||||
|
||||
:param artifact_id: ID of the artifact to download a blob
|
||||
:param blob_property: blob property name
|
||||
"""
|
||||
type_name = self._check_type_name(type_name)
|
||||
hdrs = {'Content-Type': 'application/octet-stream'}
|
||||
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
|
||||
self.http_client.put(url, headers=hdrs, data=data)
|
||||
|
||||
def download_blob(self, artifact_id, blob_property, type_name=None,
|
||||
do_checksum=True):
|
||||
"""Get blob data.
|
||||
|
||||
:param artifact_id: ID of the artifact to download a blob
|
||||
:param blob_property: blob property name
|
||||
:param do_checksum: Enable/disable checksum validation.
|
||||
"""
|
||||
type_name = self._check_type_name(type_name)
|
||||
url = '/artifacts/%s/%s/%s' % (type_name, artifact_id, blob_property)
|
||||
resp, body = self.http_client.get(url)
|
||||
checksum = resp.headers.get('content-md5', None)
|
||||
content_length = int(resp.headers.get('content-length', 0))
|
||||
if checksum is not None and do_checksum:
|
||||
body = utils.integrity_iter(body, checksum)
|
||||
return utils.IterableWithLength(body, content_length)
|
40
glareclient/v1/client.py
Normal file
40
glareclient/v1/client.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Copyright 2012 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.
|
||||
|
||||
|
||||
from glareclient.common import http
|
||||
from glareclient.common import utils
|
||||
from glareclient.v1 import artifacts
|
||||
from glareclient.v1 import versions
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Client for the Glare Artifact Repository v2 API.
|
||||
|
||||
:param string endpoint: A user-supplied endpoint URL for the glare
|
||||
service.
|
||||
:param string token: Token for authentication.
|
||||
:param integer timeout: Allows customization of the timeout for client
|
||||
http requests. (optional)
|
||||
:param string language_header: Set Accept-Language header to be sent in
|
||||
requests to glare.
|
||||
"""
|
||||
|
||||
def __init__(self, endpoint=None, **kwargs):
|
||||
endpoint, self.version = utils.endpoint_version_from_url(endpoint, 1.0)
|
||||
self.http_client = http.get_http_client(endpoint=endpoint, **kwargs)
|
||||
|
||||
self.artifacts = artifacts.Controller(self.http_client)
|
||||
self.versions = versions.VersionController(self.http_client)
|
26
glareclient/v1/versions.py
Normal file
26
glareclient/v1/versions.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Copyright 2015 OpenStack Foundation
|
||||
# Copyright 2015 Huawei Corp.
|
||||
# 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.
|
||||
|
||||
|
||||
class VersionController(object):
|
||||
def __init__(self, http_client):
|
||||
self.http_client = http_client
|
||||
|
||||
def list(self):
|
||||
"""List all versions."""
|
||||
url = '/versions'
|
||||
resp, body = self.http_client.get(url)
|
||||
return body.get('versions', None)
|
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
277
releasenotes/source/conf.py
Normal file
277
releasenotes/source/conf.py
Normal file
@@ -0,0 +1,277 @@
|
||||
# -*- 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.
|
||||
|
||||
# Glare 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'glareclient Release Notes'
|
||||
copyright = u'2016, Glare Developers'
|
||||
|
||||
# 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.
|
||||
import pbr.version
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
glare_version = pbr.version.VersionInfo('glareclient')
|
||||
release = glare_version.version_string_with_vcs()
|
||||
# The short X.Y version.
|
||||
version = glare_version.canonical_version_string()
|
||||
|
||||
# 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 = 'GlareClientReleaseNotesdoc'
|
||||
|
||||
|
||||
# -- 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', 'glareclientReleaseNotes.tex',
|
||||
u'glareclient Release Notes Documentation',
|
||||
u'Glare 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', 'glareclientreleasenotes',
|
||||
u'glareclient Release Notes Documentation',
|
||||
[u'Glare 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', 'glareclientReleaseNotes',
|
||||
u'glareclient Release Notes Documentation',
|
||||
u'Glare Developers', 'glareclientReleaseNotes',
|
||||
'Python bindings for the Glare Artifact Repository.',
|
||||
'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
|
9
releasenotes/source/index.rst
Normal file
9
releasenotes/source/index.rst
Normal file
@@ -0,0 +1,9 @@
|
||||
=========================
|
||||
glareclient Release Notes
|
||||
=========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
unreleased
|
||||
mitaka
|
5
releasenotes/source/unreleased.rst
Normal file
5
releasenotes/source/unreleased.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
============================
|
||||
Current Series Release Notes
|
||||
============================
|
||||
|
||||
.. release-notes::
|
12
requirements.txt
Normal file
12
requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
pbr>=1.6 # Apache-2.0
|
||||
Babel>=2.3.4 # BSD
|
||||
PrettyTable<0.8,>=0.7 # BSD
|
||||
keystoneauth1>=2.10.0 # Apache-2.0
|
||||
requests>=2.10.0 # Apache-2.0
|
||||
six>=1.9.0 # MIT
|
||||
oslo.utils>=3.16.0 # Apache-2.0
|
||||
oslo.i18n>=2.1.0 # Apache-2.0
|
||||
osc-lib>=1.0.2 # Apache-2.0
|
49
run_tests.sh
Executable file
49
run_tests.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
function usage {
|
||||
echo "Usage: $0 [OPTION]..."
|
||||
echo "Run python-glareclient's test suite(s)"
|
||||
echo ""
|
||||
echo " -p, --pep8 Just run flake8"
|
||||
echo " -h, --help Print this usage message"
|
||||
echo ""
|
||||
echo "This script is deprecated and currently retained for compatibility."
|
||||
echo 'You can run the full test suite for multiple environments by running "tox".'
|
||||
echo 'You can run tests for only python 2.7 by running "tox -e py27", or run only'
|
||||
echo 'the flake8 tests with "tox -e pep8".'
|
||||
exit
|
||||
}
|
||||
|
||||
command -v tox > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo 'This script requires "tox" to run.'
|
||||
echo 'You can install it with "pip install tox".'
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
just_pep8=0
|
||||
|
||||
function process_option {
|
||||
case "$1" in
|
||||
-h|--help) usage;;
|
||||
-p|--pep8) let just_pep8=1;;
|
||||
esac
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
process_option $arg
|
||||
done
|
||||
|
||||
if [ $just_pep8 -eq 1 ]; then
|
||||
tox -e pep8
|
||||
exit
|
||||
fi
|
||||
|
||||
tox -e py27 $toxargs 2>&1 | tee run_tests.err.log || exit
|
||||
if [ ${PIPESTATUS[0]} -ne 0 ]; then
|
||||
exit ${PIPESTATUS[0]}
|
||||
fi
|
||||
|
||||
if [ -z "$toxargs" ]; then
|
||||
tox -e pep8
|
||||
fi
|
59
setup.cfg
Normal file
59
setup.cfg
Normal file
@@ -0,0 +1,59 @@
|
||||
[metadata]
|
||||
name = python-glareclient
|
||||
summary = Glare Artifact Repository
|
||||
description-file =
|
||||
README.rst
|
||||
license = Apache License, Version 2.0
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
home-page = http://docs.openstack.org/developer/python-glareclient
|
||||
classifier =
|
||||
Development Status :: 5 - Production/Stable
|
||||
Environment :: Console
|
||||
Environment :: OpenStack
|
||||
Intended Audience :: Information Technology
|
||||
Intended Audience :: System Administrators
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.4
|
||||
Programming Language :: Python :: 3.5
|
||||
|
||||
[files]
|
||||
packages =
|
||||
glareclient
|
||||
|
||||
[global]
|
||||
setup-hooks =
|
||||
pbr.hooks.setup_hook
|
||||
|
||||
[entry_points]
|
||||
openstack.cli.extension =
|
||||
artifact = glareclient.osc.plugin
|
||||
|
||||
openstack.artifact.v1 =
|
||||
artifact_list = glareclient.osc.v1.artifacts:ListArtifacts
|
||||
artifact_show = glareclient.osc.v1.artifacts:ShowArtifact
|
||||
artifact_create = glareclient.osc.v1.artifacts:CreateArtifact
|
||||
artifact_update = glareclient.osc.v1.artifacts:UpdateArtifact
|
||||
artifact_delete = glareclient.osc.v1.artifacts:DeleteArtifact
|
||||
artifact_activate = glareclient.osc.v1.artifacts:ActivateArtifact
|
||||
artifact_deactivate = glareclient.osc.v1.artifacts:DeactivateArtifact
|
||||
artifact_reactivate = glareclient.osc.v1.artifacts:ReactivateArtifact
|
||||
artifact_publish = glareclient.osc.v1.artifacts:PublishArtifact
|
||||
artifact_upload = glareclient.osc.v1.blobs:UploadBlob
|
||||
artifact_download = glareclient.osc.v1.blobs:DownloadBlob
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
all_files = 1
|
||||
|
||||
[upload_sphinx]
|
||||
upload-dir = doc/build/html
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
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.11.0,<0.12 # Apache-2.0
|
||||
|
||||
coverage>=3.6 # Apache-2.0
|
||||
mock>=2.0 # BSD
|
||||
ordereddict # MIT
|
||||
os-client-config!=1.19.0,!=1.19.1,!=1.20.0,!=1.20.1,!=1.21.0,>=1.13.1 # Apache-2.0
|
||||
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
|
||||
reno>=1.8.0 # Apache2
|
||||
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD
|
||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
testtools>=1.4.0 # MIT
|
||||
testscenarios>=0.4 # Apache-2.0/BSD
|
||||
fixtures>=3.0.0 # Apache-2.0/BSD
|
||||
requests-mock>=1.0 # Apache-2.0
|
25
tools/glance.bash_completion
Normal file
25
tools/glance.bash_completion
Normal file
@@ -0,0 +1,25 @@
|
||||
_glare_opts="" # lazy init
|
||||
_glare_flags="" # lazy init
|
||||
_glare_opts_exp="" # lazy init
|
||||
_glare()
|
||||
{
|
||||
local cur prev nbc cflags
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
if [ "x$_glare_opts" == "x" ] ; then
|
||||
nbc="`glare bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`"
|
||||
_glare_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`"
|
||||
_glare_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`"
|
||||
_glare_opts_exp="`echo "$_glare_opts" | sed 's/^ *//' | tr ' ' '|'`"
|
||||
fi
|
||||
|
||||
if [[ " ${COMP_WORDS[@]} " =~ " "($_glare_opts_exp)" " && "$prev" != "help" ]] ; then
|
||||
COMPREPLY=($(compgen -W "${_glare_flags}" -- ${cur}))
|
||||
else
|
||||
COMPREPLY=($(compgen -W "${_glare_opts}" -- ${cur}))
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
complete -F _glare glare
|
55
tools/tox_install.sh
Executable file
55
tools/tox_install.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Client constraint file contains this client version pin that is in conflict
|
||||
# with installing the client from source. We should replace the version pin in
|
||||
# the constraints file before applying it for from-source installation.
|
||||
|
||||
ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner
|
||||
BRANCH_NAME=master
|
||||
CLIENT_NAME=python-glareclient
|
||||
requirements_installed=$(echo "import openstack_requirements" | python 2>/dev/null ; echo $?)
|
||||
|
||||
set -e
|
||||
|
||||
CONSTRAINTS_FILE=$1
|
||||
shift
|
||||
|
||||
install_cmd="pip install"
|
||||
if [ $CONSTRAINTS_FILE != "unconstrained" ]; then
|
||||
|
||||
mydir=$(mktemp -dt "$CLIENT_NAME-tox_install-XXXXXXX")
|
||||
localfile=$mydir/upper-constraints.txt
|
||||
if [[ $CONSTRAINTS_FILE != http* ]]; then
|
||||
CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE
|
||||
fi
|
||||
curl $CONSTRAINTS_FILE -k -o $localfile
|
||||
install_cmd="$install_cmd -c$localfile"
|
||||
|
||||
if [ $requirements_installed -eq 0 ]; then
|
||||
echo "ALREADY INSTALLED" > /tmp/tox_install.txt
|
||||
echo "Requirements already installed; using existing package"
|
||||
elif [ -x "$ZUUL_CLONER" ]; then
|
||||
export ZUUL_BRANCH=${ZUUL_BRANCH-$BRANCH}
|
||||
echo "ZUUL CLONER" > /tmp/tox_install.txt
|
||||
pushd $mydir
|
||||
$ZUUL_CLONER --cache-dir \
|
||||
/opt/git \
|
||||
--branch $BRANCH_NAME \
|
||||
git://git.openstack.org \
|
||||
openstack/requirements
|
||||
cd openstack/requirements
|
||||
$install_cmd -e .
|
||||
popd
|
||||
else
|
||||
echo "PIP HARDCODE" > /tmp/tox_install.txt
|
||||
if [ -z "$REQUIREMENTS_PIP_LOCATION" ]; then
|
||||
REQUIREMENTS_PIP_LOCATION="git+https://git.openstack.org/openstack/requirements@$BRANCH_NAME#egg=requirements"
|
||||
fi
|
||||
$install_cmd -U -e ${REQUIREMENTS_PIP_LOCATION}
|
||||
fi
|
||||
|
||||
edit-constraints $localfile -- $CLIENT_NAME "-e file://$PWD#egg=$CLIENT_NAME"
|
||||
fi
|
||||
|
||||
$install_cmd -U $*
|
||||
exit $?
|
10
tools/with_venv.sh
Executable file
10
tools/with_venv.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
command -v tox > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo 'This script requires "tox" to run.'
|
||||
echo 'You can install it with "pip install tox".'
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
tox -evenv -- $@
|
59
tox.ini
Normal file
59
tox.ini
Normal file
@@ -0,0 +1,59 @@
|
||||
[tox]
|
||||
envlist = py35,py34,py27,pep8
|
||||
minversion = 1.6
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
install_command =
|
||||
{toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
OS_STDOUT_NOCAPTURE=False
|
||||
OS_STDERR_NOCAPTURE=False
|
||||
PYTHONHASHSEED=0
|
||||
PYTHONDONTWRITEBYTECODE = 1
|
||||
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands = python setup.py testr --testr-args='{posargs}'
|
||||
|
||||
[testenv:pep8]
|
||||
commands = flake8
|
||||
|
||||
[testenv:venv]
|
||||
# NOTE(NiallBunting) Infra does not support constraints for the venv
|
||||
# job.
|
||||
install_command = pip install -U {opts} {packages}
|
||||
commands = {posargs}
|
||||
|
||||
[pbr]
|
||||
warnerror = True
|
||||
|
||||
[testenv:functional]
|
||||
# See glareclient/tests/functional/README.rst
|
||||
# for information on running the functional tests.
|
||||
setenv =
|
||||
OS_TEST_PATH = ./glareclient/tests/functional
|
||||
|
||||
[testenv:cover]
|
||||
install_command = pip install -U {opts} {packages}
|
||||
commands =
|
||||
coverage erase
|
||||
python setup.py testr --coverage --testr-args='{posargs}'
|
||||
|
||||
[testenv:docs]
|
||||
commands=
|
||||
python setup.py build_sphinx
|
||||
|
||||
[testenv:releasenotes]
|
||||
# NOTE(Niall Bunting) Does not support constraints.
|
||||
install_command = pip install -U {opts} {packages}
|
||||
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
||||
|
||||
[flake8]
|
||||
ignore = F403,F812,F821
|
||||
show-source = True
|
||||
exclude = .venv*,.tox,dist,*egg,build,.git,doc,*openstack/common*,*lib/python*,.update-venv
|
||||
|
||||
[hacking]
|
||||
import_exceptions = six.moves,glareclient._i18n
|
Reference in New Issue
Block a user