Retire Packaging Deb project repos
This commit is part of a series to retire the Packaging Deb project. Step 2 is to remove all content from the project repos, replacing it with a README notification where to find ongoing work, and how to recover the repo if needed at some future point (as in https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project). Change-Id: I514c788c2d90adbb4d028385c001b63da41aaa9b
This commit is contained in:
parent
28c003dc11
commit
f923549734
@ -1,6 +0,0 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = glanceclient
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
27
.gitignore
vendored
27
.gitignore
vendored
@ -1,27 +0,0 @@
|
||||
.coverage
|
||||
subunit.log
|
||||
.venv
|
||||
*,cover
|
||||
cover
|
||||
*.pyc
|
||||
.idea
|
||||
*.sw?
|
||||
*~
|
||||
AUTHORS
|
||||
build
|
||||
dist
|
||||
python_glanceclient.egg-info
|
||||
ChangeLog
|
||||
run_tests.err.log
|
||||
.testrepository
|
||||
.tox
|
||||
doc/source/api
|
||||
doc/build
|
||||
*.egg
|
||||
.eggs/*
|
||||
glanceclient/versioninfo
|
||||
# Files created by releasenotes build
|
||||
releasenotes/build
|
||||
# File created by docs build process
|
||||
/doc/source/ref
|
||||
/doc/source/reference/api/*
|
@ -1,4 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/python-glanceclient.git
|
4
.mailmap
4
.mailmap
@ -1,4 +0,0 @@
|
||||
# "man git-shortlog" for reference
|
||||
<mr.alex.meade@gmail.com> <hatboy112@yahoo.com>
|
||||
<mr.alex.meade@gmail.com> <alex.meade@rackspace.com>
|
||||
David Koo <david.koo@huawei.com> <kpublicmail@gmail.com>
|
@ -1,4 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_command=${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./glanceclient/tests/unit} $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
@ -1,16 +0,0 @@
|
||||
If you would like to contribute to the development of OpenStack,
|
||||
you must follow the steps documented at:
|
||||
|
||||
https://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:
|
||||
|
||||
https://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-glanceclient
|
12
HACKING.rst
12
HACKING.rst
@ -1,12 +0,0 @@
|
||||
Glance Style Commandments
|
||||
=========================
|
||||
|
||||
- Step 1: Read the OpenStack Style Commandments
|
||||
https://docs.openstack.org/hacking/latest/
|
||||
- Step 2: Read on
|
||||
|
||||
|
||||
Glance Specific Commandments
|
||||
----------------------------
|
||||
|
||||
None so far
|
175
LICENSE
175
LICENSE
@ -1,175 +0,0 @@
|
||||
|
||||
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.
|
14
README
Normal file
14
README
Normal file
@ -0,0 +1,14 @@
|
||||
This project is no longer maintained.
|
||||
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
For ongoing work on maintaining OpenStack packages in the Debian
|
||||
distribution, please see the Debian OpenStack packaging team at
|
||||
https://wiki.debian.org/OpenStack/.
|
||||
|
||||
For any further questions, please email
|
||||
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||
Freenode.
|
56
README.rst
56
README.rst
@ -1,56 +0,0 @@
|
||||
========================
|
||||
Team and repository tags
|
||||
========================
|
||||
|
||||
.. image:: https://governance.openstack.org/tc/badges/python-glanceclient.svg
|
||||
:target: https://governance.openstack.org/tc/reference/tags/index.html
|
||||
:alt: The following tags have been asserted for Python bindings to the
|
||||
OpenStack Images API:
|
||||
"project:official",
|
||||
"stable:follows-policy",
|
||||
"vulnerability:managed",
|
||||
"team:diverse-affiliation".
|
||||
Follow the link for an explanation of these tags.
|
||||
.. NOTE(rosmaita): the alt text above will have to be updated when
|
||||
additional tags are asserted for python-glanceclient. (The SVG in the
|
||||
governance repo is updated automatically.)
|
||||
|
||||
.. Change things from this point on
|
||||
|
||||
===========================================
|
||||
Python bindings to the OpenStack Images API
|
||||
===========================================
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/python-glanceclient.svg
|
||||
:target: https://pypi.python.org/pypi/python-glanceclient/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/python-glanceclient.svg
|
||||
:target: https://pypi.python.org/pypi/python-glanceclient/
|
||||
:alt: Downloads
|
||||
|
||||
This is a client library for Glance built on the OpenStack Images API. It provides a Python API (the ``glanceclient`` module) and a command-line tool (``glance``). This library fully supports the v1 Images API, while support for the v2 API is in progress.
|
||||
|
||||
Development takes place via the usual OpenStack processes as outlined in the `developer guide <http://docs.openstack.org/infra/manual/developers.html>`_. The master repository is in `Git <https://git.openstack.org/cgit/openstack/python-glanceclient>`_.
|
||||
|
||||
See release notes and more at `<http://docs.openstack.org/python-glanceclient/>`_.
|
||||
|
||||
* License: Apache License, Version 2.0
|
||||
* `PyPi`_ - package installation
|
||||
* `Online Documentation`_
|
||||
* `Launchpad project`_ - release management
|
||||
* `Blueprints`_ - feature specifications
|
||||
* `Bugs`_ - issue tracking
|
||||
* `Source`_
|
||||
* `Specs`_
|
||||
* `How to Contribute`_
|
||||
|
||||
.. _PyPi: https://pypi.python.org/pypi/python-glanceclient
|
||||
.. _Online Documentation: https://docs.openstack.org/python-glanceclient/latest/
|
||||
.. _Launchpad project: https://launchpad.net/python-glanceclient
|
||||
.. _Blueprints: https://blueprints.launchpad.net/python-glanceclient
|
||||
.. _Bugs: https://bugs.launchpad.net/python-glanceclient
|
||||
.. _Source: https://git.openstack.org/cgit/openstack/python-glanceclient
|
||||
.. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html
|
||||
.. _Specs: https://specs.openstack.org/openstack/glance-specs/
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,87 +0,0 @@
|
||||
==============================
|
||||
:program:`glance` CLI man page
|
||||
==============================
|
||||
|
||||
.. program:: glance
|
||||
.. highlight:: bash
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
|
||||
:program:`glance` [options] <command> [command-options]
|
||||
|
||||
:program:`glance help`
|
||||
|
||||
:program:`glance help` <command>
|
||||
|
||||
|
||||
DESCRIPTION
|
||||
===========
|
||||
|
||||
The :program:`glance` command line utility interacts with OpenStack Images
|
||||
Service (Glance).
|
||||
|
||||
In order to use the CLI, you must provide your OpenStack username, password,
|
||||
project (historically called tenant), and auth endpoint. You can use
|
||||
configuration options ``--os-username``, ``--os-password``, ``--os-tenant-id``,
|
||||
and ``--os-auth-url`` or set corresponding environment variables::
|
||||
|
||||
export OS_USERNAME=user
|
||||
export OS_PASSWORD=pass
|
||||
export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b
|
||||
export OS_AUTH_URL=http://auth.example.com:5000/v2.0
|
||||
|
||||
The command line tool will attempt to reauthenticate using provided credentials
|
||||
for every request. You can override this behavior by manually supplying an auth
|
||||
token using ``--os-image-url`` and ``--os-auth-token`` or by setting
|
||||
corresponding environment variables::
|
||||
|
||||
export OS_IMAGE_URL=http://glance.example.org:9292/
|
||||
export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155
|
||||
|
||||
You can select an API version to use by ``--os-image-api-version`` option or by
|
||||
setting corresponding environment variable::
|
||||
|
||||
export OS_IMAGE_API_VERSION=1
|
||||
|
||||
Default Images API used is v2.
|
||||
|
||||
OPTIONS
|
||||
=======
|
||||
|
||||
To get a list of available commands and options run::
|
||||
|
||||
glance help
|
||||
|
||||
To get usage and options of a command::
|
||||
|
||||
glance help <command>
|
||||
|
||||
|
||||
EXAMPLES
|
||||
========
|
||||
|
||||
Get information about image-create command::
|
||||
|
||||
glance help image-create
|
||||
|
||||
See available images::
|
||||
|
||||
glance image-list
|
||||
|
||||
Create new image::
|
||||
|
||||
glance image-create --name foo --disk-format=qcow2 \
|
||||
--container-format=bare --visibility=public \
|
||||
--file /tmp/foo.img
|
||||
|
||||
Describe a specific image::
|
||||
|
||||
glance image-show <Image-ID>
|
||||
|
||||
|
||||
BUGS
|
||||
====
|
||||
|
||||
Glance client is hosted in Launchpad so you can view current bugs at
|
||||
https://bugs.launchpad.net/python-glanceclient/.
|
@ -1,32 +0,0 @@
|
||||
=============================
|
||||
Command-line Tool Reference
|
||||
=============================
|
||||
|
||||
In order to use the CLI, you must provide your OpenStack username,
|
||||
password, tenant, and auth endpoint. Use the corresponding
|
||||
configuration options (``--os-username``, ``--os-password``,
|
||||
``--os-tenant-id``, and ``--os-auth-url``) or set them in environment
|
||||
variables::
|
||||
|
||||
export OS_USERNAME=user
|
||||
export OS_PASSWORD=pass
|
||||
export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b
|
||||
export OS_AUTH_URL=http://auth.example.com:5000/v2.0
|
||||
|
||||
The command line tool will attempt to reauthenticate using your
|
||||
provided credentials for every request. You can override this behavior
|
||||
by manually supplying an auth token using ``--os-image-url`` and
|
||||
``--os-auth-token``. You can alternatively set these environment
|
||||
variables::
|
||||
|
||||
export OS_IMAGE_URL=http://glance.example.org:9292/
|
||||
export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155
|
||||
|
||||
Once you've configured your authentication parameters, you can run
|
||||
``glance help`` to see a complete listing of available commands.
|
||||
|
||||
.. toctree::
|
||||
|
||||
details
|
||||
property-keys
|
||||
glance
|
@ -1,340 +0,0 @@
|
||||
===========================
|
||||
Image service property keys
|
||||
===========================
|
||||
|
||||
The following keys, together with the components to which they are specific,
|
||||
can be used with the property option for both the
|
||||
:command:`openstack image set` and :command:`openstack image create` commands.
|
||||
For example:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ openstack image set IMG-UUID --property architecture=x86_64
|
||||
|
||||
.. note::
|
||||
|
||||
Behavior set using image properties overrides behavior set using flavors.
|
||||
For more information, refer to the `Manage images
|
||||
<https://docs.openstack.org/glance/latest/admin/manage-images.html>`_
|
||||
in the OpenStack Administrator Guide.
|
||||
|
||||
.. list-table:: Image service property keys
|
||||
:widths: 15 35 50 90
|
||||
:header-rows: 1
|
||||
|
||||
* - Specific to
|
||||
- Key
|
||||
- Description
|
||||
- Supported values
|
||||
* - All
|
||||
- ``architecture``
|
||||
- The CPU architecture that must be supported by the hypervisor. For
|
||||
example, ``x86_64``, ``arm``, or ``ppc64``. Run :command:`uname -m`
|
||||
to get the architecture of a machine. We strongly recommend using
|
||||
the architecture data vocabulary defined by the `libosinfo project
|
||||
<http://libosinfo.org/>`_ for this purpose.
|
||||
- * ``alpha`` - `DEC 64-bit RISC
|
||||
<https://en.wikipedia.org/wiki/DEC_Alpha>`_
|
||||
* ``armv7l`` - `ARM Cortex-A7 MPCore
|
||||
<https://en.wikipedia.org/wiki/ARM_architecture>`_
|
||||
* ``cris`` - `Ethernet, Token Ring, AXis—Code Reduced Instruction
|
||||
Set <https://en.wikipedia.org/wiki/ETRAX_CRIS>`_
|
||||
* ``i686`` - `Intel sixth-generation x86 (P6 micro architecture)
|
||||
<https://en.wikipedia.org/wiki/X86>`_
|
||||
* ``ia64`` - `Itanium <https://en.wikipedia.org/wiki/Itanium>`_
|
||||
* ``lm32`` - `Lattice Micro32
|
||||
<https://en.wikipedia.org/wiki/Milkymist>`_
|
||||
* ``m68k`` - `Motorola 68000
|
||||
<https://en.wikipedia.org/wiki/Motorola_68000_family>`_
|
||||
* ``microblaze`` - `Xilinx 32-bit FPGA (Big Endian)
|
||||
<https://en.wikipedia.org/wiki/MicroBlaze>`_
|
||||
* ``microblazeel`` - `Xilinx 32-bit FPGA (Little Endian)
|
||||
<https://en.wikipedia.org/wiki/MicroBlaze>`_
|
||||
* ``mips`` - `MIPS 32-bit RISC (Big Endian)
|
||||
<https://en.wikipedia.org/wiki/MIPS_architecture>`_
|
||||
* ``mipsel`` - `MIPS 32-bit RISC (Little Endian)
|
||||
<https://en.wikipedia.org/wiki/MIPS_architecture>`_
|
||||
* ``mips64`` - `MIPS 64-bit RISC (Big Endian)
|
||||
<https://en.wikipedia.org/wiki/MIPS_architecture>`_
|
||||
* ``mips64el`` - `MIPS 64-bit RISC (Little Endian)
|
||||
<https://en.wikipedia.org/wiki/MIPS_architecture>`_
|
||||
* ``openrisc`` - `OpenCores RISC
|
||||
<https://en.wikipedia.org/wiki/OpenRISC#QEMU_support>`_
|
||||
* ``parisc`` - `HP Precision Architecture RISC
|
||||
<https://en.wikipedia.org/wiki/PA-RISC>`_
|
||||
* parisc64 - `HP Precision Architecture 64-bit RISC
|
||||
<https://en.wikipedia.org/wiki/PA-RISC>`_
|
||||
* ppc - `PowerPC 32-bit <https://en.wikipedia.org/wiki/PowerPC>`_
|
||||
* ppc64 - `PowerPC 64-bit <https://en.wikipedia.org/wiki/PowerPC>`_
|
||||
* ppcemb - `PowerPC (Embedded 32-bit)
|
||||
<https://en.wikipedia.org/wiki/PowerPC>`_
|
||||
* s390 - `IBM Enterprise Systems Architecture/390
|
||||
<https://en.wikipedia.org/wiki/S390>`_
|
||||
* s390x - `S/390 64-bit <https://en.wikipedia.org/wiki/S390x>`_
|
||||
* sh4 - `SuperH SH-4 (Little Endian)
|
||||
<https://en.wikipedia.org/wiki/SuperH>`_
|
||||
* sh4eb - `SuperH SH-4 (Big Endian)
|
||||
<https://en.wikipedia.org/wiki/SuperH>`_
|
||||
* sparc - `Scalable Processor Architecture, 32-bit
|
||||
<https://en.wikipedia.org/wiki/Sparc>`_
|
||||
* sparc64 - `Scalable Processor Architecture, 64-bit
|
||||
<https://en.wikipedia.org/wiki/Sparc>`_
|
||||
* unicore32 - `Microprocessor Research and Development Center RISC
|
||||
Unicore32 <https://en.wikipedia.org/wiki/Unicore>`_
|
||||
* x86_64 - `64-bit extension of IA-32
|
||||
<https://en.wikipedia.org/wiki/X86>`_
|
||||
* xtensa - `Tensilica Xtensa configurable microprocessor core
|
||||
<https://en.wikipedia.org/wiki/Xtensa#Processor_Cores>`_
|
||||
* xtensaeb - `Tensilica Xtensa configurable microprocessor core
|
||||
<https://en.wikipedia.org/wiki/Xtensa#Processor_Cores>`_ (Big Endian)
|
||||
* - All
|
||||
- ``hypervisor_type``
|
||||
- The hypervisor type. Note that ``qemu`` is used for both QEMU and KVM
|
||||
hypervisor types.
|
||||
- ``hyperv``, ``ironic``, ``lxc``, ``qemu``, ``uml``, ``vmware``, or
|
||||
``xen``.
|
||||
* - All
|
||||
- ``instance_type_rxtx_factor``
|
||||
- Optional property allows created servers to have a different bandwidth
|
||||
cap than that defined in the network they are attached to. This factor
|
||||
is multiplied by the ``rxtx_base`` property of the network. The
|
||||
``rxtx_base`` property defaults to ``1.0``, which is the same as the
|
||||
attached network. This parameter is only available for Xen or NSX based
|
||||
systems.
|
||||
- Float (default value is ``1.0``)
|
||||
* - All
|
||||
- ``instance_uuid``
|
||||
- For snapshot images, this is the UUID of the server used to create this
|
||||
image.
|
||||
- Valid server UUID
|
||||
* - All
|
||||
- ``img_config_drive``
|
||||
- Specifies whether the image needs a config drive.
|
||||
- ``mandatory`` or ``optional`` (default if property is not used).
|
||||
* - All
|
||||
- ``kernel_id``
|
||||
- The ID of an image stored in the Image service that should be used as
|
||||
the kernel when booting an AMI-style image.
|
||||
- Valid image ID
|
||||
* - All
|
||||
- ``os_distro``
|
||||
- The common name of the operating system distribution in lowercase
|
||||
(uses the same data vocabulary as the
|
||||
`libosinfo project`_). Specify only a recognized
|
||||
value for this field. Deprecated values are listed to assist you in
|
||||
searching for the recognized value.
|
||||
- * ``arch`` - Arch Linux. Do not use ``archlinux`` or ``org.archlinux``.
|
||||
* ``centos`` - Community Enterprise Operating System. Do not use
|
||||
``org.centos`` or ``CentOS``.
|
||||
* ``debian`` - Debian. Do not use ``Debian` or ``org.debian``.
|
||||
* ``fedora`` - Fedora. Do not use ``Fedora``, ``org.fedora``, or
|
||||
``org.fedoraproject``.
|
||||
* ``freebsd`` - FreeBSD. Do not use ``org.freebsd``, ``freeBSD``, or
|
||||
``FreeBSD``.
|
||||
* ``gentoo`` - Gentoo Linux. Do not use ``Gentoo`` or ``org.gentoo``.
|
||||
* ``mandrake`` - Mandrakelinux (MandrakeSoft) distribution. Do not use
|
||||
``mandrakelinux`` or ``MandrakeLinux``.
|
||||
* ``mandriva`` - Mandriva Linux. Do not use ``mandrivalinux``.
|
||||
* ``mes`` - Mandriva Enterprise Server. Do not use ``mandrivaent`` or
|
||||
``mandrivaES``.
|
||||
* ``msdos`` - Microsoft Disc Operating System. Do not use ``ms-dos``.
|
||||
* ``netbsd`` - NetBSD. Do not use ``NetBSD`` or ``org.netbsd``.
|
||||
* ``netware`` - Novell NetWare. Do not use ``novell`` or ``NetWare``.
|
||||
* ``openbsd`` - OpenBSD. Do not use ``OpenBSD`` or ``org.openbsd``.
|
||||
* ``opensolaris`` - OpenSolaris. Do not use ``OpenSolaris`` or
|
||||
``org.opensolaris``.
|
||||
* ``opensuse`` - openSUSE. Do not use ``suse``, ``SuSE``, or
|
||||
`` org.opensuse``.
|
||||
* ``rhel`` - Red Hat Enterprise Linux. Do not use ``redhat``, ``RedHat``,
|
||||
or ``com.redhat``.
|
||||
* ``sled`` - SUSE Linux Enterprise Desktop. Do not use ``com.suse``.
|
||||
* ``ubuntu`` - Ubuntu. Do not use ``Ubuntu``, ``com.ubuntu``,
|
||||
``org.ubuntu``, or ``canonical``.
|
||||
* ``windows`` - Microsoft Windows. Do not use ``com.microsoft.server``
|
||||
or ``windoze``.
|
||||
* - All
|
||||
- ``os_version``
|
||||
- The operating system version as specified by the distributor.
|
||||
- Valid version number (for example, ``11.10``).
|
||||
* - All
|
||||
- ``os_secure_boot``
|
||||
- Secure Boot is a security standard. When the instance starts,
|
||||
Secure Boot first examines software such as firmware and OS by their
|
||||
signature and only allows them to run if the signatures are valid.
|
||||
|
||||
For Hyper-V: Images must be prepared as Generation 2 VMs. Instance must
|
||||
also contain ``hw_machine_type=hyperv-gen2`` image property. Linux
|
||||
guests will also require bootloader's digital signature provided as
|
||||
``os_secure_boot_signature`` and
|
||||
``hypervisor_version_requires'>=10.0'`` image properties.
|
||||
- * ``required`` - Enable the Secure Boot feature.
|
||||
* ``disabled`` or ``optional`` - (default) Disable the Secure Boot
|
||||
feature.
|
||||
* - All
|
||||
- ``ramdisk_id``
|
||||
- The ID of image stored in the Image service that should be used as the
|
||||
ramdisk when booting an AMI-style image.
|
||||
- Valid image ID.
|
||||
* - All
|
||||
- ``vm_mode``
|
||||
- The virtual machine mode. This represents the host/guest ABI
|
||||
(application binary interface) used for the virtual machine.
|
||||
- * ``hvm`` - Fully virtualized. This is the mode used by QEMU and KVM.
|
||||
* ``xen`` - Xen 3.0 paravirtualized.
|
||||
* ``uml`` - User Mode Linux paravirtualized.
|
||||
* ``exe`` - Executables in containers. This is the mode used by LXC.
|
||||
* - libvirt API driver
|
||||
- ``hw_cpu_sockets``
|
||||
- The preferred number of sockets to expose to the guest.
|
||||
- Integer.
|
||||
* - libvirt API driver
|
||||
- ``hw_cpu_cores``
|
||||
- The preferred number of cores to expose to the guest.
|
||||
- Integer.
|
||||
* - libvirt API driver
|
||||
- ``hw_cpu_threads``
|
||||
- The preferred number of threads to expose to the guest.
|
||||
- Integer.
|
||||
* - libvirt API driver
|
||||
- ``hw_disk_bus``
|
||||
- Specifies the type of disk controller to attach disk devices to.
|
||||
- One of ``scsi``, ``virtio``, ``uml``, ``xen``, ``ide``, or ``usb``.
|
||||
* - libvirt API driver
|
||||
- ``hw_rng_model``
|
||||
- Adds a random-number generator device to the image's instances. The
|
||||
cloud administrator can enable and control device behavior by
|
||||
configuring the instance's flavor. By default:
|
||||
|
||||
* The generator device is disabled.
|
||||
* ``/dev/random`` is used as the default entropy source. To specify a
|
||||
physical HW RNG device, use the following option in the nova.conf
|
||||
file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
rng_dev_path=/dev/hwrng
|
||||
|
||||
- ``virtio``, or other supported device.
|
||||
* - libvirt API driver, Hyper-V driver
|
||||
- ``hw_machine_type``
|
||||
- For libvirt: Enables booting an ARM system using the specified machine
|
||||
type. By default, if an ARM image is used and its type is not specified,
|
||||
Compute uses ``vexpress-a15`` (for ARMv7) or ``virt`` (for AArch64)
|
||||
machine types.
|
||||
|
||||
For Hyper-V: Specifies whether the Hyper-V instance will be a generation
|
||||
1 or generation 2 VM. By default, if the property is not provided, the
|
||||
instances will be generation 1 VMs. If the image is specific for
|
||||
generation 2 VMs but the property is not provided accordingly, the
|
||||
instance will fail to boot.
|
||||
- For libvirt: Valid types can be viewed by using the
|
||||
:command:`virsh capabilities` command (machine types are displayed in
|
||||
the ``machine`` tag).
|
||||
|
||||
For hyper-V: Acceptable values are either ``hyperv-gen1`` or
|
||||
``hyperv-gen2``.
|
||||
* - libvirt API driver, XenAPI driver
|
||||
- ``os_type``
|
||||
- The operating system installed on the image. The ``libvirt`` API driver
|
||||
and ``XenAPI`` driver contains logic that takes different actions
|
||||
depending on the value of the ``os_type`` parameter of the image.
|
||||
For example, for ``os_type=windows`` images, it creates a FAT32-based
|
||||
swap partition instead of a Linux swap partition, and it limits the
|
||||
injected host name to less than 16 characters.
|
||||
- ``linux`` or ``windows``.
|
||||
|
||||
* - libvirt API driver
|
||||
- ``hw_scsi_model``
|
||||
- Enables the use of VirtIO SCSI (``virtio-scsi``) to provide block
|
||||
device access for compute instances; by default, instances use VirtIO
|
||||
Block (``virtio-blk``). VirtIO SCSI is a para-virtualized SCSI
|
||||
controller device that provides improved scalability and performance,
|
||||
and supports advanced SCSI hardware.
|
||||
- ``virtio-scsi``
|
||||
* - libvirt API driver
|
||||
- ``hw_serial_port_count``
|
||||
- Specifies the count of serial ports that should be provided. If
|
||||
``hw:serial_port_count`` is not set in the flavor's extra_specs, then
|
||||
any count is permitted. If ``hw:serial_port_count`` is set, then this
|
||||
provides the default serial port count. It is permitted to override the
|
||||
default serial port count, but only with a lower value.
|
||||
- Integer
|
||||
* - libvirt API driver
|
||||
- ``hw_video_model``
|
||||
- The video image driver used.
|
||||
- ``vga``, ``cirrus``, ``vmvga``, ``xen``, or ``qxl``.
|
||||
* - libvirt API driver
|
||||
- ``hw_video_ram``
|
||||
- Maximum RAM for the video image. Used only if a ``hw_video:ram_max_mb``
|
||||
value has been set in the flavor's extra_specs and that value is higher
|
||||
than the value set in ``hw_video_ram``.
|
||||
- Integer in MB (for example, ``64``).
|
||||
* - libvirt API driver
|
||||
- ``hw_watchdog_action``
|
||||
- Enables a virtual hardware watchdog device that carries out the
|
||||
specified action if the server hangs. The watchdog uses the
|
||||
``i6300esb`` device (emulating a PCI Intel 6300ESB). If
|
||||
``hw_watchdog_action`` is not specified, the watchdog is disabled.
|
||||
- * ``disabled`` - (default) The device is not attached. Allows the user to
|
||||
disable the watchdog for the image, even if it has been enabled using
|
||||
the image's flavor.
|
||||
* ``reset`` - Forcefully reset the guest.
|
||||
* ``poweroff`` - Forcefully power off the guest.
|
||||
* ``pause`` - Pause the guest.
|
||||
* ``none`` - Only enable the watchdog; do nothing if the server hangs.
|
||||
* - libvirt API driver
|
||||
- ``os_command_line``
|
||||
- The kernel command line to be used by the ``libvirt`` driver, instead
|
||||
of the default. For Linux Containers (LXC), the value is used as
|
||||
arguments for initialization. This key is valid only for Amazon kernel,
|
||||
``ramdisk``, or machine images (``aki``, ``ari``, or ``ami``).
|
||||
-
|
||||
* - libvirt API driver and VMware API driver
|
||||
- ``hw_vif_model``
|
||||
- Specifies the model of virtual network interface device to use.
|
||||
- The valid options depend on the configured hypervisor.
|
||||
* ``KVM`` and ``QEMU``: ``e1000``, ``ne2k_pci``, ``pcnet``,
|
||||
``rtl8139``, and ``virtio``.
|
||||
* VMware: ``e1000``, ``e1000e``, ``VirtualE1000``, ``VirtualE1000e``,
|
||||
``VirtualPCNet32``, ``VirtualSriovEthernetCard``, and
|
||||
``VirtualVmxnet``.
|
||||
* Xen: ``e1000``, ``netfront``, ``ne2k_pci``, ``pcnet``, and
|
||||
``rtl8139``.
|
||||
* - libvirt API driver
|
||||
- ``hw_vif_multiqueue_enabled``
|
||||
- If ``true``, this enables the ``virtio-net multiqueue`` feature. In
|
||||
this case, the driver sets the number of queues equal to the number
|
||||
of guest vCPUs. This makes the network performance scale across a
|
||||
number of vCPUs.
|
||||
- ``true`` | ``false``
|
||||
* - libvirt API driver
|
||||
- ``hw_boot_menu``
|
||||
- If ``true``, enables the BIOS bootmenu. In cases where both the image
|
||||
metadata and Extra Spec are set, the Extra Spec setting is used. This
|
||||
allows for flexibility in setting/overriding the default behavior as
|
||||
needed.
|
||||
- ``true`` or ``false``
|
||||
* - VMware API driver
|
||||
- ``vmware_adaptertype``
|
||||
- The virtual SCSI or IDE controller used by the hypervisor.
|
||||
- ``lsiLogic``, ``lsiLogicsas``, ``busLogic``, ``ide``, or
|
||||
``paraVirtual``.
|
||||
* - VMware API driver
|
||||
- ``vmware_ostype``
|
||||
- A VMware GuestID which describes the operating system installed in
|
||||
the image. This value is passed to the hypervisor when creating a
|
||||
virtual machine. If not specified, the key defaults to ``otherGuest``.
|
||||
- See `thinkvirt.com <http://www.thinkvirt.com/?q=node/181>`_.
|
||||
* - VMware API driver
|
||||
- ``vmware_image_version``
|
||||
- Currently unused.
|
||||
- ``1``
|
||||
* - XenAPI driver
|
||||
- ``auto_disk_config``
|
||||
- If ``true``, the root partition on the disk is automatically resized
|
||||
before the instance boots. This value is only taken into account by
|
||||
the Compute service when using a Xen-based hypervisor with the
|
||||
``XenAPI`` driver. The Compute service will only attempt to resize if
|
||||
there is a single partition on the image, and only if the partition
|
||||
is in ``ext3`` or ``ext4`` format.
|
||||
- ``true`` or ``false``
|
@ -1,88 +0,0 @@
|
||||
# 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.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import openstackdocstheme
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'..', '..')))
|
||||
|
||||
# -- General configuration ----------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'openstackdocstheme',
|
||||
]
|
||||
|
||||
# openstackdocstheme options
|
||||
repository_name = 'openstack/python-glanceclient'
|
||||
bug_project = 'python-glanceclient'
|
||||
bug_tag = ''
|
||||
|
||||
# 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-glanceclient'
|
||||
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'
|
||||
|
||||
# -- 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'
|
||||
html_theme = 'openstackdocs'
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = ['_theme']
|
||||
html_theme_path = [openstackdocstheme.get_html_theme_path()]
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = '%sdoc' % project
|
||||
|
||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||
|
||||
# -- Options for man page output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree for man pages.
|
||||
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
|
||||
man_pages = [
|
||||
('cli/glance', 'glance', u'Client for OpenStack Images API',
|
||||
[u'OpenStack Foundation'], 1),
|
||||
]
|
@ -1,15 +0,0 @@
|
||||
==============================================
|
||||
Python Bindings for the OpenStack Images API
|
||||
==============================================
|
||||
|
||||
This is a client for the OpenStack Images API. There's :doc:`a Python
|
||||
API <reference/api/index>` (the :mod:`glanceclient` module) and a
|
||||
:doc:`command-line script <cli/glance>` (installed as
|
||||
:program:`glance`).
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
reference/index
|
||||
cli/index
|
||||
|
@ -1,8 +0,0 @@
|
||||
======================
|
||||
Python API Reference
|
||||
======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
autoindex
|
@ -1,89 +0,0 @@
|
||||
Python API v2
|
||||
=============
|
||||
|
||||
To create a client::
|
||||
|
||||
from keystoneauth1 import loading
|
||||
from keystoneauth1 import session
|
||||
from glanceclient import Client
|
||||
|
||||
loader = loading.get_plugin_loader('password')
|
||||
auth = loader.load_from_options(
|
||||
auth_url=AUTH_URL,
|
||||
username=USERNAME,
|
||||
password=PASSWORD,
|
||||
project_id=PROJECT_ID)
|
||||
session = session.Session(auth=auth)
|
||||
|
||||
glance = Client('2', session=session)
|
||||
|
||||
|
||||
Create
|
||||
------
|
||||
Create a new image::
|
||||
|
||||
image = glance.images.create(name="myNewImage")
|
||||
glance.images.upload(image.id, open('/tmp/myimage.iso', 'rb'))
|
||||
|
||||
Show
|
||||
----
|
||||
Describe a specific image::
|
||||
|
||||
glance.images.get(image.id)
|
||||
|
||||
Update
|
||||
------
|
||||
Update a specific image::
|
||||
|
||||
# update with a list of image attribute names and their new values
|
||||
glance.images.update(image.id, name="myNewImageName")
|
||||
|
||||
Custom Properties
|
||||
-----------------
|
||||
Set a custom property on an image::
|
||||
|
||||
# set an arbitrary property on an image
|
||||
glance.images.update(image.id, my_custom_property='value')
|
||||
|
||||
Remove a custom property from an image::
|
||||
|
||||
# remove the custom property 'my_custom_property'
|
||||
glance.images.update(image.id, remove_props=['my_custom_property'])
|
||||
|
||||
Delete
|
||||
------
|
||||
Delete specified image(s)::
|
||||
|
||||
glance.images.delete(image.id)
|
||||
|
||||
List
|
||||
----
|
||||
List images you can access::
|
||||
|
||||
for image in glance.images.list():
|
||||
print image
|
||||
|
||||
Download
|
||||
--------
|
||||
Download a specific image::
|
||||
|
||||
d = glance.images.data(image.id)
|
||||
|
||||
Share an Image
|
||||
--------------
|
||||
Share a specific image with a tenant::
|
||||
|
||||
glance.image_members.create(image_id, member_id)
|
||||
|
||||
Remove a Share
|
||||
--------------
|
||||
Remove a shared image from a tenant::
|
||||
|
||||
glance.image_members.delete(image_id, member_id)
|
||||
|
||||
List Sharings
|
||||
-------------
|
||||
Describe sharing permissions by image or tenant::
|
||||
|
||||
glance.image_members.list(image_id)
|
||||
|
@ -1,27 +0,0 @@
|
||||
==========================
|
||||
Python Library Reference
|
||||
==========================
|
||||
|
||||
In order to use the python api directly, you must first obtain an auth
|
||||
token and identify which endpoint you wish to speak to. Once you have
|
||||
done so, you can use the API like so::
|
||||
|
||||
>>> from glanceclient import Client
|
||||
>>> glance = Client('1', endpoint=OS_IMAGE_ENDPOINT, token=OS_AUTH_TOKEN)
|
||||
>>> image = glance.images.create(name="My Test Image")
|
||||
>>> print image.status
|
||||
'queued'
|
||||
>>> image.update(data=open('/tmp/myimage.iso', 'rb'))
|
||||
>>> print image.status
|
||||
'active'
|
||||
>>> image.update(properties=dict(my_custom_property='value'))
|
||||
>>> with open('/tmp/copyimage.iso', 'wb') as f:
|
||||
for chunk in image.data():
|
||||
f.write(chunk)
|
||||
>>> image.delete()
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
api/index
|
||||
apiv2
|
@ -1,31 +0,0 @@
|
||||
# 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 glanceclient.client
|
||||
Client = glanceclient.client.Client
|
||||
except ImportError:
|
||||
import warnings
|
||||
warnings.warn("Could not import glanceclient.client", ImportWarning)
|
||||
|
||||
import pbr.version
|
||||
|
||||
version_info = pbr.version.VersionInfo('python-glanceclient')
|
||||
|
||||
try:
|
||||
__version__ = version_info.version_string()
|
||||
except AttributeError:
|
||||
__version__ = None
|
@ -1,21 +0,0 @@
|
||||
# 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='glanceclient')
|
||||
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = _translators.primary
|
@ -1,64 +0,0 @@
|
||||
# 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 oslo_utils import importutils
|
||||
|
||||
from glanceclient.common import utils
|
||||
|
||||
|
||||
def Client(version=None, endpoint=None, session=None, *args, **kwargs):
|
||||
"""Client for the OpenStack Images API.
|
||||
|
||||
Generic client for the OpenStack Images API. 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 = importutils.import_versioned_module('glanceclient', int(version),
|
||||
'client')
|
||||
client_class = getattr(module, 'Client')
|
||||
return client_class(endpoint, *args, session=session, **kwargs)
|
@ -1,15 +0,0 @@
|
||||
# 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 is here for compatibility purposes. Once all known OpenStack clients
|
||||
# are updated to use glanceclient.exc, this file should be removed
|
||||
from glanceclient.exc import * # noqa
|
@ -1,359 +0,0 @@
|
||||
# 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
|
||||
import OpenSSL
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import netutils
|
||||
import requests
|
||||
import six
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
|
||||
from glanceclient.common import utils
|
||||
from glanceclient import exc
|
||||
|
||||
osprofiler_web = importutils.try_import("osprofiler.web")
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
USER_AGENT = 'python-glanceclient'
|
||||
CHUNKSIZE = 1024 * 64 # 64kB
|
||||
REQ_ID_HEADER = 'X-OpenStack-Request-ID'
|
||||
|
||||
|
||||
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 headers.items() 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):
|
||||
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')
|
||||
|
||||
# Read body into string if it isn't obviously image data
|
||||
if content_type == 'application/octet-stream':
|
||||
# Do not read all response in memory when downloading an image.
|
||||
body_iter = _close_after_stream(resp, CHUNKSIZE)
|
||||
else:
|
||||
content = resp.text
|
||||
if content_type and content_type.startswith('application/json'):
|
||||
# Let's use requests json method, it should take care of
|
||||
# response encoding
|
||||
body_iter = resp.json()
|
||||
else:
|
||||
body_iter = six.StringIO(content)
|
||||
try:
|
||||
body_iter = json.loads(''.join([c for c in body_iter]))
|
||||
except ValueError:
|
||||
body_iter = None
|
||||
|
||||
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.global_request_id = kwargs.get('global_request_id')
|
||||
if self.identity_headers:
|
||||
self.auth_token = self.identity_headers.pop('X-Auth-Token',
|
||||
self.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"):
|
||||
|
||||
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 headers.items():
|
||||
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 self.identity_headers.items():
|
||||
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 self.global_request_id:
|
||||
headers.setdefault(REQ_ID_HEADER, self.global_request_id)
|
||||
|
||||
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, IOError) as e:
|
||||
endpoint = self.endpoint
|
||||
message = ("Error communicating with %(endpoint)s %(e)s" %
|
||||
{'endpoint': endpoint, 'e': e})
|
||||
raise exc.CommunicationError(message=message)
|
||||
except OpenSSL.SSL.Error as e:
|
||||
message = ("SSL Error communicating with %(url)s: %(e)s" %
|
||||
{'url': conn_url, 'e': e})
|
||||
raise exc.CommunicationError(message=message)
|
||||
|
||||
# log request-id for each api call
|
||||
request_id = resp.headers.get('x-openstack-request-id')
|
||||
if request_id:
|
||||
LOG.debug('%(method)s call to image for '
|
||||
'%(url)s used request id '
|
||||
'%(response_request_id)s',
|
||||
{'method': resp.request.method,
|
||||
'url': resp.url,
|
||||
'response_request_id': 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', 'image')
|
||||
self.global_request_id = kwargs.pop('global_request_id', None)
|
||||
super(SessionClient, self).__init__(session, **kwargs)
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
headers = kwargs.pop('headers', {})
|
||||
if self.global_request_id:
|
||||
headers.setdefault(REQ_ID_HEADER, self.global_request_id)
|
||||
|
||||
kwargs['raise_exc'] = False
|
||||
data = self._set_common_request_kwargs(headers, kwargs)
|
||||
try:
|
||||
# NOTE(pumaranikar): To avoid bug #1641239, no modification of
|
||||
# headers should be allowed after encode_headers() is called.
|
||||
resp = super(SessionClient,
|
||||
self).request(url,
|
||||
method,
|
||||
headers=encode_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)
|
||||
|
||||
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')
|
@ -1,261 +0,0 @@
|
||||
# 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
|
||||
|
||||
|
||||
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 glanceclient 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 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))
|
@ -1,99 +0,0 @@
|
||||
# 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() and self._totalsize != 0
|
||||
self._percent = 0
|
||||
|
||||
def _display_progress_bar(self, size_read):
|
||||
if self._show_progress:
|
||||
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
|
@ -1,558 +0,0 @@
|
||||
# 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 functools
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import six.moves.urllib.parse as urlparse
|
||||
import sys
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
import six
|
||||
|
||||
if os.name == 'nt':
|
||||
import msvcrt
|
||||
else:
|
||||
msvcrt = None
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import strutils
|
||||
import prettytable
|
||||
import wrapt
|
||||
|
||||
from glanceclient._i18n import _
|
||||
from glanceclient import exc
|
||||
|
||||
|
||||
_memoized_property_lock = threading.Lock()
|
||||
|
||||
SENSITIVE_HEADERS = ('X-Auth-Token', )
|
||||
REQUIRED_FIELDS_ON_DATA = ('disk_format', 'container_format')
|
||||
|
||||
|
||||
# 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 on_data_require_fields(data_fields, required=REQUIRED_FIELDS_ON_DATA):
|
||||
"""Decorator to check commands' validity
|
||||
|
||||
This decorator checks that required fields are present when image
|
||||
data has been supplied via command line arguments or via stdin
|
||||
|
||||
On error throws CommandError exception with meaningful message.
|
||||
|
||||
:param data_fields: Which fields' presence imply image data
|
||||
:type data_fields: iter
|
||||
:param required: Required fields
|
||||
:type required: iter
|
||||
:return: function decorator
|
||||
"""
|
||||
|
||||
def args_decorator(func):
|
||||
def prepare_fields(fields):
|
||||
args = ('--' + x.replace('_', '-') for x in fields)
|
||||
return ', '.join(args)
|
||||
|
||||
@functools.wraps(func)
|
||||
def func_wrapper(gc, args):
|
||||
# Set of arguments with data
|
||||
fields = set(a[0] for a in vars(args).items() if a[1])
|
||||
|
||||
# Fields the conditional requirements depend on
|
||||
present = fields.intersection(data_fields)
|
||||
|
||||
# How many conditional requirements are missing
|
||||
missing = set(required) - fields
|
||||
|
||||
# We use get_data_file to check if data is provided in stdin
|
||||
if (present or get_data_file(args)) and missing:
|
||||
msg = (_("error: Must provide %(req)s when using %(opt)s.") %
|
||||
{'req': prepare_fields(missing),
|
||||
'opt': prepare_fields(present) or 'stdin'})
|
||||
raise exc.CommandError(msg)
|
||||
return func(gc, args)
|
||||
return func_wrapper
|
||||
return args_decorator
|
||||
|
||||
|
||||
def schema_args(schema_getter, omit=None):
|
||||
omit = omit or []
|
||||
typemap = {
|
||||
'string': encodeutils.safe_decode,
|
||||
'integer': int,
|
||||
'boolean': strutils.bool_from_string,
|
||||
'array': list
|
||||
}
|
||||
|
||||
def _decorator(func):
|
||||
schema = schema_getter()
|
||||
if schema is None:
|
||||
param = '<unavailable>'
|
||||
kwargs = {
|
||||
'help': ("Please run with connection parameters set to "
|
||||
"retrieve the schema for generating help for this "
|
||||
"command")
|
||||
}
|
||||
func.__dict__.setdefault('arguments', []).insert(0, ((param, ),
|
||||
kwargs))
|
||||
else:
|
||||
properties = schema.get('properties', {})
|
||||
for name, property in properties.items():
|
||||
if name in omit:
|
||||
continue
|
||||
param = '--' + name.replace('_', '-')
|
||||
kwargs = {}
|
||||
|
||||
type_str = property.get('type', 'string')
|
||||
|
||||
if isinstance(type_str, list):
|
||||
# NOTE(flaper87): This means the server has
|
||||
# returned something like `['null', 'string']`,
|
||||
# therefore we use the first non-`null` type as
|
||||
# the valid type.
|
||||
for t in type_str:
|
||||
if t != 'null':
|
||||
type_str = t
|
||||
break
|
||||
|
||||
if type_str == 'array':
|
||||
items = property.get('items')
|
||||
kwargs['type'] = typemap.get(items.get('type'))
|
||||
kwargs['nargs'] = '+'
|
||||
else:
|
||||
kwargs['type'] = typemap.get(type_str)
|
||||
|
||||
if type_str == 'boolean':
|
||||
kwargs['metavar'] = '[True|False]'
|
||||
else:
|
||||
kwargs['metavar'] = '<%s>' % name.upper()
|
||||
|
||||
description = property.get('description', "")
|
||||
if 'enum' in property:
|
||||
if len(description):
|
||||
description += " "
|
||||
|
||||
# NOTE(flaper87): Make sure all values are `str/unicode`
|
||||
# for the `join` to succeed. Enum types can also be `None`
|
||||
# therefore, join's call would fail without the following
|
||||
# list comprehension
|
||||
vals = [six.text_type(val) for val in property.get('enum')]
|
||||
description += ('Valid values: ' + ', '.join(vals))
|
||||
kwargs['help'] = description
|
||||
|
||||
func.__dict__.setdefault('arguments',
|
||||
[]).insert(0, ((param, ), kwargs))
|
||||
return func
|
||||
|
||||
return _decorator
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters=None, field_settings=None):
|
||||
formatters = formatters or {}
|
||||
field_settings = field_settings or {}
|
||||
pt = prettytable.PrettyTable([f for f in fields], caching=False)
|
||||
pt.align = 'l'
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in field_settings:
|
||||
for setting, value in field_settings[field].items():
|
||||
setting_dict = getattr(pt, setting)
|
||||
setting_dict[field] = value
|
||||
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
field_name = field.lower().replace(' ', '_')
|
||||
data = getattr(o, field_name, None) or ''
|
||||
row.append(data)
|
||||
pt.add_row(row)
|
||||
|
||||
print(encodeutils.safe_decode(pt.get_string()))
|
||||
|
||||
|
||||
def print_dict(d, max_column_width=80):
|
||||
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
|
||||
pt.align = 'l'
|
||||
pt.max_width = max_column_width
|
||||
for k, v in d.items():
|
||||
if isinstance(v, (dict, list)):
|
||||
v = json.dumps(v)
|
||||
pt.add_row([k, v])
|
||||
print(encodeutils.safe_decode(pt.get_string(sortby='Property')))
|
||||
|
||||
|
||||
def find_resource(manager, name_or_id):
|
||||
"""Helper for the _find_* methods."""
|
||||
# first try to get entity as integer id
|
||||
try:
|
||||
if isinstance(name_or_id, int) or name_or_id.isdigit():
|
||||
return manager.get(int(name_or_id))
|
||||
except exc.NotFound:
|
||||
pass
|
||||
|
||||
# now try to get entity as uuid
|
||||
try:
|
||||
# This must be unicode for Python 3 compatibility.
|
||||
# If you pass a bytestring to uuid.UUID, you will get a TypeError
|
||||
uuid.UUID(encodeutils.safe_decode(name_or_id))
|
||||
return manager.get(name_or_id)
|
||||
except (ValueError, exc.NotFound):
|
||||
pass
|
||||
|
||||
# finally try to find entity by name
|
||||
matches = list(manager.list(filters={'name': name_or_id}))
|
||||
num_matches = len(matches)
|
||||
if num_matches == 0:
|
||||
msg = "No %s with a name or ID of '%s' exists." % \
|
||||
(manager.resource_class.__name__.lower(), name_or_id)
|
||||
raise exc.CommandError(msg)
|
||||
elif num_matches > 1:
|
||||
msg = ("Multiple %s matches found for '%s', use an ID to be more"
|
||||
" specific." % (manager.resource_class.__name__.lower(),
|
||||
name_or_id))
|
||||
raise exc.CommandError(msg)
|
||||
else:
|
||||
return matches[0]
|
||||
|
||||
|
||||
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 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 save_image(data, path):
|
||||
"""Save an image to the specified path.
|
||||
|
||||
:param data: binary data of the image
|
||||
:param path: path to save the image to
|
||||
"""
|
||||
if path is None:
|
||||
# NOTE(kragniz): for py3 compatibility: sys.stdout.buffer is only
|
||||
# present on py3, otherwise fall back to sys.stdout
|
||||
image = getattr(sys.stdout, 'buffer',
|
||||
sys.stdout)
|
||||
else:
|
||||
image = open(path, 'wb')
|
||||
try:
|
||||
for chunk in data:
|
||||
image.write(chunk)
|
||||
finally:
|
||||
if path is not None:
|
||||
image.close()
|
||||
|
||||
|
||||
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 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 image data to the client,
|
||||
# echo testdata | bin/glance 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
|
||||
|
||||
|
||||
def get_data_file(args):
|
||||
if args.file:
|
||||
return open(args.file, 'rb')
|
||||
else:
|
||||
# distinguish cases where:
|
||||
# (1) stdin is not valid (as in cron jobs):
|
||||
# glance ... <&-
|
||||
# (2) image data is provided through standard input:
|
||||
# glance ... < /tmp/file or cat /tmp/file | glance ...
|
||||
# (3) no image data provided:
|
||||
# glance ...
|
||||
try:
|
||||
os.fstat(0)
|
||||
except OSError:
|
||||
# (1) stdin is not valid (closed...)
|
||||
return None
|
||||
if not sys.stdin.isatty():
|
||||
# (2) image data is provided through standard input
|
||||
image = sys.stdin
|
||||
if hasattr(sys.stdin, 'buffer'):
|
||||
image = sys.stdin.buffer
|
||||
if msvcrt:
|
||||
msvcrt.setmode(image.fileno(), os.O_BINARY)
|
||||
return image
|
||||
else:
|
||||
# (3) no image data provided
|
||||
return None
|
||||
|
||||
|
||||
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 print_image(image_obj, human_readable=False, max_col_width=None):
|
||||
ignore = ['self', 'access', 'file', 'schema']
|
||||
image = dict([item for item in image_obj.items()
|
||||
if item[0] not in ignore])
|
||||
if human_readable:
|
||||
image['size'] = make_size_human_readable(image['size'])
|
||||
if str(max_col_width).isdigit():
|
||||
print_dict(image, max_column_width=max_col_width)
|
||||
else:
|
||||
print_dict(image)
|
||||
|
||||
|
||||
def integrity_iter(iter, checksum):
|
||||
"""Check image data 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 image download. Checksum was %s expected %s' %
|
||||
(md5sum, checksum))
|
||||
|
||||
|
||||
def memoized_property(fn):
|
||||
attr_name = '_lazy_once_' + fn.__name__
|
||||
|
||||
@property
|
||||
def _memoized_property(self):
|
||||
if hasattr(self, attr_name):
|
||||
return getattr(self, attr_name)
|
||||
else:
|
||||
with _memoized_property_lock:
|
||||
if not hasattr(self, attr_name):
|
||||
setattr(self, attr_name, fn(self))
|
||||
return getattr(self, attr_name)
|
||||
return _memoized_property
|
||||
|
||||
|
||||
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('GLANCECLIENT_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)
|
||||
|
||||
# In Python 3, __next__() has replaced next().
|
||||
__next__ = next
|
||||
|
||||
def __len__(self):
|
||||
return self.length
|
||||
|
||||
|
||||
class RequestIdProxy(wrapt.ObjectProxy):
|
||||
def __init__(self, wrapped):
|
||||
# `wrapped` is a tuple: (original_obj, response_obj)
|
||||
super(RequestIdProxy, self).__init__(wrapped[0])
|
||||
self._self_wrapped = wrapped[0]
|
||||
req_id = _extract_request_id(wrapped[1])
|
||||
self._self_request_ids = [req_id]
|
||||
|
||||
@property
|
||||
def request_ids(self):
|
||||
return self._self_request_ids
|
||||
|
||||
@property
|
||||
def wrapped(self):
|
||||
return self._self_wrapped
|
||||
|
||||
# Overriden next method to act as iterator
|
||||
def next(self):
|
||||
return next(self._self_wrapped)
|
||||
|
||||
# In Python 3, __next__() has replaced next().
|
||||
__next__ = next
|
||||
|
||||
|
||||
class GeneratorProxy(wrapt.ObjectProxy):
|
||||
def __init__(self, wrapped):
|
||||
super(GeneratorProxy, self).__init__(wrapped)
|
||||
self._self_wrapped = wrapped
|
||||
self._self_request_ids = []
|
||||
|
||||
def _set_request_ids(self, resp):
|
||||
if self._self_request_ids == []:
|
||||
req_id = _extract_request_id(resp)
|
||||
self._self_request_ids = [req_id]
|
||||
|
||||
def _next(self):
|
||||
obj, resp = next(self._self_wrapped)
|
||||
self._set_request_ids(resp)
|
||||
return obj
|
||||
|
||||
# Override generator's next method to add
|
||||
# request id on each iteration
|
||||
def next(self):
|
||||
return self._next()
|
||||
|
||||
# For Python 3 compatibility
|
||||
def __next__(self):
|
||||
return self._next()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
@property
|
||||
def request_ids(self):
|
||||
return self._self_request_ids
|
||||
|
||||
@property
|
||||
def wrapped(self):
|
||||
return self._self_wrapped
|
||||
|
||||
|
||||
def add_req_id_to_object():
|
||||
@wrapt.decorator
|
||||
def inner(wrapped, instance, args, kwargs):
|
||||
return RequestIdProxy(wrapped(*args, **kwargs))
|
||||
return inner
|
||||
|
||||
|
||||
def add_req_id_to_generator():
|
||||
@wrapt.decorator
|
||||
def inner(wrapped, instance, args, kwargs):
|
||||
return GeneratorProxy(wrapped(*args, **kwargs))
|
||||
return inner
|
||||
|
||||
|
||||
def _extract_request_id(resp):
|
||||
# TODO(rsjethani): Do we need more checks here?
|
||||
return resp.headers.get('x-openstack-request-id')
|
@ -1,205 +0,0 @@
|
||||
# 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 Images 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 = [obj.get('message') for obj in response.json().values()]
|
||||
# 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
|
@ -1,703 +0,0 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Command-line interface to the OpenStack Images API.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import getpass
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
import glanceclient
|
||||
from glanceclient._i18n import _
|
||||
from glanceclient.common import utils
|
||||
from glanceclient import exc
|
||||
|
||||
from keystoneauth1 import discover
|
||||
from keystoneauth1 import exceptions as ks_exc
|
||||
from keystoneauth1.identity import v2 as v2_auth
|
||||
from keystoneauth1.identity import v3 as v3_auth
|
||||
from keystoneauth1 import loading
|
||||
|
||||
osprofiler_profiler = importutils.try_import("osprofiler.profiler")
|
||||
|
||||
SUPPORTED_VERSIONS = [1, 2]
|
||||
|
||||
|
||||
class OpenStackImagesShell(object):
|
||||
|
||||
def _append_global_identity_args(self, parser, argv):
|
||||
# register common identity args
|
||||
parser.set_defaults(os_auth_url=utils.env('OS_AUTH_URL'))
|
||||
|
||||
parser.set_defaults(os_project_name=utils.env(
|
||||
'OS_PROJECT_NAME', 'OS_TENANT_NAME'))
|
||||
parser.set_defaults(os_project_id=utils.env(
|
||||
'OS_PROJECT_ID', 'OS_TENANT_ID'))
|
||||
|
||||
parser.add_argument('--key-file',
|
||||
dest='os_key',
|
||||
help='DEPRECATED! Use --os-key.')
|
||||
|
||||
parser.add_argument('--ca-file',
|
||||
dest='os_cacert',
|
||||
help='DEPRECATED! Use --os-cacert.')
|
||||
|
||||
parser.add_argument('--cert-file',
|
||||
dest='os_cert',
|
||||
help='DEPRECATED! Use --os-cert.')
|
||||
|
||||
parser.add_argument('--os_tenant_id',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os_tenant_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-region-name',
|
||||
default=utils.env('OS_REGION_NAME'),
|
||||
help='Defaults to env[OS_REGION_NAME].')
|
||||
|
||||
parser.add_argument('--os_region_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-auth-token',
|
||||
default=utils.env('OS_AUTH_TOKEN'),
|
||||
help='Defaults to env[OS_AUTH_TOKEN].')
|
||||
|
||||
parser.add_argument('--os_auth_token',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-service-type',
|
||||
default=utils.env('OS_SERVICE_TYPE'),
|
||||
help='Defaults to env[OS_SERVICE_TYPE].')
|
||||
|
||||
parser.add_argument('--os_service_type',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-endpoint-type',
|
||||
default=utils.env('OS_ENDPOINT_TYPE'),
|
||||
help='Defaults to env[OS_ENDPOINT_TYPE].')
|
||||
|
||||
parser.add_argument('--os_endpoint_type',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
loading.register_session_argparse_arguments(parser)
|
||||
# Peek into argv to see if os-auth-token (or the deprecated
|
||||
# os_auth_token) or the new os-token or the environment variable
|
||||
# OS_AUTH_TOKEN were given. In which case, the token auth plugin is
|
||||
# what the user wants. Else, we'll default to password.
|
||||
default_auth_plugin = 'password'
|
||||
token_opts = ['os-token', 'os-auth-token', 'os_auth-token']
|
||||
if argv and any(i in token_opts for i in argv):
|
||||
default_auth_plugin = 'token'
|
||||
loading.register_auth_argparse_arguments(
|
||||
parser, argv, default=default_auth_plugin)
|
||||
|
||||
def get_base_parser(self, argv):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='glance',
|
||||
description=__doc__.strip(),
|
||||
epilog='See "glance help COMMAND" '
|
||||
'for help on a specific command.',
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter,
|
||||
)
|
||||
|
||||
# Global arguments
|
||||
parser.add_argument('-h', '--help',
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--version',
|
||||
action='version',
|
||||
version=glanceclient.__version__)
|
||||
|
||||
parser.add_argument('-d', '--debug',
|
||||
default=bool(utils.env('GLANCECLIENT_DEBUG')),
|
||||
action='store_true',
|
||||
help='Defaults to env[GLANCECLIENT_DEBUG].')
|
||||
|
||||
parser.add_argument('-v', '--verbose',
|
||||
default=False, action="store_true",
|
||||
help="Print more verbose output.")
|
||||
|
||||
parser.add_argument('--get-schema',
|
||||
default=False, action="store_true",
|
||||
dest='get_schema',
|
||||
help='Ignores cached copy and forces retrieval '
|
||||
'of schema that generates portions of the '
|
||||
'help text. Ignored with API version 1.')
|
||||
|
||||
parser.add_argument('-f', '--force',
|
||||
dest='force',
|
||||
default=False, action='store_true',
|
||||
help='Prevent select actions from requesting '
|
||||
'user confirmation.')
|
||||
|
||||
parser.add_argument('--os-image-url',
|
||||
default=utils.env('OS_IMAGE_URL'),
|
||||
help=('Defaults to env[OS_IMAGE_URL]. '
|
||||
'If the provided image url contains '
|
||||
'a version number and '
|
||||
'`--os-image-api-version` is omitted '
|
||||
'the version of the URL will be picked as '
|
||||
'the image api version to use.'))
|
||||
|
||||
parser.add_argument('--os_image_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-image-api-version',
|
||||
default=utils.env('OS_IMAGE_API_VERSION',
|
||||
default=None),
|
||||
help='Defaults to env[OS_IMAGE_API_VERSION] or 2.')
|
||||
|
||||
parser.add_argument('--os_image_api_version',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
if osprofiler_profiler:
|
||||
parser.add_argument('--profile',
|
||||
metavar='HMAC_KEY',
|
||||
help='HMAC key to use for encrypting context '
|
||||
'data for performance profiling of operation. '
|
||||
'This key should be the value of HMAC key '
|
||||
'configured in osprofiler middleware in '
|
||||
'glance, it is specified in paste '
|
||||
'configuration file at '
|
||||
'/etc/glance/api-paste.ini and '
|
||||
'/etc/glance/registry-paste.ini. Without key '
|
||||
'the profiling will not be triggered even '
|
||||
'if osprofiler is enabled on server side.')
|
||||
|
||||
self._append_global_identity_args(parser, argv)
|
||||
|
||||
return parser
|
||||
|
||||
def get_subcommand_parser(self, version, argv=None):
|
||||
parser = self.get_base_parser(argv)
|
||||
|
||||
self.subcommands = {}
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
submodule = importutils.import_versioned_module('glanceclient',
|
||||
version, 'shell')
|
||||
|
||||
self._find_actions(subparsers, submodule)
|
||||
self._find_actions(subparsers, self)
|
||||
|
||||
self._add_bash_completion_subparser(subparsers)
|
||||
|
||||
return parser
|
||||
|
||||
def _find_actions(self, subparsers, actions_module):
|
||||
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||
# Replace underscores with hyphens in the commands
|
||||
# displayed to the user
|
||||
command = attr[3:].replace('_', '-')
|
||||
callback = getattr(actions_module, attr)
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(command,
|
||||
help=help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter
|
||||
)
|
||||
subparser.add_argument('-h', '--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
self.subcommands[command] = subparser
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
def _add_bash_completion_subparser(self, subparsers):
|
||||
subparser = subparsers.add_parser('bash_completion',
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter)
|
||||
self.subcommands['bash_completion'] = subparser
|
||||
subparser.set_defaults(func=self.do_bash_completion)
|
||||
|
||||
def _get_image_url(self, args):
|
||||
"""Translate the available url-related options into a single string.
|
||||
|
||||
Return the endpoint that should be used to talk to Glance if a
|
||||
clear decision can be made. Otherwise, return None.
|
||||
"""
|
||||
if args.os_image_url:
|
||||
return args.os_image_url
|
||||
else:
|
||||
return None
|
||||
|
||||
def _discover_auth_versions(self, session, auth_url):
|
||||
# discover the API versions the server is supporting base on the
|
||||
# given URL
|
||||
v2_auth_url = None
|
||||
v3_auth_url = None
|
||||
try:
|
||||
ks_discover = discover.Discover(session=session, url=auth_url)
|
||||
v2_auth_url = ks_discover.url_for('2.0')
|
||||
v3_auth_url = ks_discover.url_for('3.0')
|
||||
except ks_exc.ClientException as e:
|
||||
# Identity service may not support discover API version.
|
||||
# Lets trying to figure out the API version from the original URL.
|
||||
url_parts = urlparse.urlparse(auth_url)
|
||||
(scheme, netloc, path, params, query, fragment) = url_parts
|
||||
path = path.lower()
|
||||
if path.startswith('/v3'):
|
||||
v3_auth_url = auth_url
|
||||
elif path.startswith('/v2'):
|
||||
v2_auth_url = auth_url
|
||||
else:
|
||||
# not enough information to determine the auth version
|
||||
msg = ('Unable to determine the Keystone version '
|
||||
'to authenticate with using the given '
|
||||
'auth_url. Identity service may not support API '
|
||||
'version discovery. Please provide a versioned '
|
||||
'auth_url instead. error=%s') % (e)
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
return (v2_auth_url, v3_auth_url)
|
||||
|
||||
def _get_keystone_auth_plugin(self, ks_session, **kwargs):
|
||||
# discover the supported keystone versions using the given auth url
|
||||
auth_url = kwargs.pop('auth_url', None)
|
||||
(v2_auth_url, v3_auth_url) = self._discover_auth_versions(
|
||||
session=ks_session,
|
||||
auth_url=auth_url)
|
||||
|
||||
# Determine which authentication plugin to use. First inspect the
|
||||
# auth_url to see the supported version. If both v3 and v2 are
|
||||
# supported, then use the highest version if possible.
|
||||
user_id = kwargs.pop('user_id', None)
|
||||
username = kwargs.pop('username', None)
|
||||
password = kwargs.pop('password', None)
|
||||
user_domain_name = kwargs.pop('user_domain_name', None)
|
||||
user_domain_id = kwargs.pop('user_domain_id', None)
|
||||
# project and tenant can be used interchangeably
|
||||
project_id = (kwargs.pop('project_id', None) or
|
||||
kwargs.pop('tenant_id', None))
|
||||
project_name = (kwargs.pop('project_name', None) or
|
||||
kwargs.pop('tenant_name', None))
|
||||
project_domain_id = kwargs.pop('project_domain_id', None)
|
||||
project_domain_name = kwargs.pop('project_domain_name', None)
|
||||
auth = None
|
||||
|
||||
use_domain = (user_domain_id or
|
||||
user_domain_name or
|
||||
project_domain_id or
|
||||
project_domain_name)
|
||||
use_v3 = v3_auth_url and (use_domain or (not v2_auth_url))
|
||||
use_v2 = v2_auth_url and not use_domain
|
||||
|
||||
if use_v3:
|
||||
auth = v3_auth.Password(
|
||||
v3_auth_url,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
password=password,
|
||||
user_domain_id=user_domain_id,
|
||||
user_domain_name=user_domain_name,
|
||||
project_id=project_id,
|
||||
project_name=project_name,
|
||||
project_domain_id=project_domain_id,
|
||||
project_domain_name=project_domain_name)
|
||||
elif use_v2:
|
||||
auth = v2_auth.Password(
|
||||
v2_auth_url,
|
||||
username,
|
||||
password,
|
||||
tenant_id=project_id,
|
||||
tenant_name=project_name)
|
||||
else:
|
||||
# if we get here it means domain information is provided
|
||||
# (caller meant to use Keystone V3) but the auth url is
|
||||
# actually Keystone V2. Obviously we can't authenticate a V3
|
||||
# user using V2.
|
||||
exc.CommandError("Credential and auth_url mismatch. The given "
|
||||
"auth_url is using Keystone V2 endpoint, which "
|
||||
"may not able to handle Keystone V3 credentials. "
|
||||
"Please provide a correct Keystone V3 auth_url.")
|
||||
|
||||
return auth
|
||||
|
||||
def _get_kwargs_to_create_auth_plugin(self, args):
|
||||
if not args.os_username:
|
||||
raise exc.CommandError(
|
||||
_("You must provide a username via"
|
||||
" either --os-username or "
|
||||
"env[OS_USERNAME]"))
|
||||
|
||||
if not args.os_password:
|
||||
# No password, If we've got a tty, try prompting for it
|
||||
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
|
||||
# Check for Ctl-D
|
||||
try:
|
||||
args.os_password = getpass.getpass('OS Password: ')
|
||||
except EOFError:
|
||||
pass
|
||||
# No password because we didn't have a tty or the
|
||||
# user Ctl-D when prompted.
|
||||
if not args.os_password:
|
||||
raise exc.CommandError(
|
||||
_("You must provide a password via "
|
||||
"either --os-password, "
|
||||
"env[OS_PASSWORD], "
|
||||
"or prompted response"))
|
||||
|
||||
# Validate password flow auth
|
||||
os_project_name = getattr(
|
||||
args, 'os_project_name', getattr(args, 'os_tenant_name', None))
|
||||
os_project_id = getattr(
|
||||
args, 'os_project_id', getattr(args, 'os_tenant_id', None))
|
||||
if not any([os_project_name, os_project_id]):
|
||||
# tenant is deprecated in Keystone v3. Use the latest
|
||||
# terminology instead.
|
||||
raise exc.CommandError(
|
||||
_("You must provide a project_id or project_name ("
|
||||
"with project_domain_name or project_domain_id) "
|
||||
"via "
|
||||
" --os-project-id (env[OS_PROJECT_ID])"
|
||||
" --os-project-name (env[OS_PROJECT_NAME]),"
|
||||
" --os-project-domain-id "
|
||||
"(env[OS_PROJECT_DOMAIN_ID])"
|
||||
" --os-project-domain-name "
|
||||
"(env[OS_PROJECT_DOMAIN_NAME])"))
|
||||
|
||||
if not args.os_auth_url:
|
||||
raise exc.CommandError(
|
||||
_("You must provide an auth url via"
|
||||
" either --os-auth-url or "
|
||||
"via env[OS_AUTH_URL]"))
|
||||
|
||||
kwargs = {
|
||||
'auth_url': args.os_auth_url,
|
||||
'username': args.os_username,
|
||||
'user_id': args.os_user_id,
|
||||
'user_domain_id': args.os_user_domain_id,
|
||||
'user_domain_name': args.os_user_domain_name,
|
||||
'password': args.os_password,
|
||||
'tenant_name': args.os_tenant_name,
|
||||
'tenant_id': args.os_tenant_id,
|
||||
'project_name': args.os_project_name,
|
||||
'project_id': args.os_project_id,
|
||||
'project_domain_name': args.os_project_domain_name,
|
||||
'project_domain_id': args.os_project_domain_id,
|
||||
}
|
||||
return kwargs
|
||||
|
||||
def _get_versioned_client(self, api_version, args):
|
||||
endpoint = self._get_image_url(args)
|
||||
auth_token = args.os_auth_token
|
||||
|
||||
if endpoint and auth_token:
|
||||
kwargs = {
|
||||
'token': auth_token,
|
||||
'insecure': args.insecure,
|
||||
'timeout': args.timeout,
|
||||
'cacert': args.os_cacert,
|
||||
'cert': args.os_cert,
|
||||
'key': args.os_key,
|
||||
}
|
||||
else:
|
||||
ks_session = loading.load_session_from_argparse_arguments(args)
|
||||
auth_plugin_kwargs = self._get_kwargs_to_create_auth_plugin(args)
|
||||
ks_session.auth = self._get_keystone_auth_plugin(
|
||||
ks_session=ks_session, **auth_plugin_kwargs)
|
||||
kwargs = {'session': ks_session}
|
||||
|
||||
if endpoint is None:
|
||||
endpoint_type = args.os_endpoint_type or 'public'
|
||||
service_type = args.os_service_type or 'image'
|
||||
endpoint = ks_session.get_endpoint(
|
||||
service_type=service_type,
|
||||
interface=endpoint_type,
|
||||
region_name=args.os_region_name)
|
||||
|
||||
return glanceclient.Client(api_version, endpoint, **kwargs)
|
||||
|
||||
def _cache_schemas(self, options, client, home_dir='~/.glanceclient'):
|
||||
homedir = os.path.expanduser(home_dir)
|
||||
path_prefix = homedir
|
||||
if options.os_auth_url:
|
||||
hash_host = hashlib.sha1(options.os_auth_url.encode('utf-8'))
|
||||
path_prefix = os.path.join(path_prefix, hash_host.hexdigest())
|
||||
if not os.path.exists(path_prefix):
|
||||
try:
|
||||
os.makedirs(path_prefix)
|
||||
except OSError as e:
|
||||
# This avoids glanceclient to crash if it can't write to
|
||||
# ~/.glanceclient, which may happen on some env (for me,
|
||||
# it happens in Jenkins, as glanceclient can't write to
|
||||
# /var/lib/jenkins).
|
||||
msg = '%s' % e
|
||||
print(encodeutils.safe_decode(msg), file=sys.stderr)
|
||||
resources = ['image', 'metadefs/namespace', 'metadefs/resource_type']
|
||||
schema_file_paths = [os.path.join(path_prefix, x + '_schema.json')
|
||||
for x in ['image', 'namespace', 'resource_type']]
|
||||
|
||||
failed_download_schema = 0
|
||||
for resource, schema_file_path in zip(resources, schema_file_paths):
|
||||
if (not os.path.exists(schema_file_path)) or options.get_schema:
|
||||
try:
|
||||
schema = client.schemas.get(resource)
|
||||
with open(schema_file_path, 'w') as f:
|
||||
f.write(json.dumps(schema.raw()))
|
||||
except exc.Unauthorized:
|
||||
raise exc.CommandError(
|
||||
"Invalid OpenStack Identity credentials.")
|
||||
except Exception:
|
||||
# NOTE(esheffield) do nothing here, we'll get a message
|
||||
# later if the schema is missing
|
||||
failed_download_schema += 1
|
||||
pass
|
||||
|
||||
return failed_download_schema >= len(resources)
|
||||
|
||||
def main(self, argv):
|
||||
|
||||
def _get_subparser(api_version):
|
||||
try:
|
||||
return self.get_subcommand_parser(api_version, argv)
|
||||
except ImportError as e:
|
||||
if not str(e):
|
||||
# Add a generic import error message if the raised
|
||||
# ImportError has none.
|
||||
raise ImportError('Unable to import module. Re-run '
|
||||
'with --debug for more info.')
|
||||
raise
|
||||
|
||||
# Parse args once to find version
|
||||
|
||||
# NOTE(flepied) Under Python3, parsed arguments are removed
|
||||
# from the list so make a copy for the first parsing
|
||||
base_argv = copy.deepcopy(argv)
|
||||
parser = self.get_base_parser(argv)
|
||||
(options, args) = parser.parse_known_args(base_argv)
|
||||
|
||||
try:
|
||||
# NOTE(flaper87): Try to get the version from the
|
||||
# image-url first. If no version was specified, fallback
|
||||
# to the api-image-version arg. If both of these fail then
|
||||
# fallback to the minimum supported one and let keystone
|
||||
# do the magic.
|
||||
endpoint = self._get_image_url(options)
|
||||
endpoint, url_version = utils.strip_version(endpoint)
|
||||
except ValueError:
|
||||
# NOTE(flaper87): ValueError is raised if no endpoint is provided
|
||||
url_version = None
|
||||
|
||||
# build available subcommands based on version
|
||||
try:
|
||||
api_version = int(options.os_image_api_version or url_version or 2)
|
||||
if api_version not in SUPPORTED_VERSIONS:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
msg = ("Invalid API version parameter. "
|
||||
"Supported values are %s" % SUPPORTED_VERSIONS)
|
||||
utils.exit(msg=msg)
|
||||
|
||||
# Handle top-level --help/-h before attempting to parse
|
||||
# a command off the command line
|
||||
if options.help or not argv:
|
||||
parser = _get_subparser(api_version)
|
||||
self.do_help(options, parser=parser)
|
||||
return 0
|
||||
|
||||
# NOTE(sigmavirus24): Above, args is defined as the left over
|
||||
# arguments from parser.parse_known_args(). This allows us to
|
||||
# skip any parameters to command-line flags that may have been passed
|
||||
# to glanceclient, e.g., --os-auth-token.
|
||||
self._fixup_subcommand(args, argv)
|
||||
|
||||
# short-circuit and deal with help command right away.
|
||||
sub_parser = _get_subparser(api_version)
|
||||
args = sub_parser.parse_args(argv)
|
||||
|
||||
if args.func == self.do_help:
|
||||
self.do_help(args, parser=sub_parser)
|
||||
return 0
|
||||
elif args.func == self.do_bash_completion:
|
||||
self.do_bash_completion(args)
|
||||
return 0
|
||||
|
||||
if not options.os_image_api_version and api_version == 2:
|
||||
switch_version = True
|
||||
client = self._get_versioned_client('2', args)
|
||||
|
||||
resp, body = client.http_client.get('/versions')
|
||||
|
||||
for version in body['versions']:
|
||||
if version['id'].startswith('v2'):
|
||||
# NOTE(flaper87): We know v2 is enabled in the server,
|
||||
# which means we should be able to get the schemas and
|
||||
# move on.
|
||||
switch_version = self._cache_schemas(options, client)
|
||||
break
|
||||
|
||||
if switch_version:
|
||||
print('WARNING: The client is falling back to v1 because'
|
||||
' the accessing to v2 failed. This behavior will'
|
||||
' be removed in future versions', file=sys.stderr)
|
||||
api_version = 1
|
||||
|
||||
sub_parser = _get_subparser(api_version)
|
||||
|
||||
# Parse args again and call whatever callback was selected
|
||||
args = sub_parser.parse_args(argv)
|
||||
|
||||
# NOTE(flaper87): Make sure we re-use the password input if we
|
||||
# have one. This may happen if the schemas were downloaded in
|
||||
# this same command. Password will be asked to download the
|
||||
# schemas and then for the operations below.
|
||||
if not args.os_password and options.os_password:
|
||||
args.os_password = options.os_password
|
||||
|
||||
if args.debug:
|
||||
# Set up the root logger to debug so that the submodules can
|
||||
# print debug messages
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
# for iso8601 < 0.1.11
|
||||
logging.getLogger('iso8601').setLevel(logging.WARNING)
|
||||
LOG = logging.getLogger('glanceclient')
|
||||
LOG.addHandler(logging.StreamHandler())
|
||||
LOG.setLevel(logging.DEBUG if args.debug else logging.INFO)
|
||||
|
||||
profile = osprofiler_profiler and options.profile
|
||||
if profile:
|
||||
osprofiler_profiler.init(options.profile)
|
||||
|
||||
client = self._get_versioned_client(api_version, args)
|
||||
|
||||
try:
|
||||
args.func(client, args)
|
||||
except exc.Unauthorized:
|
||||
raise exc.CommandError("Invalid OpenStack Identity credentials.")
|
||||
finally:
|
||||
if profile:
|
||||
trace_id = osprofiler_profiler.get().get_base_id()
|
||||
print("Profiling trace ID: %s" % trace_id)
|
||||
print("To display trace use next command:\n"
|
||||
"osprofiler trace show --html %s " % trace_id)
|
||||
|
||||
@staticmethod
|
||||
def _fixup_subcommand(unknown_args, argv):
|
||||
# NOTE(sigmavirus24): Sometimes users pass the wrong subcommand name
|
||||
# to glanceclient. If they're using Python 2 they will see an error:
|
||||
# > invalid choice: u'imgae-list' (choose from ...)
|
||||
# To avoid this, we look at the extra args already parsed from above
|
||||
# and try to predict what the subcommand will be based on it being the
|
||||
# first non - or -- prefixed argument in args. We then find that in
|
||||
# argv and encode it from unicode so users don't see the pesky `u'`
|
||||
# prefix.
|
||||
for arg in unknown_args:
|
||||
if not arg.startswith('-'): # This will cover both - and --
|
||||
subcommand_name = arg
|
||||
break
|
||||
else:
|
||||
subcommand_name = ''
|
||||
|
||||
if (subcommand_name and six.PY2 and
|
||||
isinstance(subcommand_name, six.text_type)):
|
||||
# NOTE(sigmavirus24): if we found a subcommand name, then let's
|
||||
# find it in the argv list and replace it with a bytes object
|
||||
# instead. Note, that if we encode the argument on Python 3, the
|
||||
# user will instead see a pesky `b'` string instead of the `u'`
|
||||
# string we mention above.
|
||||
subcommand_index = argv.index(subcommand_name)
|
||||
argv[subcommand_index] = encodeutils.safe_encode(subcommand_name)
|
||||
|
||||
@utils.arg('command', metavar='<subcommand>', nargs='?',
|
||||
help='Display help for <subcommand>.')
|
||||
def do_help(self, args, parser):
|
||||
"""Display help about this program or one of its subcommands."""
|
||||
command = getattr(args, 'command', '')
|
||||
|
||||
if command:
|
||||
if args.command in self.subcommands:
|
||||
self.subcommands[args.command].print_help()
|
||||
else:
|
||||
raise exc.CommandError("'%s' is not a valid subcommand" %
|
||||
args.command)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
if not args.os_image_api_version or args.os_image_api_version == '2':
|
||||
# NOTE(NiallBunting) This currently assumes that the only versions
|
||||
# are one and two.
|
||||
try:
|
||||
if command is None:
|
||||
print("\nRun `glance --os-image-api-version 1 help`"
|
||||
" for v1 help")
|
||||
else:
|
||||
self.get_subcommand_parser(1)
|
||||
if command in self.subcommands:
|
||||
command = ' ' + command
|
||||
print(("\nRun `glance --os-image-api-version 1 help%s`"
|
||||
" for v1 help") % (command or ''))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def do_bash_completion(self, _args):
|
||||
"""Prints arguments for bash_completion.
|
||||
|
||||
Prints all of the commands and options to stdout so that the
|
||||
glance.bash_completion script doesn't have to hard code them.
|
||||
"""
|
||||
commands = set()
|
||||
options = set()
|
||||
for sc_str, sc in self.subcommands.items():
|
||||
commands.add(sc_str)
|
||||
for option in sc._optionals._option_string_actions.keys():
|
||||
options.add(option)
|
||||
|
||||
commands.remove('bash_completion')
|
||||
commands.remove('bash-completion')
|
||||
print(' '.join(commands | options))
|
||||
|
||||
|
||||
class HelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
# Title-case the headings
|
||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||
super(HelpFormatter, self).start_section(heading)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
argv = [encodeutils.safe_decode(a) for a in sys.argv[1:]]
|
||||
OpenStackImagesShell().main(argv)
|
||||
except KeyboardInterrupt:
|
||||
utils.exit('... terminating glance client', exit_code=130)
|
||||
except Exception as e:
|
||||
if utils.debug_enabled(argv) is True:
|
||||
traceback.print_exc()
|
||||
utils.exit(encodeutils.exception_to_unicode(e))
|
@ -1,53 +0,0 @@
|
||||
======================================
|
||||
python-glanceclient functional testing
|
||||
======================================
|
||||
|
||||
Idea
|
||||
----
|
||||
|
||||
Run real client/server requests in the gate to catch issues which
|
||||
are difficult to catch with a purely unit test approach.
|
||||
|
||||
Many projects (nova, keystone...) already have this form of testing in
|
||||
the gate.
|
||||
|
||||
|
||||
Testing Theory
|
||||
--------------
|
||||
|
||||
Since python-glanceclient has two uses, CLI and python API, we should
|
||||
have two sets of functional tests. CLI and python API. The python API
|
||||
tests should never use the CLI. But the CLI tests can use the python API
|
||||
where adding native support to the CLI for the required functionality
|
||||
would involve a non trivial amount of work.
|
||||
|
||||
|
||||
Functional Test Guidelines
|
||||
--------------------------
|
||||
|
||||
The functional tests require:
|
||||
|
||||
1) A working Glance/Keystone installation (eg devstack)
|
||||
2) A yaml file containing valid credentials
|
||||
|
||||
If you are using devstack a yaml file will have been created for you.
|
||||
|
||||
If you are not using devstack you should create a yaml file
|
||||
with the following format:
|
||||
|
||||
clouds:
|
||||
devstack-admin:
|
||||
auth:
|
||||
auth_url: http://10.0.0.1:35357/v2.0
|
||||
|
||||
password: example
|
||||
|
||||
project_name: admin
|
||||
|
||||
username: admin
|
||||
|
||||
identity_api_version: '2.0'
|
||||
|
||||
region_name: RegionOne
|
||||
|
||||
and copy it to ~/.config/openstack/clouds.yaml
|
@ -1,91 +0,0 @@
|
||||
# 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 glanceclient
|
||||
from keystoneauth1 import loading
|
||||
from keystoneauth1 import session
|
||||
import os
|
||||
import os_client_config
|
||||
from tempest.lib.cli import base
|
||||
|
||||
|
||||
def credentials(cloud='devstack-admin'):
|
||||
"""Retrieves credentials to run functional tests
|
||||
|
||||
Credentials are either read via os-client-config from the environment
|
||||
or from a config file ('clouds.yaml'). Environment variables override
|
||||
those from the config file.
|
||||
|
||||
devstack produces a clouds.yaml with two named clouds - one named
|
||||
'devstack' which has user privs and one named 'devstack-admin' which
|
||||
has admin privs. This function will default to getting the devstack-admin
|
||||
cloud as that is the current expected behavior.
|
||||
"""
|
||||
|
||||
return os_client_config.OpenStackConfig().get_one_cloud(cloud=cloud)
|
||||
|
||||
|
||||
class ClientTestBase(base.ClientTestBase):
|
||||
"""This is a first pass at a simple read only python-glanceclient test.
|
||||
|
||||
This only exercises client commands that are read only.
|
||||
This should test commands:
|
||||
* as a regular user
|
||||
* as an admin user
|
||||
* with and without optional parameters
|
||||
* initially just check return codes, and later test command outputs
|
||||
|
||||
"""
|
||||
|
||||
def _get_clients(self):
|
||||
self.creds = credentials().get_auth_args()
|
||||
cli_dir = os.environ.get(
|
||||
'OS_GLANCECLIENT_EXEC_DIR',
|
||||
os.path.join(os.path.abspath('.'), '.tox/functional/bin'))
|
||||
|
||||
return base.CLIClient(
|
||||
username=self.creds['username'],
|
||||
password=self.creds['password'],
|
||||
tenant_name=self.creds['project_name'],
|
||||
uri=self.creds['auth_url'],
|
||||
cli_dir=cli_dir)
|
||||
|
||||
def glance(self, *args, **kwargs):
|
||||
return self.clients.glance(*args,
|
||||
**kwargs)
|
||||
|
||||
def glance_pyclient(self):
|
||||
ks_creds = dict(
|
||||
auth_url=self.creds["auth_url"],
|
||||
username=self.creds["username"],
|
||||
password=self.creds["password"],
|
||||
project_name=self.creds["project_name"])
|
||||
keystoneclient = self.Keystone(**ks_creds)
|
||||
return self.Glance(keystoneclient)
|
||||
|
||||
class Keystone(object):
|
||||
def __init__(self, **kwargs):
|
||||
loader = loading.get_plugin_loader("password")
|
||||
auth = loader.load_from_options(**kwargs)
|
||||
self.session = session.Session(auth=auth)
|
||||
|
||||
class Glance(object):
|
||||
def __init__(self, keystone, version="2"):
|
||||
self.glance = glanceclient.Client(
|
||||
version,
|
||||
session=keystone.session)
|
||||
|
||||
def find(self, image_name):
|
||||
for image in self.glance.images.list():
|
||||
if image.name == image_name:
|
||||
return image
|
||||
return None
|
@ -1,46 +0,0 @@
|
||||
#!/bin/bash -xe
|
||||
|
||||
# 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 script is executed inside post_test_hook function in devstack gate.
|
||||
|
||||
function generate_testr_results {
|
||||
if [ -f .testrepository/0 ]; then
|
||||
sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit
|
||||
sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit
|
||||
sudo /usr/os-testr-env/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html
|
||||
sudo gzip -9 $BASE/logs/testrepository.subunit
|
||||
sudo gzip -9 $BASE/logs/testr_results.html
|
||||
sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz
|
||||
sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz
|
||||
fi
|
||||
}
|
||||
|
||||
export GLANCECLIENT_DIR="$BASE/new/python-glanceclient"
|
||||
|
||||
sudo chown -R jenkins:stack $GLANCECLIENT_DIR
|
||||
|
||||
# Go to the glanceclient dir
|
||||
cd $GLANCECLIENT_DIR
|
||||
|
||||
# Run tests
|
||||
echo "Running glanceclient functional test suite"
|
||||
set +e
|
||||
# Preserve env for OS_ credentials
|
||||
sudo -E -H -u jenkins tox -efunctional
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
# Collect and parse result
|
||||
generate_testr_results
|
||||
exit $EXIT_CODE
|
@ -1,61 +0,0 @@
|
||||
# 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 glanceclient.tests.functional import base
|
||||
import time
|
||||
|
||||
|
||||
IMAGE = {"protected": False,
|
||||
"disk_format": "qcow2",
|
||||
"name": "glance_functional_test_image.img",
|
||||
"visibility": "private",
|
||||
"container_format": "bare"}
|
||||
|
||||
|
||||
class HttpHeadersTest(base.ClientTestBase):
|
||||
def test_encode_headers_python(self):
|
||||
"""Test proper handling of Content-Type headers.
|
||||
|
||||
encode_headers() must be called as late as possible before a
|
||||
request is sent. If this principle is violated, and if any
|
||||
changes are made to the headers between encode_headers() and the
|
||||
actual request (for instance a call to
|
||||
_set_common_request_kwargs()), and if you're trying to set a
|
||||
Content-Type that is not equal to application/octet-stream (the
|
||||
default), it is entirely possible that you'll end up with two
|
||||
Content-Type headers defined (yours plus
|
||||
application/octet-stream). The request will go out the door with
|
||||
only one of them chosen seemingly at random.
|
||||
|
||||
This test uses a call to update() because it sets a header such
|
||||
as the following (this example may be subject to change):
|
||||
Content-Type: application/openstack-images-v2.1-json-patch
|
||||
|
||||
This situation only occurs in python3. This test will never fail
|
||||
in python2.
|
||||
|
||||
There is no test against the CLI because it swallows the error.
|
||||
"""
|
||||
# the failure is intermittent - try up to 6 times
|
||||
for attempt in range(0, 6):
|
||||
glanceclient = self.glance_pyclient()
|
||||
image = glanceclient.find(IMAGE["name"])
|
||||
if image:
|
||||
glanceclient.glance.images.delete(image.id)
|
||||
image = glanceclient.glance.images.create(name=IMAGE["name"])
|
||||
self.assertTrue(image.status == "queued")
|
||||
try:
|
||||
image = glanceclient.glance.images.update(image.id,
|
||||
disk_format="qcow2")
|
||||
except Exception as e:
|
||||
self.assertFalse("415 Unsupported Media Type" in e.details)
|
||||
time.sleep(5)
|
@ -1,111 +0,0 @@
|
||||
# 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
|
||||
|
||||
from tempest.lib import exceptions
|
||||
|
||||
from glanceclient.tests.functional import base
|
||||
|
||||
|
||||
class SimpleReadOnlyGlanceClientTest(base.ClientTestBase):
|
||||
|
||||
"""Read only functional python-glanceclient tests.
|
||||
|
||||
This only exercises client commands that are read only.
|
||||
"""
|
||||
|
||||
def test_list_v1(self):
|
||||
out = self.glance('--os-image-api-version 1 image-list')
|
||||
endpoints = self.parser.listing(out)
|
||||
self.assertTableStruct(endpoints, [
|
||||
'ID', 'Name', 'Disk Format', 'Container Format',
|
||||
'Size', 'Status'])
|
||||
|
||||
def test_list_v2(self):
|
||||
out = self.glance('--os-image-api-version 2 image-list')
|
||||
endpoints = self.parser.listing(out)
|
||||
self.assertTableStruct(endpoints, ['ID', 'Name'])
|
||||
|
||||
def test_fake_action(self):
|
||||
self.assertRaises(exceptions.CommandFailed,
|
||||
self.glance,
|
||||
'this-does-not-exist')
|
||||
|
||||
def test_member_list_v1(self):
|
||||
tenant_name = '--tenant-id %s' % self.creds['project_name']
|
||||
out = self.glance('--os-image-api-version 1 member-list',
|
||||
params=tenant_name)
|
||||
endpoints = self.parser.listing(out)
|
||||
self.assertTableStruct(endpoints,
|
||||
['Image ID', 'Member ID', 'Can Share'])
|
||||
|
||||
def test_member_list_v2(self):
|
||||
try:
|
||||
# NOTE(flwang): If set disk-format and container-format, Jenkins
|
||||
# will raise an error said can't recognize the params, though it
|
||||
# works fine at local. Without the two params, Glance will
|
||||
# complain. So we just catch the exception can skip it.
|
||||
self.glance('--os-image-api-version 2 image-create --name temp')
|
||||
except Exception:
|
||||
pass
|
||||
out = self.glance('--os-image-api-version 2 image-list'
|
||||
' --visibility private')
|
||||
image_list = self.parser.listing(out)
|
||||
# NOTE(flwang): Because the member-list command of v2 is using
|
||||
# image-id as required parameter, so we have to get a valid image id
|
||||
# based on current environment. If there is no valid image id, we will
|
||||
# pass in a fake one and expect a 404 error.
|
||||
if len(image_list) > 0:
|
||||
param_image_id = '--image-id %s' % image_list[0]['ID']
|
||||
out = self.glance('--os-image-api-version 2 member-list',
|
||||
params=param_image_id)
|
||||
endpoints = self.parser.listing(out)
|
||||
self.assertTableStruct(endpoints,
|
||||
['Image ID', 'Member ID', 'Status'])
|
||||
else:
|
||||
param_image_id = '--image-id fake_image_id'
|
||||
self.assertRaises(exceptions.CommandFailed,
|
||||
self.glance,
|
||||
'--os-image-api-version 2 member-list',
|
||||
params=param_image_id)
|
||||
|
||||
def test_help(self):
|
||||
help_text = self.glance('--os-image-api-version 2 help')
|
||||
lines = help_text.split('\n')
|
||||
self.assertFirstLineStartsWith(lines, 'usage: glance')
|
||||
|
||||
commands = []
|
||||
cmds_start = lines.index('Positional arguments:')
|
||||
cmds_end = lines.index('Optional arguments:')
|
||||
command_pattern = re.compile('^ {4}([a-z0-9\-\_]+)')
|
||||
for line in lines[cmds_start:cmds_end]:
|
||||
match = command_pattern.match(line)
|
||||
if match:
|
||||
commands.append(match.group(1))
|
||||
commands = set(commands)
|
||||
wanted_commands = {'bash-completion', 'help',
|
||||
'image-create', 'image-deactivate', 'image-delete',
|
||||
'image-download', 'image-list', 'image-reactivate',
|
||||
'image-show', 'image-tag-delete',
|
||||
'image-tag-update', 'image-update', 'image-upload',
|
||||
'location-add', 'location-delete',
|
||||
'location-update', 'member-create', 'member-delete',
|
||||
'member-list', 'member-update', 'task-create',
|
||||
'task-list', 'task-show'}
|
||||
self.assertFalse(wanted_commands - commands)
|
||||
|
||||
def test_version(self):
|
||||
self.glance('', flags='--version')
|
||||
|
||||
def test_debug_list(self):
|
||||
self.glance('--os-image-api-version 2 image-list', flags='--debug')
|
@ -1,63 +0,0 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright (C) 2013 Yahoo! 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 glanceclient.v1.apiclient import base
|
||||
|
||||
|
||||
class TestBase(testtools.TestCase):
|
||||
|
||||
def test_resource_repr(self):
|
||||
r = base.Resource(None, dict(foo="bar", baz="spam"))
|
||||
self.assertEqual("<Resource baz=spam, foo=bar>", repr(r))
|
||||
|
||||
def test_getid(self):
|
||||
self.assertEqual(4, base.getid(4))
|
||||
|
||||
class TmpObject(object):
|
||||
id = 4
|
||||
self.assertEqual(4, base.getid(TmpObject))
|
||||
|
||||
def test_two_resources_with_same_id_are_not_equal(self):
|
||||
# Two resources with same ID: never equal if their info is not equal
|
||||
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
|
||||
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||
self.assertNotEqual(r1, r2)
|
||||
|
||||
def test_two_resources_with_same_id_and_info_are_equal(self):
|
||||
# Two resources with same ID: equal if their info is equal
|
||||
r1 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
def test_two_resources_with_eq_info_are_equal(self):
|
||||
# Two resources with no ID: equal if their info is equal
|
||||
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
def test_two_resources_with_diff_id_are_not_equal(self):
|
||||
# Two resources with diff ID: not equal
|
||||
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
|
||||
r2 = base.Resource(None, {'id': 2, 'name': 'hello'})
|
||||
self.assertNotEqual(r1, r2)
|
||||
|
||||
def test_two_resources_with_not_eq_info_are_not_equal(self):
|
||||
# Two resources with no ID: not equal if their info is not equal
|
||||
r1 = base.Resource(None, {'name': 'bill', 'age': 21})
|
||||
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
self.assertNotEqual(r1, r2)
|
@ -1,66 +0,0 @@
|
||||
# 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 glanceclient import client
|
||||
from glanceclient import v1
|
||||
from glanceclient import v2
|
||||
|
||||
|
||||
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/v2")
|
||||
self.assertEqual("http://example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v1.client.Client)
|
||||
|
||||
def test_versioned_endpoint_no_version(self):
|
||||
gc = client.Client(endpoint="http://example.com/v2")
|
||||
self.assertEqual("http://example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v2.client.Client)
|
||||
|
||||
def test_versioned_endpoint_with_minor_revision(self):
|
||||
gc = client.Client(2.2, "http://example.com/v2.1")
|
||||
self.assertEqual("http://example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v2.client.Client)
|
||||
|
||||
def test_endpoint_with_version_hostname(self):
|
||||
gc = client.Client(2, "http://v1.example.com")
|
||||
self.assertEqual("http://v1.example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v2.client.Client)
|
||||
|
||||
def test_versioned_endpoint_with_version_hostname_v2(self):
|
||||
gc = client.Client(endpoint="http://v1.example.com/v2")
|
||||
self.assertEqual("http://v1.example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v2.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/v2.1")
|
||||
self.assertEqual("http://v1.example.com", gc.http_client.endpoint)
|
||||
self.assertIsInstance(gc, v2.client.Client)
|
@ -1,78 +0,0 @@
|
||||
# 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
|
||||
import testtools
|
||||
|
||||
from glanceclient import exc
|
||||
|
||||
HTML_MSG = """<html>
|
||||
<head>
|
||||
<title>404 Entity Not Found</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404 Entity Not Found</h1>
|
||||
Entity could not be found
|
||||
<br /><br />
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
class TestHTTPExceptions(testtools.TestCase):
|
||||
def test_from_response(self):
|
||||
"""exc.from_response should return instance of an HTTP exception."""
|
||||
mock_resp = mock.Mock()
|
||||
mock_resp.status_code = 400
|
||||
out = exc.from_response(mock_resp)
|
||||
self.assertIsInstance(out, exc.HTTPBadRequest)
|
||||
|
||||
def test_handles_json(self):
|
||||
"""exc.from_response should not print JSON."""
|
||||
mock_resp = mock.Mock()
|
||||
mock_resp.status_code = 413
|
||||
mock_resp.json.return_value = {
|
||||
"overLimit": {
|
||||
"code": 413,
|
||||
"message": "OverLimit Retry...",
|
||||
"details": "Error Details...",
|
||||
"retryAt": "2014-12-03T13:33:06Z"
|
||||
}
|
||||
}
|
||||
mock_resp.headers = {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
err = exc.from_response(mock_resp, "Non-empty body")
|
||||
self.assertIsInstance(err, exc.HTTPOverLimit)
|
||||
self.assertEqual("OverLimit Retry...", err.details)
|
||||
|
||||
def test_handles_html(self):
|
||||
"""exc.from_response should not print HTML."""
|
||||
mock_resp = mock.Mock()
|
||||
mock_resp.status_code = 404
|
||||
mock_resp.text = HTML_MSG
|
||||
mock_resp.headers = {
|
||||
"content-type": "text/html"
|
||||
}
|
||||
err = exc.from_response(mock_resp, HTML_MSG)
|
||||
self.assertIsInstance(err, exc.HTTPNotFound)
|
||||
self.assertEqual("404 Entity Not Found: Entity could not be found",
|
||||
err.details)
|
||||
|
||||
def test_format_no_content_type(self):
|
||||
mock_resp = mock.Mock()
|
||||
mock_resp.status_code = 400
|
||||
mock_resp.headers = {'content-type': 'application/octet-stream'}
|
||||
body = b'Error \n\n'
|
||||
err = exc.from_response(mock_resp, body)
|
||||
self.assertEqual('Error \n', err.details)
|
@ -1,469 +0,0 @@
|
||||
# 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
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import fixtures
|
||||
from keystoneauth1 import session
|
||||
from keystoneauth1 import token_endpoint
|
||||
import mock
|
||||
from oslo_utils import encodeutils
|
||||
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 glanceclient
|
||||
from glanceclient.common import http
|
||||
from glanceclient.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 = '/v1/images/my-image'
|
||||
self.mock.get(self.endpoint + path)
|
||||
http_client.get(path)
|
||||
|
||||
headers = self.mock.last_request.headers
|
||||
for k, v in identity_headers.items():
|
||||
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_request_id_header_passed(self):
|
||||
global_id = encodeutils.safe_encode("req-%s" % uuid.uuid4())
|
||||
kwargs = {'global_request_id': global_id}
|
||||
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(global_id, headers['X-OpenStack-Request-ID'])
|
||||
|
||||
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.assertNotIn('Accept-Language', 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(glanceclient.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 = '/v1/images/detail?limit=20'
|
||||
self.mock.get(self.endpoint + path, text=cb)
|
||||
|
||||
comm_err = self.assertRaises(glanceclient.exc.CommunicationError,
|
||||
self.client.get,
|
||||
'/v1/images/detail?limit=20')
|
||||
|
||||
self.assertIn(self.endpoint, comm_err.message)
|
||||
|
||||
def test_http_encoding(self):
|
||||
path = '/v1/images/detail'
|
||||
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_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)
|
||||
|
||||
@mock.patch('keystoneauth1.adapter.Adapter.request')
|
||||
def test_http_duplicate_content_type_headers(self, mock_ksarq):
|
||||
"""Test proper handling of Content-Type headers.
|
||||
|
||||
encode_headers() must be called as late as possible before a
|
||||
request is sent. If this principle is violated, and if any
|
||||
changes are made to the headers between encode_headers() and the
|
||||
actual request (for instance a call to
|
||||
_set_common_request_kwargs()), and if you're trying to set a
|
||||
Content-Type that is not equal to application/octet-stream (the
|
||||
default), it is entirely possible that you'll end up with two
|
||||
Content-Type headers defined (yours plus
|
||||
application/octet-stream). The request will go out the door with
|
||||
only one of them chosen seemingly at random.
|
||||
|
||||
This situation only occurs in python3. This test will never fail
|
||||
in python2.
|
||||
"""
|
||||
path = "/v2/images/my-image"
|
||||
headers = {
|
||||
"Content-Type": "application/openstack-images-v2.1-json-patch"
|
||||
}
|
||||
data = '[{"value": "qcow2", "path": "/disk_format", "op": "replace"}]'
|
||||
self.mock.patch(self.endpoint + path)
|
||||
sess_http_client = self._create_session_client()
|
||||
sess_http_client.patch(path, headers=headers, data=data)
|
||||
# Pull out the headers with which Adapter.request was invoked
|
||||
ksarqh = mock_ksarq.call_args[1]['headers']
|
||||
# Only one Content-Type header (of any text-type)
|
||||
self.assertEqual(1, [encodeutils.safe_decode(key)
|
||||
for key in ksarqh.keys()].count(u'Content-Type'))
|
||||
# And it's the one we set
|
||||
self.assertEqual(b"application/openstack-images-v2.1-json-patch",
|
||||
ksarqh[b"Content-Type"])
|
||||
|
||||
def test_raw_request(self):
|
||||
"""Verify the path being used for HTTP requests reflects accurately."""
|
||||
headers = {"Content-Type": "text/plain"}
|
||||
text = 'Ok'
|
||||
path = '/v1/images/detail'
|
||||
|
||||
self.mock.get(self.endpoint + path, text=text, headers=headers)
|
||||
|
||||
resp, body = self.client.get('/v1/images/detail', 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 = '/v1/images/'
|
||||
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 = '/v1/images'
|
||||
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 = '/v1/images/'
|
||||
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('glanceclient.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 needles.items():
|
||||
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('glanceclient.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('glanceclient.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('glanceclient.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('glanceclient.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_log_request_id_once(self):
|
||||
logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG))
|
||||
data = "TEST"
|
||||
path = '/v1/images/'
|
||||
self.mock.get(self.endpoint + path, body=six.StringIO(data),
|
||||
headers={"Content-Type": "application/octet-stream",
|
||||
'x-openstack-request-id': "1234"})
|
||||
|
||||
resp, body = self.client.get(path)
|
||||
self.assertIsInstance(body, types.GeneratorType)
|
||||
self.assertEqual([data], list(body))
|
||||
expected_log = ("GET call to image "
|
||||
"for http://example.com:9292/v1/images/ "
|
||||
"used request id 1234")
|
||||
self.assertEqual(1, logger.output.count(expected_log))
|
||||
|
||||
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 = '/v1/images/my-image'
|
||||
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'])
|
@ -1,82 +0,0 @@
|
||||
# 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 requests
|
||||
import six
|
||||
import testtools
|
||||
|
||||
from glanceclient.common import progressbar
|
||||
from glanceclient.common import utils
|
||||
from glanceclient.tests import utils as test_utils
|
||||
|
||||
|
||||
class TestProgressBarWrapper(testtools.TestCase):
|
||||
|
||||
def test_iter_iterator_display_progress_bar(self):
|
||||
size = 100
|
||||
# create fake response object to return request-id with iterator
|
||||
resp = requests.Response()
|
||||
resp.headers['x-openstack-request-id'] = 'req-1234'
|
||||
iterator_with_len = utils.IterableWithLength(iter('X' * 100), size)
|
||||
requestid_proxy = utils.RequestIdProxy((iterator_with_len, resp))
|
||||
saved_stdout = sys.stdout
|
||||
try:
|
||||
sys.stdout = output = test_utils.FakeTTYStdout()
|
||||
# Consume iterator.
|
||||
data = list(progressbar.VerboseIteratorWrapper(requestid_proxy,
|
||||
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
|
@ -1,991 +0,0 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright (C) 2013 Yahoo! 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 argparse
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from ordereddict import OrderedDict
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
import fixtures
|
||||
from keystoneauth1 import exceptions as ks_exc
|
||||
from keystoneauth1 import fixture as ks_fixture
|
||||
import mock
|
||||
from requests_mock.contrib import fixture as rm_fixture
|
||||
import six
|
||||
|
||||
from glanceclient.common import utils
|
||||
from glanceclient import exc
|
||||
from glanceclient import shell as openstack_shell
|
||||
from glanceclient.tests.unit.v2.fixtures import image_show_fixture
|
||||
from glanceclient.tests.unit.v2.fixtures import image_versions_fixture
|
||||
from glanceclient.tests import utils as testutils
|
||||
|
||||
# NOTE (esheffield) Used for the schema caching tests
|
||||
from glanceclient.v2 import schemas as schemas
|
||||
import json
|
||||
|
||||
|
||||
DEFAULT_IMAGE_URL = 'http://127.0.0.1:9292/'
|
||||
DEFAULT_IMAGE_URL_INTERNAL = 'http://127.0.0.1:9191/'
|
||||
DEFAULT_USERNAME = 'username'
|
||||
DEFAULT_PASSWORD = 'password'
|
||||
DEFAULT_TENANT_ID = 'tenant_id'
|
||||
DEFAULT_TENANT_NAME = 'tenant_name'
|
||||
DEFAULT_PROJECT_ID = '0123456789'
|
||||
DEFAULT_USER_DOMAIN_NAME = 'user_domain_name'
|
||||
DEFAULT_UNVERSIONED_AUTH_URL = 'http://127.0.0.1:5000/'
|
||||
DEFAULT_V2_AUTH_URL = '%sv2.0' % DEFAULT_UNVERSIONED_AUTH_URL
|
||||
DEFAULT_V3_AUTH_URL = '%sv3' % DEFAULT_UNVERSIONED_AUTH_URL
|
||||
DEFAULT_AUTH_TOKEN = ' 3bcc3d3a03f44e3d8377f9247b0ad155'
|
||||
TEST_SERVICE_URL = 'http://127.0.0.1:5000/'
|
||||
DEFAULT_SERVICE_TYPE = 'image'
|
||||
DEFAULT_ENDPOINT_TYPE = 'public'
|
||||
|
||||
FAKE_V2_ENV = {'OS_USERNAME': DEFAULT_USERNAME,
|
||||
'OS_PASSWORD': DEFAULT_PASSWORD,
|
||||
'OS_TENANT_NAME': DEFAULT_TENANT_NAME,
|
||||
'OS_AUTH_URL': DEFAULT_V2_AUTH_URL,
|
||||
'OS_IMAGE_URL': DEFAULT_IMAGE_URL}
|
||||
|
||||
FAKE_V3_ENV = {'OS_USERNAME': DEFAULT_USERNAME,
|
||||
'OS_PASSWORD': DEFAULT_PASSWORD,
|
||||
'OS_PROJECT_ID': DEFAULT_PROJECT_ID,
|
||||
'OS_USER_DOMAIN_NAME': DEFAULT_USER_DOMAIN_NAME,
|
||||
'OS_AUTH_URL': DEFAULT_V3_AUTH_URL,
|
||||
'OS_IMAGE_URL': DEFAULT_IMAGE_URL}
|
||||
|
||||
FAKE_V4_ENV = {'OS_USERNAME': DEFAULT_USERNAME,
|
||||
'OS_PASSWORD': DEFAULT_PASSWORD,
|
||||
'OS_PROJECT_ID': DEFAULT_PROJECT_ID,
|
||||
'OS_USER_DOMAIN_NAME': DEFAULT_USER_DOMAIN_NAME,
|
||||
'OS_AUTH_URL': DEFAULT_V3_AUTH_URL,
|
||||
'OS_SERVICE_TYPE': DEFAULT_SERVICE_TYPE,
|
||||
'OS_ENDPOINT_TYPE': DEFAULT_ENDPOINT_TYPE,
|
||||
'OS_AUTH_TOKEN': DEFAULT_AUTH_TOKEN}
|
||||
|
||||
TOKEN_ID = uuid.uuid4().hex
|
||||
|
||||
V2_TOKEN = ks_fixture.V2Token(token_id=TOKEN_ID)
|
||||
V2_TOKEN.set_scope()
|
||||
_s = V2_TOKEN.add_service('image', name='glance')
|
||||
_s.add_endpoint(DEFAULT_IMAGE_URL)
|
||||
|
||||
V3_TOKEN = ks_fixture.V3Token()
|
||||
V3_TOKEN.set_project_scope()
|
||||
_s = V3_TOKEN.add_service('image', name='glance')
|
||||
_s.add_standard_endpoints(public=DEFAULT_IMAGE_URL,
|
||||
internal=DEFAULT_IMAGE_URL_INTERNAL)
|
||||
|
||||
|
||||
class ShellTest(testutils.TestCase):
|
||||
# auth environment to use
|
||||
auth_env = FAKE_V2_ENV.copy()
|
||||
# expected auth plugin to invoke
|
||||
token_url = DEFAULT_V2_AUTH_URL + '/tokens'
|
||||
|
||||
# Patch os.environ to avoid required auth info
|
||||
def make_env(self, exclude=None):
|
||||
env = dict((k, v) for k, v in self.auth_env.items() if k != exclude)
|
||||
self.useFixture(fixtures.MonkeyPatch('os.environ', env))
|
||||
|
||||
def setUp(self):
|
||||
super(ShellTest, self).setUp()
|
||||
global _old_env
|
||||
_old_env, os.environ = os.environ, self.auth_env
|
||||
|
||||
self.requests = self.useFixture(rm_fixture.Fixture())
|
||||
|
||||
json_list = ks_fixture.DiscoveryList(DEFAULT_UNVERSIONED_AUTH_URL)
|
||||
self.requests.get(DEFAULT_UNVERSIONED_AUTH_URL,
|
||||
json=json_list,
|
||||
status_code=300)
|
||||
|
||||
json_v2 = {'version': ks_fixture.V2Discovery(DEFAULT_V2_AUTH_URL)}
|
||||
self.requests.get(DEFAULT_V2_AUTH_URL, json=json_v2)
|
||||
|
||||
json_v3 = {'version': ks_fixture.V3Discovery(DEFAULT_V3_AUTH_URL)}
|
||||
self.requests.get(DEFAULT_V3_AUTH_URL, json=json_v3)
|
||||
|
||||
self.v2_auth = self.requests.post(DEFAULT_V2_AUTH_URL + '/tokens',
|
||||
json=V2_TOKEN)
|
||||
|
||||
headers = {'X-Subject-Token': TOKEN_ID}
|
||||
self.v3_auth = self.requests.post(DEFAULT_V3_AUTH_URL + '/auth/tokens',
|
||||
headers=headers,
|
||||
json=V3_TOKEN)
|
||||
|
||||
global shell, _shell, assert_called, assert_called_anytime
|
||||
_shell = openstack_shell.OpenStackImagesShell()
|
||||
shell = lambda cmd: _shell.main(cmd.split())
|
||||
|
||||
def tearDown(self):
|
||||
super(ShellTest, self).tearDown()
|
||||
global _old_env
|
||||
os.environ = _old_env
|
||||
|
||||
def shell(self, argstr, exitcodes=(0,)):
|
||||
orig = sys.stdout
|
||||
orig_stderr = sys.stderr
|
||||
try:
|
||||
sys.stdout = six.StringIO()
|
||||
sys.stderr = six.StringIO()
|
||||
_shell = openstack_shell.OpenStackImagesShell()
|
||||
_shell.main(argstr.split())
|
||||
except SystemExit:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
self.assertIn(exc_value.code, exitcodes)
|
||||
finally:
|
||||
stdout = sys.stdout.getvalue()
|
||||
sys.stdout.close()
|
||||
sys.stdout = orig
|
||||
stderr = sys.stderr.getvalue()
|
||||
sys.stderr.close()
|
||||
sys.stderr = orig_stderr
|
||||
return (stdout, stderr)
|
||||
|
||||
def test_fixup_subcommand(self):
|
||||
arglist = [u'image-list', u'--help']
|
||||
if six.PY2:
|
||||
expected_arglist = [b'image-list', u'--help']
|
||||
elif six.PY3:
|
||||
expected_arglist = [u'image-list', u'--help']
|
||||
|
||||
openstack_shell.OpenStackImagesShell._fixup_subcommand(
|
||||
arglist, arglist
|
||||
)
|
||||
self.assertEqual(expected_arglist, arglist)
|
||||
|
||||
def test_fixup_subcommand_with_options_preceding(self):
|
||||
arglist = [u'--os-auth-token', u'abcdef', u'image-list', u'--help']
|
||||
unknown = arglist[2:]
|
||||
if six.PY2:
|
||||
expected_arglist = [
|
||||
u'--os-auth-token', u'abcdef', b'image-list', u'--help'
|
||||
]
|
||||
elif six.PY3:
|
||||
expected_arglist = [
|
||||
u'--os-auth-token', u'abcdef', u'image-list', u'--help'
|
||||
]
|
||||
|
||||
openstack_shell.OpenStackImagesShell._fixup_subcommand(
|
||||
unknown, arglist
|
||||
)
|
||||
self.assertEqual(expected_arglist, arglist)
|
||||
|
||||
def test_help_unknown_command(self):
|
||||
shell = openstack_shell.OpenStackImagesShell()
|
||||
argstr = '--os-image-api-version 2 help foofoo'
|
||||
self.assertRaises(exc.CommandError, shell.main, argstr.split())
|
||||
|
||||
@mock.patch('sys.stdout', six.StringIO())
|
||||
@mock.patch('sys.stderr', six.StringIO())
|
||||
@mock.patch('sys.argv', ['glance', 'help', 'foofoo'])
|
||||
def test_no_stacktrace_when_debug_disabled(self):
|
||||
with mock.patch.object(traceback, 'print_exc') as mock_print_exc:
|
||||
try:
|
||||
openstack_shell.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
self.assertFalse(mock_print_exc.called)
|
||||
|
||||
@mock.patch('sys.stdout', six.StringIO())
|
||||
@mock.patch('sys.stderr', six.StringIO())
|
||||
@mock.patch('sys.argv', ['glance', 'help', 'foofoo'])
|
||||
def test_stacktrace_when_debug_enabled_by_env(self):
|
||||
old_environment = os.environ.copy()
|
||||
os.environ = {'GLANCECLIENT_DEBUG': '1'}
|
||||
try:
|
||||
with mock.patch.object(traceback, 'print_exc') as mock_print_exc:
|
||||
try:
|
||||
openstack_shell.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
self.assertTrue(mock_print_exc.called)
|
||||
finally:
|
||||
os.environ = old_environment
|
||||
|
||||
@mock.patch('sys.stdout', six.StringIO())
|
||||
@mock.patch('sys.stderr', six.StringIO())
|
||||
@mock.patch('sys.argv', ['glance', '--debug', 'help', 'foofoo'])
|
||||
def test_stacktrace_when_debug_enabled(self):
|
||||
with mock.patch.object(traceback, 'print_exc') as mock_print_exc:
|
||||
try:
|
||||
openstack_shell.main()
|
||||
except SystemExit:
|
||||
pass
|
||||
self.assertTrue(mock_print_exc.called)
|
||||
|
||||
def test_help(self):
|
||||
shell = openstack_shell.OpenStackImagesShell()
|
||||
argstr = '--os-image-api-version 2 help'
|
||||
with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock:
|
||||
actual = shell.main(argstr.split())
|
||||
self.assertEqual(0, actual)
|
||||
self.assertFalse(et_mock.called)
|
||||
|
||||
def test_blank_call(self):
|
||||
shell = openstack_shell.OpenStackImagesShell()
|
||||
with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock:
|
||||
actual = shell.main('')
|
||||
self.assertEqual(0, actual)
|
||||
self.assertFalse(et_mock.called)
|
||||
|
||||
def test_help_on_subcommand_error(self):
|
||||
self.assertRaises(exc.CommandError, shell,
|
||||
'--os-image-api-version 2 help bad')
|
||||
|
||||
def test_help_v2_no_schema(self):
|
||||
shell = openstack_shell.OpenStackImagesShell()
|
||||
argstr = '--os-image-api-version 2 help image-create'
|
||||
with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock:
|
||||
actual = shell.main(argstr.split())
|
||||
self.assertEqual(0, actual)
|
||||
self.assertNotIn('<unavailable>', actual)
|
||||
self.assertFalse(et_mock.called)
|
||||
|
||||
argstr = '--os-image-api-version 2 help md-namespace-create'
|
||||
with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock:
|
||||
actual = shell.main(argstr.split())
|
||||
self.assertEqual(0, actual)
|
||||
self.assertNotIn('<unavailable>', actual)
|
||||
self.assertFalse(et_mock.called)
|
||||
|
||||
argstr = '--os-image-api-version 2 help md-resource-type-associate'
|
||||
with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock:
|
||||
actual = shell.main(argstr.split())
|
||||
self.assertEqual(0, actual)
|
||||
self.assertNotIn('<unavailable>', actual)
|
||||
self.assertFalse(et_mock.called)
|
||||
|
||||
def test_get_base_parser(self):
|
||||
test_shell = openstack_shell.OpenStackImagesShell()
|
||||
# NOTE(stevemar): Use the current sys.argv for base_parser since it
|
||||
# doesn't matter for this test, it just needs to initialize the CLI
|
||||
actual_parser = test_shell.get_base_parser(sys.argv)
|
||||
description = 'Command-line interface to the OpenStack Images API.'
|
||||
expected = argparse.ArgumentParser(
|
||||
prog='glance', usage=None,
|
||||
description=description,
|
||||
conflict_handler='error',
|
||||
add_help=False,
|
||||
formatter_class=openstack_shell.HelpFormatter,)
|
||||
# NOTE(guochbo): Can't compare ArgumentParser instances directly
|
||||
# Convert ArgumentPaser to string first.
|
||||
self.assertEqual(str(expected), str(actual_parser))
|
||||
|
||||
@mock.patch.object(openstack_shell.OpenStackImagesShell,
|
||||
'_get_versioned_client')
|
||||
def test_cert_and_key_args_interchangeable(self,
|
||||
mock_versioned_client):
|
||||
# make sure --os-cert and --os-key are passed correctly
|
||||
args = ('--os-image-api-version 2 '
|
||||
'--os-cert mycert '
|
||||
'--os-key mykey image-list')
|
||||
shell(args)
|
||||
assert mock_versioned_client.called
|
||||
((api_version, args), kwargs) = mock_versioned_client.call_args
|
||||
self.assertEqual('mycert', args.os_cert)
|
||||
self.assertEqual('mykey', args.os_key)
|
||||
|
||||
# make sure we get the same thing with --cert-file and --key-file
|
||||
args = ('--os-image-api-version 2 '
|
||||
'--cert-file mycertfile '
|
||||
'--key-file mykeyfile image-list')
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
assert mock_versioned_client.called
|
||||
((api_version, args), kwargs) = mock_versioned_client.call_args
|
||||
self.assertEqual('mycertfile', args.os_cert)
|
||||
self.assertEqual('mykeyfile', args.os_key)
|
||||
|
||||
@mock.patch('glanceclient.v1.client.Client')
|
||||
def test_no_auth_with_token_and_image_url_with_v1(self, v1_client):
|
||||
# test no authentication is required if both token and endpoint url
|
||||
# are specified
|
||||
args = ('--os-image-api-version 1 --os-auth-token mytoken'
|
||||
' --os-image-url https://image:1234/v1 image-list')
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
assert v1_client.called
|
||||
(args, kwargs) = v1_client.call_args
|
||||
self.assertEqual('mytoken', kwargs['token'])
|
||||
self.assertEqual('https://image:1234', args[0])
|
||||
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
def test_no_auth_with_token_and_image_url_with_v2(self, v2_client):
|
||||
# test no authentication is required if both token and endpoint url
|
||||
# are specified
|
||||
args = ('--os-image-api-version 2 --os-auth-token mytoken '
|
||||
'--os-image-url https://image:1234 image-list')
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
self.assertTrue(v2_client.called)
|
||||
(args, kwargs) = v2_client.call_args
|
||||
self.assertEqual('mytoken', kwargs['token'])
|
||||
self.assertEqual('https://image:1234', args[0])
|
||||
|
||||
def _assert_auth_plugin_args(self):
|
||||
# make sure our auth plugin is invoked with the correct args
|
||||
self.assertFalse(self.v3_auth.called)
|
||||
|
||||
body = json.loads(self.v2_auth.last_request.body)
|
||||
|
||||
self.assertEqual(self.auth_env['OS_TENANT_NAME'],
|
||||
body['auth']['tenantName'])
|
||||
self.assertEqual(self.auth_env['OS_USERNAME'],
|
||||
body['auth']['passwordCredentials']['username'])
|
||||
self.assertEqual(self.auth_env['OS_PASSWORD'],
|
||||
body['auth']['passwordCredentials']['password'])
|
||||
|
||||
@mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas',
|
||||
return_value=False)
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
def test_auth_plugin_invocation_without_version(self,
|
||||
v2_client,
|
||||
cache_schemas):
|
||||
|
||||
cli2 = mock.MagicMock()
|
||||
v2_client.return_value = cli2
|
||||
cli2.http_client.get.return_value = (None, {'versions':
|
||||
[{'id': 'v2'}]})
|
||||
|
||||
args = 'image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
# NOTE(flaper87): this currently calls auth twice since it'll
|
||||
# authenticate to get the version list *and* to execute the command.
|
||||
# This is not the ideal behavior and it should be fixed in a follow
|
||||
# up patch.
|
||||
|
||||
@mock.patch('glanceclient.v1.client.Client')
|
||||
def test_auth_plugin_invocation_with_v1(self, v1_client):
|
||||
args = '--os-image-api-version 1 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
self.assertEqual(0, self.v2_auth.call_count)
|
||||
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
def test_auth_plugin_invocation_with_v2(self,
|
||||
v2_client):
|
||||
args = '--os-image-api-version 2 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
self.assertEqual(0, self.v2_auth.call_count)
|
||||
|
||||
@mock.patch('glanceclient.v1.client.Client')
|
||||
def test_auth_plugin_invocation_with_unversioned_auth_url_with_v1(
|
||||
self, v1_client):
|
||||
args = ('--os-image-api-version 1 --os-auth-url %s image-list' %
|
||||
DEFAULT_UNVERSIONED_AUTH_URL)
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
@mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas',
|
||||
return_value=False)
|
||||
def test_auth_plugin_invocation_with_unversioned_auth_url_with_v2(
|
||||
self, v2_client, cache_schemas):
|
||||
args = ('--os-auth-url %s --os-image-api-version 2 '
|
||||
'image-list') % DEFAULT_UNVERSIONED_AUTH_URL
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
|
||||
@mock.patch('glanceclient.Client')
|
||||
def test_endpoint_token_no_auth_req(self, mock_client):
|
||||
|
||||
def verify_input(version=None, endpoint=None, *args, **kwargs):
|
||||
self.assertIn('token', kwargs)
|
||||
self.assertEqual(TOKEN_ID, kwargs['token'])
|
||||
self.assertEqual(DEFAULT_IMAGE_URL, endpoint)
|
||||
return mock.MagicMock()
|
||||
|
||||
mock_client.side_effect = verify_input
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
args = ['--os-image-api-version', '2',
|
||||
'--os-auth-token', TOKEN_ID,
|
||||
'--os-image-url', DEFAULT_IMAGE_URL,
|
||||
'image-list']
|
||||
|
||||
glance_shell.main(args)
|
||||
self.assertEqual(1, mock_client.call_count)
|
||||
|
||||
@mock.patch('sys.stdin', side_effect=mock.MagicMock)
|
||||
@mock.patch('getpass.getpass', side_effect=EOFError)
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
def test_password_prompted_ctrlD_with_v2(self, v2_client,
|
||||
mock_getpass, mock_stdin):
|
||||
cli2 = mock.MagicMock()
|
||||
v2_client.return_value = cli2
|
||||
cli2.http_client.get.return_value = (None, {'versions': []})
|
||||
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
self.make_env(exclude='OS_PASSWORD')
|
||||
# We should get Command Error because we mock Ctl-D.
|
||||
self.assertRaises(exc.CommandError, glance_shell.main, ['image-list'])
|
||||
# Make sure we are actually prompted.
|
||||
mock_getpass.assert_called_with('OS Password: ')
|
||||
|
||||
@mock.patch(
|
||||
'glanceclient.shell.OpenStackImagesShell._get_keystone_auth_plugin')
|
||||
@mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas',
|
||||
return_value=False)
|
||||
def test_no_auth_with_proj_name(self, cache_schemas, session):
|
||||
with mock.patch('glanceclient.v2.client.Client'):
|
||||
args = ('--os-project-name myname '
|
||||
'--os-project-domain-name mydomain '
|
||||
'--os-project-domain-id myid '
|
||||
'--os-image-api-version 2 image-list')
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
((args), kwargs) = session.call_args
|
||||
self.assertEqual('myname', kwargs['project_name'])
|
||||
self.assertEqual('mydomain', kwargs['project_domain_name'])
|
||||
self.assertEqual('myid', kwargs['project_domain_id'])
|
||||
|
||||
@mock.patch.object(openstack_shell.OpenStackImagesShell, 'main')
|
||||
def test_shell_keyboard_interrupt(self, mock_glance_shell):
|
||||
# Ensure that exit code is 130 for KeyboardInterrupt
|
||||
try:
|
||||
mock_glance_shell.side_effect = KeyboardInterrupt()
|
||||
openstack_shell.main()
|
||||
except SystemExit as ex:
|
||||
self.assertEqual(130, ex.code)
|
||||
|
||||
@mock.patch('glanceclient.common.utils.exit', side_effect=utils.exit)
|
||||
def test_shell_illegal_version(self, mock_exit):
|
||||
# Only int versions are allowed on cli
|
||||
shell = openstack_shell.OpenStackImagesShell()
|
||||
argstr = '--os-image-api-version 1.1 image-list'
|
||||
try:
|
||||
shell.main(argstr.split())
|
||||
except SystemExit as ex:
|
||||
self.assertEqual(1, ex.code)
|
||||
msg = ("Invalid API version parameter. "
|
||||
"Supported values are %s" % openstack_shell.SUPPORTED_VERSIONS)
|
||||
mock_exit.assert_called_with(msg=msg)
|
||||
|
||||
@mock.patch('glanceclient.common.utils.exit', side_effect=utils.exit)
|
||||
def test_shell_unsupported_version(self, mock_exit):
|
||||
# Test an integer version which is not supported (-1)
|
||||
shell = openstack_shell.OpenStackImagesShell()
|
||||
argstr = '--os-image-api-version -1 image-list'
|
||||
try:
|
||||
shell.main(argstr.split())
|
||||
except SystemExit as ex:
|
||||
self.assertEqual(1, ex.code)
|
||||
msg = ("Invalid API version parameter. "
|
||||
"Supported values are %s" % openstack_shell.SUPPORTED_VERSIONS)
|
||||
mock_exit.assert_called_with(msg=msg)
|
||||
|
||||
@mock.patch.object(openstack_shell.OpenStackImagesShell,
|
||||
'get_subcommand_parser')
|
||||
def test_shell_import_error_with_mesage(self, mock_parser):
|
||||
msg = 'Unable to import module xxx'
|
||||
mock_parser.side_effect = ImportError('%s' % msg)
|
||||
shell = openstack_shell.OpenStackImagesShell()
|
||||
argstr = '--os-image-api-version 2 image-list'
|
||||
try:
|
||||
shell.main(argstr.split())
|
||||
self.fail('No import error returned')
|
||||
except ImportError as e:
|
||||
self.assertEqual(msg, str(e))
|
||||
|
||||
@mock.patch.object(openstack_shell.OpenStackImagesShell,
|
||||
'get_subcommand_parser')
|
||||
def test_shell_import_error_default_message(self, mock_parser):
|
||||
mock_parser.side_effect = ImportError
|
||||
shell = openstack_shell.OpenStackImagesShell()
|
||||
argstr = '--os-image-api-version 2 image-list'
|
||||
try:
|
||||
shell.main(argstr.split())
|
||||
self.fail('No import error returned')
|
||||
except ImportError as e:
|
||||
msg = 'Unable to import module. Re-run with --debug for more info.'
|
||||
self.assertEqual(msg, str(e))
|
||||
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
@mock.patch('glanceclient.v1.images.ImageManager.list')
|
||||
def test_shell_v1_fallback_from_v2(self, v1_imgs, v2_client):
|
||||
self.make_env()
|
||||
cli2 = mock.MagicMock()
|
||||
v2_client.return_value = cli2
|
||||
cli2.http_client.get.return_value = (None, {'versions': []})
|
||||
args = 'image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
self.assertFalse(cli2.schemas.get.called)
|
||||
self.assertTrue(v1_imgs.called)
|
||||
|
||||
@mock.patch.object(openstack_shell.OpenStackImagesShell,
|
||||
'_cache_schemas')
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
def test_shell_no_fallback_from_v2(self, v2_client, cache_schemas):
|
||||
self.make_env()
|
||||
cli2 = mock.MagicMock()
|
||||
v2_client.return_value = cli2
|
||||
cli2.http_client.get.return_value = (None,
|
||||
{'versions': [{'id': 'v2'}]})
|
||||
cache_schemas.return_value = False
|
||||
args = 'image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
self.assertTrue(cli2.images.list.called)
|
||||
|
||||
@mock.patch('glanceclient.v1.client.Client')
|
||||
def test_auth_plugin_invocation_without_username_with_v1(self, v1_client):
|
||||
self.make_env(exclude='OS_USERNAME')
|
||||
args = '--os-image-api-version 2 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
self.assertRaises(exc.CommandError, glance_shell.main, args.split())
|
||||
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
def test_auth_plugin_invocation_without_username_with_v2(self, v2_client):
|
||||
self.make_env(exclude='OS_USERNAME')
|
||||
args = '--os-image-api-version 2 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
self.assertRaises(exc.CommandError, glance_shell.main, args.split())
|
||||
|
||||
@mock.patch('glanceclient.v1.client.Client')
|
||||
def test_auth_plugin_invocation_without_auth_url_with_v1(self, v1_client):
|
||||
self.make_env(exclude='OS_AUTH_URL')
|
||||
args = '--os-image-api-version 1 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
self.assertRaises(exc.CommandError, glance_shell.main, args.split())
|
||||
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
def test_auth_plugin_invocation_without_auth_url_with_v2(self, v2_client):
|
||||
self.make_env(exclude='OS_AUTH_URL')
|
||||
args = '--os-image-api-version 2 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
self.assertRaises(exc.CommandError, glance_shell.main, args.split())
|
||||
|
||||
@mock.patch('glanceclient.v1.client.Client')
|
||||
def test_auth_plugin_invocation_without_tenant_with_v1(self, v1_client):
|
||||
if 'OS_TENANT_NAME' in os.environ:
|
||||
self.make_env(exclude='OS_TENANT_NAME')
|
||||
if 'OS_PROJECT_ID' in os.environ:
|
||||
self.make_env(exclude='OS_PROJECT_ID')
|
||||
args = '--os-image-api-version 1 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
self.assertRaises(exc.CommandError, glance_shell.main, args.split())
|
||||
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
@mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas',
|
||||
return_value=False)
|
||||
def test_auth_plugin_invocation_without_tenant_with_v2(self, v2_client,
|
||||
cache_schemas):
|
||||
if 'OS_TENANT_NAME' in os.environ:
|
||||
self.make_env(exclude='OS_TENANT_NAME')
|
||||
if 'OS_PROJECT_ID' in os.environ:
|
||||
self.make_env(exclude='OS_PROJECT_ID')
|
||||
args = '--os-image-api-version 2 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
self.assertRaises(exc.CommandError, glance_shell.main, args.split())
|
||||
|
||||
@mock.patch('sys.argv', ['glance'])
|
||||
@mock.patch('sys.stdout', six.StringIO())
|
||||
@mock.patch('sys.stderr', six.StringIO())
|
||||
def test_main_noargs(self):
|
||||
# Ensure that main works with no command-line arguments
|
||||
try:
|
||||
openstack_shell.main()
|
||||
except SystemExit:
|
||||
self.fail('Unexpected SystemExit')
|
||||
|
||||
# We expect the normal v2 usage as a result
|
||||
expected = ['Command-line interface to the OpenStack Images API',
|
||||
'image-list',
|
||||
'image-deactivate',
|
||||
'location-add']
|
||||
for output in expected:
|
||||
self.assertIn(output,
|
||||
sys.stdout.getvalue())
|
||||
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
@mock.patch('glanceclient.v1.shell.do_image_list')
|
||||
@mock.patch('glanceclient.shell.logging.basicConfig')
|
||||
def test_setup_debug(self, conf, func, v2_client):
|
||||
cli2 = mock.MagicMock()
|
||||
v2_client.return_value = cli2
|
||||
cli2.http_client.get.return_value = (None, {'versions': []})
|
||||
args = '--debug image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
glance_logger = logging.getLogger('glanceclient')
|
||||
self.assertEqual(glance_logger.getEffectiveLevel(), logging.DEBUG)
|
||||
conf.assert_called_with(level=logging.DEBUG)
|
||||
|
||||
|
||||
class ShellTestWithKeystoneV3Auth(ShellTest):
|
||||
# auth environment to use
|
||||
auth_env = FAKE_V3_ENV.copy()
|
||||
token_url = DEFAULT_V3_AUTH_URL + '/auth/tokens'
|
||||
|
||||
def _assert_auth_plugin_args(self):
|
||||
self.assertFalse(self.v2_auth.called)
|
||||
|
||||
body = json.loads(self.v3_auth.last_request.body)
|
||||
user = body['auth']['identity']['password']['user']
|
||||
|
||||
self.assertEqual(self.auth_env['OS_USERNAME'], user['name'])
|
||||
self.assertEqual(self.auth_env['OS_PASSWORD'], user['password'])
|
||||
self.assertEqual(self.auth_env['OS_USER_DOMAIN_NAME'],
|
||||
user['domain']['name'])
|
||||
self.assertEqual(self.auth_env['OS_PROJECT_ID'],
|
||||
body['auth']['scope']['project']['id'])
|
||||
|
||||
@mock.patch('glanceclient.v1.client.Client')
|
||||
def test_auth_plugin_invocation_with_v1(self, v1_client):
|
||||
args = '--os-image-api-version 1 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
self.assertEqual(0, self.v3_auth.call_count)
|
||||
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
def test_auth_plugin_invocation_with_v2(self, v2_client):
|
||||
args = '--os-image-api-version 2 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
self.assertEqual(0, self.v3_auth.call_count)
|
||||
|
||||
@mock.patch('keystoneauth1.discover.Discover',
|
||||
side_effect=ks_exc.ClientException())
|
||||
def test_api_discovery_failed_with_unversioned_auth_url(self,
|
||||
discover):
|
||||
args = ('--os-image-api-version 2 --os-auth-url %s image-list'
|
||||
% DEFAULT_UNVERSIONED_AUTH_URL)
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
self.assertRaises(exc.CommandError, glance_shell.main, args.split())
|
||||
|
||||
def test_bash_completion(self):
|
||||
stdout, stderr = self.shell('--os-image-api-version 2 bash_completion')
|
||||
# just check we have some output
|
||||
required = [
|
||||
'--status',
|
||||
'image-create',
|
||||
'help',
|
||||
'--size']
|
||||
for r in required:
|
||||
self.assertIn(r, stdout.split())
|
||||
avoided = [
|
||||
'bash_completion',
|
||||
'bash-completion']
|
||||
for r in avoided:
|
||||
self.assertNotIn(r, stdout.split())
|
||||
|
||||
|
||||
class ShellTestWithNoOSImageURLPublic(ShellTestWithKeystoneV3Auth):
|
||||
# auth environment to use
|
||||
# default uses public
|
||||
auth_env = FAKE_V4_ENV.copy()
|
||||
|
||||
def setUp(self):
|
||||
super(ShellTestWithNoOSImageURLPublic, self).setUp()
|
||||
self.image_url = DEFAULT_IMAGE_URL
|
||||
self.requests.get(DEFAULT_IMAGE_URL + 'v2/images',
|
||||
text='{"images": []}')
|
||||
|
||||
@mock.patch('glanceclient.v1.client.Client')
|
||||
def test_auth_plugin_invocation_with_v1(self, v1_client):
|
||||
args = '--os-image-api-version 1 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
self.assertEqual(1, self.v3_auth.call_count)
|
||||
self._assert_auth_plugin_args()
|
||||
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
def test_auth_plugin_invocation_with_v2(self, v2_client):
|
||||
args = '--os-image-api-version 2 image-list'
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
self.assertEqual(1, self.v3_auth.call_count)
|
||||
self._assert_auth_plugin_args()
|
||||
|
||||
@mock.patch('glanceclient.v2.client.Client')
|
||||
def test_endpoint_from_interface(self, v2_client):
|
||||
args = ('--os-image-api-version 2 image-list')
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
assert v2_client.called
|
||||
(args, kwargs) = v2_client.call_args
|
||||
self.assertEqual(self.image_url, kwargs['endpoint_override'])
|
||||
|
||||
def test_endpoint_real_from_interface(self):
|
||||
args = ('--os-image-api-version 2 image-list')
|
||||
glance_shell = openstack_shell.OpenStackImagesShell()
|
||||
glance_shell.main(args.split())
|
||||
self.assertEqual(self.requests.request_history[2].url,
|
||||
self.image_url + "v2/images?limit=20&"
|
||||
"sort_key=name&sort_dir=asc")
|
||||
|
||||
|
||||
class ShellTestWithNoOSImageURLInternal(ShellTestWithNoOSImageURLPublic):
|
||||
# auth environment to use
|
||||
# this uses internal
|
||||
FAKE_V5_ENV = FAKE_V4_ENV.copy()
|
||||
FAKE_V5_ENV['OS_ENDPOINT_TYPE'] = 'internal'
|
||||
auth_env = FAKE_V5_ENV.copy()
|
||||
|
||||
def setUp(self):
|
||||
super(ShellTestWithNoOSImageURLPublic, self).setUp()
|
||||
self.image_url = DEFAULT_IMAGE_URL_INTERNAL
|
||||
self.requests.get(DEFAULT_IMAGE_URL_INTERNAL + 'v2/images',
|
||||
text='{"images": []}')
|
||||
|
||||
|
||||
class ShellCacheSchemaTest(testutils.TestCase):
|
||||
def setUp(self):
|
||||
super(ShellCacheSchemaTest, self).setUp()
|
||||
self._mock_client_setup()
|
||||
self._mock_shell_setup()
|
||||
self.cache_dir = '/dir_for_cached_schema'
|
||||
self.os_auth_url = 'http://localhost:5000/v2'
|
||||
url_hex = hashlib.sha1(self.os_auth_url.encode('utf-8')).hexdigest()
|
||||
self.prefix_path = (self.cache_dir + '/' + url_hex)
|
||||
self.cache_files = [self.prefix_path + '/image_schema.json',
|
||||
self.prefix_path + '/namespace_schema.json',
|
||||
self.prefix_path + '/resource_type_schema.json']
|
||||
|
||||
def tearDown(self):
|
||||
super(ShellCacheSchemaTest, self).tearDown()
|
||||
|
||||
def _mock_client_setup(self):
|
||||
self.schema_dict = {
|
||||
'name': 'image',
|
||||
'properties': {
|
||||
'name': {'type': 'string', 'description': 'Name of image'},
|
||||
},
|
||||
}
|
||||
|
||||
self.client = mock.Mock()
|
||||
schema_odict = OrderedDict(self.schema_dict)
|
||||
self.client.schemas.get.return_value = schemas.Schema(schema_odict)
|
||||
|
||||
def _mock_shell_setup(self):
|
||||
self.shell = openstack_shell.OpenStackImagesShell()
|
||||
self.shell._get_versioned_client = mock.create_autospec(
|
||||
self.shell._get_versioned_client, return_value=self.client,
|
||||
spec_set=True
|
||||
)
|
||||
|
||||
def _make_args(self, args):
|
||||
class Args(object):
|
||||
def __init__(self, entries):
|
||||
self.__dict__.update(entries)
|
||||
|
||||
return Args(args)
|
||||
|
||||
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
|
||||
@mock.patch('os.path.exists', return_value=True)
|
||||
def test_cache_schemas_gets_when_forced(self, exists_mock):
|
||||
options = {
|
||||
'get_schema': True,
|
||||
'os_auth_url': self.os_auth_url
|
||||
}
|
||||
schema_odict = OrderedDict(self.schema_dict)
|
||||
|
||||
args = self._make_args(options)
|
||||
client = self.shell._get_versioned_client('2', args)
|
||||
self.shell._cache_schemas(args, client, home_dir=self.cache_dir)
|
||||
|
||||
self.assertEqual(12, open.mock_calls.__len__())
|
||||
self.assertEqual(mock.call(self.cache_files[0], 'w'),
|
||||
open.mock_calls[0])
|
||||
self.assertEqual(mock.call(self.cache_files[1], 'w'),
|
||||
open.mock_calls[4])
|
||||
self.assertEqual(mock.call().write(json.dumps(schema_odict)),
|
||||
open.mock_calls[2])
|
||||
self.assertEqual(mock.call().write(json.dumps(schema_odict)),
|
||||
open.mock_calls[6])
|
||||
|
||||
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
|
||||
@mock.patch('os.path.exists', side_effect=[True, False, False, False])
|
||||
def test_cache_schemas_gets_when_not_exists(self, exists_mock):
|
||||
options = {
|
||||
'get_schema': False,
|
||||
'os_auth_url': self.os_auth_url
|
||||
}
|
||||
schema_odict = OrderedDict(self.schema_dict)
|
||||
|
||||
args = self._make_args(options)
|
||||
client = self.shell._get_versioned_client('2', args)
|
||||
self.shell._cache_schemas(args, client, home_dir=self.cache_dir)
|
||||
|
||||
self.assertEqual(12, open.mock_calls.__len__())
|
||||
self.assertEqual(mock.call(self.cache_files[0], 'w'),
|
||||
open.mock_calls[0])
|
||||
self.assertEqual(mock.call(self.cache_files[1], 'w'),
|
||||
open.mock_calls[4])
|
||||
self.assertEqual(mock.call().write(json.dumps(schema_odict)),
|
||||
open.mock_calls[2])
|
||||
self.assertEqual(mock.call().write(json.dumps(schema_odict)),
|
||||
open.mock_calls[6])
|
||||
|
||||
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
|
||||
@mock.patch('os.path.exists', return_value=True)
|
||||
def test_cache_schemas_leaves_when_present_not_forced(self, exists_mock):
|
||||
options = {
|
||||
'get_schema': False,
|
||||
'os_auth_url': self.os_auth_url
|
||||
}
|
||||
|
||||
client = mock.MagicMock()
|
||||
self.shell._cache_schemas(self._make_args(options),
|
||||
client, home_dir=self.cache_dir)
|
||||
|
||||
exists_mock.assert_has_calls([
|
||||
mock.call(self.prefix_path),
|
||||
mock.call(self.cache_files[0]),
|
||||
mock.call(self.cache_files[1]),
|
||||
mock.call(self.cache_files[2])
|
||||
])
|
||||
self.assertEqual(4, exists_mock.call_count)
|
||||
self.assertEqual(0, open.mock_calls.__len__())
|
||||
|
||||
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
|
||||
@mock.patch('os.path.exists', return_value=True)
|
||||
def test_cache_schemas_leaves_auto_switch(self, exists_mock):
|
||||
options = {
|
||||
'get_schema': True,
|
||||
'os_auth_url': self.os_auth_url
|
||||
}
|
||||
|
||||
self.client.schemas.get.return_value = Exception()
|
||||
|
||||
client = mock.MagicMock()
|
||||
switch_version = self.shell._cache_schemas(self._make_args(options),
|
||||
client,
|
||||
home_dir=self.cache_dir)
|
||||
self.assertEqual(True, switch_version)
|
||||
|
||||
|
||||
class ShellTestRequests(testutils.TestCase):
|
||||
"""Shell tests using the requests mock library."""
|
||||
def _make_args(self, args):
|
||||
# NOTE(venkatesh): this conversion from a dict to an object
|
||||
# is required because the test_shell.do_xxx(gc, args) methods
|
||||
# expects the args to be attributes of an object. If passed as
|
||||
# dict directly, it throws an AttributeError.
|
||||
class Args(object):
|
||||
def __init__(self, entries):
|
||||
self.__dict__.update(entries)
|
||||
|
||||
return Args(args)
|
||||
|
||||
def setUp(self):
|
||||
super(ShellTestRequests, self).setUp()
|
||||
self._old_env = os.environ
|
||||
os.environ = {}
|
||||
|
||||
def tearDown(self):
|
||||
super(ShellTestRequests, self).tearDown()
|
||||
os.environ = self._old_env
|
||||
|
||||
def test_download_has_no_stray_output_to_stdout(self):
|
||||
"""Regression test for bug 1488914"""
|
||||
saved_stdout = sys.stdout
|
||||
try:
|
||||
sys.stdout = output = testutils.FakeNoTTYStdout()
|
||||
id = image_show_fixture['id']
|
||||
self.requests = self.useFixture(rm_fixture.Fixture())
|
||||
self.requests.get('http://example.com/versions',
|
||||
json=image_versions_fixture)
|
||||
|
||||
headers = {'Content-Length': '4',
|
||||
'Content-type': 'application/octet-stream'}
|
||||
fake = testutils.FakeResponse(headers, six.StringIO('DATA'))
|
||||
self.requests.get('http://example.com/v1/images/%s' % id,
|
||||
raw=fake)
|
||||
|
||||
self.requests.get('http://example.com/v1/images/detail'
|
||||
'?sort_key=name&sort_dir=asc&limit=20')
|
||||
|
||||
headers = {'X-Image-Meta-Id': id}
|
||||
self.requests.head('http://example.com/v1/images/%s' % id,
|
||||
headers=headers)
|
||||
|
||||
with mock.patch.object(openstack_shell.OpenStackImagesShell,
|
||||
'_cache_schemas') as mocked_cache_schema:
|
||||
mocked_cache_schema.return_value = True
|
||||
shell = openstack_shell.OpenStackImagesShell()
|
||||
argstr = ('--os-auth-token faketoken '
|
||||
'--os-image-url http://example.com '
|
||||
'image-download %s' % id)
|
||||
shell.main(argstr.split())
|
||||
self.assertTrue(mocked_cache_schema.called)
|
||||
# Ensure we have *only* image data
|
||||
self.assertEqual('DATA', output.getvalue())
|
||||
finally:
|
||||
sys.stdout = saved_stdout
|
||||
|
||||
def test_v1_download_has_no_stray_output_to_stdout(self):
|
||||
"""Ensure no stray print statements corrupt the image"""
|
||||
saved_stdout = sys.stdout
|
||||
try:
|
||||
sys.stdout = output = testutils.FakeNoTTYStdout()
|
||||
id = image_show_fixture['id']
|
||||
|
||||
self.requests = self.useFixture(rm_fixture.Fixture())
|
||||
headers = {'X-Image-Meta-Id': id}
|
||||
self.requests.head('http://example.com/v1/images/%s' % id,
|
||||
headers=headers)
|
||||
|
||||
headers = {'Content-Length': '4',
|
||||
'Content-type': 'application/octet-stream'}
|
||||
fake = testutils.FakeResponse(headers, six.StringIO('DATA'))
|
||||
self.requests.get('http://example.com/v1/images/%s' % id,
|
||||
headers=headers, raw=fake)
|
||||
|
||||
shell = openstack_shell.OpenStackImagesShell()
|
||||
argstr = ('--os-image-api-version 1 --os-auth-token faketoken '
|
||||
'--os-image-url http://example.com '
|
||||
'image-download %s' % id)
|
||||
shell.main(argstr.split())
|
||||
# Ensure we have *only* image data
|
||||
self.assertEqual('DATA', output.getvalue())
|
||||
finally:
|
||||
sys.stdout = saved_stdout
|
||||
|
||||
def test_v2_download_has_no_stray_output_to_stdout(self):
|
||||
"""Ensure no stray print statements corrupt the image"""
|
||||
saved_stdout = sys.stdout
|
||||
try:
|
||||
sys.stdout = output = testutils.FakeNoTTYStdout()
|
||||
id = image_show_fixture['id']
|
||||
headers = {'Content-Length': '4',
|
||||
'Content-type': 'application/octet-stream'}
|
||||
fake = testutils.FakeResponse(headers, six.StringIO('DATA'))
|
||||
|
||||
self.requests = self.useFixture(rm_fixture.Fixture())
|
||||
self.requests.get('http://example.com/v2/images/%s/file' % id,
|
||||
headers=headers, raw=fake)
|
||||
|
||||
shell = openstack_shell.OpenStackImagesShell()
|
||||
argstr = ('--os-image-api-version 2 --os-auth-token faketoken '
|
||||
'--os-image-url http://example.com '
|
||||
'image-download %s' % id)
|
||||
shell.main(argstr.split())
|
||||
# Ensure we have *only* image data
|
||||
self.assertEqual('DATA', output.getvalue())
|
||||
finally:
|
||||
sys.stdout = saved_stdout
|
@ -1,240 +0,0 @@
|
||||
# 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 glanceclient import Client
|
||||
from glanceclient import exc
|
||||
from glanceclient import v1
|
||||
from glanceclient import v2
|
||||
|
||||
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 glance
|
||||
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.images.get('image123')
|
||||
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')
|
||||
|
||||
@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.images.get('image123')
|
||||
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')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v2_requests_cert_verification(self, __):
|
||||
"""v2 regression test for bug 115260."""
|
||||
port = self.port
|
||||
url = 'https://0.0.0.0:%d' % port
|
||||
|
||||
try:
|
||||
gc = v2.Client(url,
|
||||
insecure=False,
|
||||
ssl_compression=True)
|
||||
gc.images.get('image123')
|
||||
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')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v2_requests_cert_verification_no_compression(self, __):
|
||||
"""v2 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:
|
||||
gc = v2.Client(url,
|
||||
insecure=False,
|
||||
ssl_compression=False)
|
||||
gc.images.get('image123')
|
||||
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')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v2_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('2', url,
|
||||
insecure=False,
|
||||
ssl_compression=True,
|
||||
cacert=cacert)
|
||||
gc.images.get('image123')
|
||||
except exc.CommunicationError as e:
|
||||
if 'certificate verify failed' in e.message:
|
||||
self.fail('Certificate failure message is received')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v2_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('2', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cacert=cacert)
|
||||
gc.images.get('image123')
|
||||
except exc.CommunicationError as e:
|
||||
if 'certificate verify failed' in e.message:
|
||||
self.fail('Certificate failure message is received')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v2_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('2', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cert_file=cert_file,
|
||||
cacert=cacert)
|
||||
gc.images.get('image123')
|
||||
except exc.CommunicationError as e:
|
||||
if ('PEM lib' not in e.message):
|
||||
self.fail('No appropriate failure message is received')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v2_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('2', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cert_file=cert_file,
|
||||
cacert=cacert)
|
||||
gc.images.get('image123')
|
||||
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')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_v2_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('2', url,
|
||||
insecure=False,
|
||||
ssl_compression=False,
|
||||
cacert=cacert)
|
||||
gc.images.get('image123')
|
||||
except exc.CommunicationError as e:
|
||||
if 'invalid path' not in e.message:
|
||||
raise
|
@ -1,230 +0,0 @@
|
||||
# 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 sys
|
||||
|
||||
import mock
|
||||
from oslo_utils import encodeutils
|
||||
from requests import Response
|
||||
import six
|
||||
# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
|
||||
from six.moves import range
|
||||
import testtools
|
||||
|
||||
from glanceclient.common import utils
|
||||
|
||||
|
||||
REQUEST_ID = 'req-1234'
|
||||
|
||||
|
||||
def create_response_obj_with_req_id(req_id):
|
||||
resp = Response()
|
||||
resp.headers['x-openstack-request-id'] = req_id
|
||||
return resp
|
||||
|
||||
|
||||
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_prettytable(self):
|
||||
class Struct(object):
|
||||
def __init__(self, **entries):
|
||||
self.__dict__.update(entries)
|
||||
|
||||
# test that the prettytable output is wellformatted (left-aligned)
|
||||
columns = ['ID', 'Name']
|
||||
val = ['Name1', 'another', 'veeeery long']
|
||||
images = [Struct(**{'id': i ** 16, 'name': val[i]})
|
||||
for i in range(len(val))]
|
||||
|
||||
saved_stdout = sys.stdout
|
||||
try:
|
||||
sys.stdout = output_list = six.StringIO()
|
||||
utils.print_list(images, columns)
|
||||
|
||||
sys.stdout = output_dict = six.StringIO()
|
||||
utils.print_dict({'K': 'k', 'Key': 'veeeeeeeeeeeeeeeeeeeeeeee'
|
||||
'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
|
||||
'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
|
||||
'eeeeeeeeeeeery long value'},
|
||||
max_column_width=60)
|
||||
|
||||
finally:
|
||||
sys.stdout = saved_stdout
|
||||
|
||||
self.assertEqual('''\
|
||||
+-------+--------------+
|
||||
| ID | Name |
|
||||
+-------+--------------+
|
||||
| | Name1 |
|
||||
| 1 | another |
|
||||
| 65536 | veeeery long |
|
||||
+-------+--------------+
|
||||
''',
|
||||
output_list.getvalue())
|
||||
|
||||
self.assertEqual('''\
|
||||
+----------+--------------------------------------------------------------+
|
||||
| Property | Value |
|
||||
+----------+--------------------------------------------------------------+
|
||||
| K | k |
|
||||
| Key | veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee |
|
||||
| | eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee |
|
||||
| | ery long value |
|
||||
+----------+--------------------------------------------------------------+
|
||||
''',
|
||||
output_dict.getvalue())
|
||||
|
||||
def test_schema_args_with_list_types(self):
|
||||
# NOTE(flaper87): Regression for bug
|
||||
# https://bugs.launchpad.net/python-glanceclient/+bug/1401032
|
||||
|
||||
def schema_getter(_type='string', enum=False):
|
||||
prop = {
|
||||
'type': ['null', _type],
|
||||
'readOnly': True,
|
||||
'description': 'Test schema',
|
||||
}
|
||||
|
||||
if enum:
|
||||
prop['enum'] = [None, 'opt-1', 'opt-2']
|
||||
|
||||
def actual_getter():
|
||||
return {
|
||||
'additionalProperties': False,
|
||||
'required': ['name'],
|
||||
'name': 'test_schema',
|
||||
'properties': {
|
||||
'test': prop,
|
||||
}
|
||||
}
|
||||
|
||||
return actual_getter
|
||||
|
||||
def dummy_func():
|
||||
pass
|
||||
|
||||
decorated = utils.schema_args(schema_getter())(dummy_func)
|
||||
arg, opts = decorated.__dict__['arguments'][0]
|
||||
self.assertIn('--test', arg)
|
||||
self.assertEqual(encodeutils.safe_decode, opts['type'])
|
||||
|
||||
decorated = utils.schema_args(schema_getter('integer'))(dummy_func)
|
||||
arg, opts = decorated.__dict__['arguments'][0]
|
||||
self.assertIn('--test', arg)
|
||||
self.assertEqual(int, opts['type'])
|
||||
|
||||
decorated = utils.schema_args(schema_getter(enum=True))(dummy_func)
|
||||
arg, opts = decorated.__dict__['arguments'][0]
|
||||
self.assertIn('--test', arg)
|
||||
self.assertEqual(encodeutils.safe_decode, opts['type'])
|
||||
self.assertIn('None, opt-1, opt-2', opts['help'])
|
||||
|
||||
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)
|
||||
|
||||
def test_generator_proxy(self):
|
||||
def _test_decorator():
|
||||
i = 1
|
||||
resp = create_response_obj_with_req_id(REQUEST_ID)
|
||||
while True:
|
||||
yield i, resp
|
||||
i += 1
|
||||
|
||||
gen_obj = _test_decorator()
|
||||
proxy = utils.GeneratorProxy(gen_obj)
|
||||
|
||||
# Proxy object should succeed in behaving as the
|
||||
# wrapped object
|
||||
self.assertIsInstance(proxy, type(gen_obj))
|
||||
|
||||
# Initially request_ids should be empty
|
||||
self.assertEqual([], proxy.request_ids)
|
||||
|
||||
# Only after we have started iterating we should
|
||||
# see non-empty `request_ids` property
|
||||
proxy.next()
|
||||
self.assertEqual([REQUEST_ID], proxy.request_ids)
|
||||
|
||||
# Even after multiple iterations `request_ids` property
|
||||
# should only contain one request id
|
||||
proxy.next()
|
||||
proxy.next()
|
||||
self.assertEqual(1, len(proxy.request_ids))
|
||||
|
||||
def test_request_id_proxy(self):
|
||||
def test_data(val):
|
||||
resp = create_response_obj_with_req_id(REQUEST_ID)
|
||||
return val, resp
|
||||
|
||||
# Object of any type except decorator can be passed to test_data
|
||||
proxy = utils.RequestIdProxy(test_data(11))
|
||||
# Verify that proxy object has a property `request_ids` and it is
|
||||
# a list of one request id
|
||||
self.assertEqual([REQUEST_ID], proxy.request_ids)
|
@ -1,125 +0,0 @@
|
||||
# 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 testtools
|
||||
|
||||
from glanceclient.tests import utils
|
||||
import glanceclient.v1.image_members
|
||||
import glanceclient.v1.images
|
||||
|
||||
|
||||
fixtures = {
|
||||
'/v1/images/1/members': {
|
||||
'GET': (
|
||||
{},
|
||||
{'members': [
|
||||
{'member_id': '1', 'can_share': False},
|
||||
]},
|
||||
),
|
||||
'PUT': ({}, None),
|
||||
},
|
||||
'/v1/images/1/members/1': {
|
||||
'GET': (
|
||||
{},
|
||||
{'member': {
|
||||
'member_id': '1',
|
||||
'can_share': False,
|
||||
}},
|
||||
),
|
||||
'PUT': ({}, None),
|
||||
'DELETE': ({}, None),
|
||||
},
|
||||
'/v1/shared-images/1': {
|
||||
'GET': (
|
||||
{},
|
||||
{'shared_images': [
|
||||
{'image_id': '1', 'can_share': False},
|
||||
]},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ImageMemberManagerTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ImageMemberManagerTest, self).setUp()
|
||||
self.api = utils.FakeAPI(fixtures)
|
||||
self.mgr = glanceclient.v1.image_members.ImageMemberManager(self.api)
|
||||
self.image = glanceclient.v1.images.Image(self.api, {'id': '1'}, True)
|
||||
|
||||
def test_list_by_image(self):
|
||||
members = self.mgr.list(image=self.image)
|
||||
expect = [('GET', '/v1/images/1/members', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual(1, len(members))
|
||||
self.assertEqual('1', members[0].member_id)
|
||||
self.assertEqual('1', members[0].image_id)
|
||||
self.assertEqual(False, members[0].can_share)
|
||||
|
||||
def test_list_by_member(self):
|
||||
resource_class = glanceclient.v1.image_members.ImageMember
|
||||
member = resource_class(self.api, {'member_id': '1'}, True)
|
||||
self.mgr.list(member=member)
|
||||
expect = [('GET', '/v1/shared-images/1', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_get(self):
|
||||
member = self.mgr.get(self.image, '1')
|
||||
expect = [('GET', '/v1/images/1/members/1', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('1', member.member_id)
|
||||
self.assertEqual('1', member.image_id)
|
||||
self.assertEqual(False, member.can_share)
|
||||
|
||||
def test_delete(self):
|
||||
self.mgr.delete('1', '1')
|
||||
expect = [('DELETE', '/v1/images/1/members/1', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_create(self):
|
||||
self.mgr.create(self.image, '1', can_share=True)
|
||||
expect_body = {'member': {'can_share': True}}
|
||||
expect = [('PUT', '/v1/images/1/members/1', {},
|
||||
sorted(expect_body.items()))]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_replace(self):
|
||||
body = [
|
||||
{'member_id': '2', 'can_share': False},
|
||||
{'member_id': '3'},
|
||||
]
|
||||
self.mgr.replace(self.image, body)
|
||||
expect = [('PUT', '/v1/images/1/members', {},
|
||||
sorted({'memberships': body}.items()))]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_replace_objects(self):
|
||||
body = [
|
||||
glanceclient.v1.image_members.ImageMember(
|
||||
self.mgr, {'member_id': '2', 'can_share': False}, True),
|
||||
glanceclient.v1.image_members.ImageMember(
|
||||
self.mgr, {'member_id': '3', 'can_share': True}, True),
|
||||
]
|
||||
self.mgr.replace(self.image, body)
|
||||
expect_body = {
|
||||
'memberships': [
|
||||
{'member_id': '2', 'can_share': False},
|
||||
{'member_id': '3', 'can_share': True},
|
||||
],
|
||||
}
|
||||
expect = [('PUT', '/v1/images/1/members', {},
|
||||
sorted(expect_body.items()))]
|
||||
self.assertEqual(expect, self.api.calls)
|
@ -1,963 +0,0 @@
|
||||
# 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 errno
|
||||
import json
|
||||
import testtools
|
||||
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from glanceclient.tests import utils
|
||||
from glanceclient.v1 import client
|
||||
from glanceclient.v1 import images
|
||||
from glanceclient.v1 import shell
|
||||
|
||||
|
||||
fixtures = {
|
||||
'/v1/images': {
|
||||
'POST': (
|
||||
{
|
||||
'location': '/v1/images/1',
|
||||
'x-openstack-request-id': 'req-1234',
|
||||
},
|
||||
json.dumps(
|
||||
{'image': {
|
||||
'id': '1',
|
||||
'name': 'image-1',
|
||||
'container_format': 'ovf',
|
||||
'disk_format': 'vhd',
|
||||
'owner': 'asdf',
|
||||
'size': '1024',
|
||||
'min_ram': '512',
|
||||
'min_disk': '10',
|
||||
'properties': {'a': 'b', 'c': 'd'},
|
||||
'is_public': False,
|
||||
'protected': False,
|
||||
'deleted': False,
|
||||
}},
|
||||
),
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?limit=20': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'a',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'b',
|
||||
'name': 'image-2',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?is_public=None&limit=20': {
|
||||
'GET': (
|
||||
{'x-openstack-request-id': 'req-1234'},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'a',
|
||||
'owner': 'A',
|
||||
'is_public': 'True',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'b',
|
||||
'owner': 'B',
|
||||
'is_public': 'False',
|
||||
'name': 'image-2',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'c',
|
||||
'is_public': 'False',
|
||||
'name': 'image-3',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?is_public=None&limit=5': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'a',
|
||||
'owner': 'A',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'b',
|
||||
'owner': 'B',
|
||||
'name': 'image-2',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'b2',
|
||||
'owner': 'B',
|
||||
'name': 'image-3',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'c',
|
||||
'name': 'image-3',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?limit=5': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'a',
|
||||
'owner': 'A',
|
||||
'is_public': 'False',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'b',
|
||||
'owner': 'A',
|
||||
'is_public': 'False',
|
||||
'name': 'image-2',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'b2',
|
||||
'owner': 'B',
|
||||
'name': 'image-3',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'c',
|
||||
'is_public': 'True',
|
||||
'name': 'image-3',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?limit=20&marker=a': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'b',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'c',
|
||||
'name': 'image-2',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?limit=1': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'a',
|
||||
'name': 'image-0',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?limit=1&marker=a': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'b',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?limit=2': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'a',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'b',
|
||||
'name': 'image-2',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?limit=2&marker=b': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'c',
|
||||
'name': 'image-3',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?limit=20&name=foo': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'a',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'b',
|
||||
'name': 'image-2',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?limit=20&property-ping=pong':
|
||||
{
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': '1',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?limit=20&sort_dir=desc': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'a',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'b',
|
||||
'name': 'image-2',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/detail?limit=20&sort_key=name': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': 'a',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'b',
|
||||
'name': 'image-2',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
|
||||
'/v1/images/1': {
|
||||
'HEAD': (
|
||||
{
|
||||
'x-image-meta-id': '1',
|
||||
'x-image-meta-name': 'image-1',
|
||||
'x-image-meta-property-arch': 'x86_64',
|
||||
'x-image-meta-is_public': 'false',
|
||||
'x-image-meta-protected': 'false',
|
||||
'x-image-meta-deleted': 'false',
|
||||
},
|
||||
None),
|
||||
'GET': (
|
||||
{},
|
||||
'XXX',
|
||||
),
|
||||
'PUT': (
|
||||
{},
|
||||
json.dumps(
|
||||
{'image': {
|
||||
'id': '1',
|
||||
'name': 'image-2',
|
||||
'container_format': 'ovf',
|
||||
'disk_format': 'vhd',
|
||||
'owner': 'asdf',
|
||||
'size': '1024',
|
||||
'min_ram': '512',
|
||||
'min_disk': '10',
|
||||
'properties': {'a': 'b', 'c': 'd'},
|
||||
'is_public': False,
|
||||
'protected': False,
|
||||
}},
|
||||
),
|
||||
),
|
||||
'DELETE': ({}, None),
|
||||
},
|
||||
'/v1/images/2': {
|
||||
'HEAD': (
|
||||
{
|
||||
'x-image-meta-id': '2'
|
||||
},
|
||||
None,
|
||||
),
|
||||
'GET': (
|
||||
{
|
||||
'x-image-meta-checksum': 'wrong'
|
||||
},
|
||||
'YYY',
|
||||
),
|
||||
},
|
||||
'/v1/images/3': {
|
||||
'HEAD': (
|
||||
{
|
||||
'x-image-meta-id': '3',
|
||||
'x-image-meta-name': u"ni\xf1o"
|
||||
},
|
||||
None,
|
||||
),
|
||||
'GET': (
|
||||
{
|
||||
'x-image-meta-checksum': '0745064918b49693cca64d6b6a13d28a'
|
||||
},
|
||||
'ZZZ',
|
||||
),
|
||||
},
|
||||
'/v1/images/4': {
|
||||
'HEAD': (
|
||||
{
|
||||
'x-image-meta-id': '4',
|
||||
'x-image-meta-name': 'image-4',
|
||||
'x-image-meta-property-arch': 'x86_64',
|
||||
'x-image-meta-is_public': 'false',
|
||||
'x-image-meta-protected': 'false',
|
||||
'x-image-meta-deleted': 'false',
|
||||
'x-openstack-request-id': 'req-1234',
|
||||
},
|
||||
None),
|
||||
'GET': (
|
||||
{
|
||||
'x-openstack-request-id': 'req-1234',
|
||||
},
|
||||
'XXX',
|
||||
),
|
||||
'PUT': (
|
||||
{
|
||||
'x-openstack-request-id': 'req-1234',
|
||||
},
|
||||
json.dumps(
|
||||
{'image': {
|
||||
'id': '4',
|
||||
'name': 'image-4',
|
||||
'container_format': 'ovf',
|
||||
'disk_format': 'vhd',
|
||||
'owner': 'asdf',
|
||||
'size': '1024',
|
||||
'min_ram': '512',
|
||||
'min_disk': '10',
|
||||
'properties': {'a': 'b', 'c': 'd'},
|
||||
'is_public': False,
|
||||
'protected': False,
|
||||
}},
|
||||
),
|
||||
),
|
||||
'DELETE': (
|
||||
{
|
||||
'x-openstack-request-id': 'req-1234',
|
||||
},
|
||||
None),
|
||||
},
|
||||
'/v1/images/v2_created_img': {
|
||||
'PUT': (
|
||||
{},
|
||||
json.dumps({
|
||||
"image": {
|
||||
"status": "queued",
|
||||
"deleted": False,
|
||||
"container_format": "bare",
|
||||
"min_ram": 0,
|
||||
"updated_at": "2013-12-20T01:51:45",
|
||||
"owner": "foo",
|
||||
"min_disk": 0,
|
||||
"is_public": False,
|
||||
"deleted_at": None,
|
||||
"id": "v2_created_img",
|
||||
"size": None,
|
||||
"name": "bar",
|
||||
"checksum": None,
|
||||
"created_at": "2013-12-20T01:50:38",
|
||||
"disk_format": "qcow2",
|
||||
"properties": {},
|
||||
"protected": False
|
||||
}
|
||||
})
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ImageManagerTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ImageManagerTest, self).setUp()
|
||||
self.api = utils.FakeAPI(fixtures)
|
||||
self.mgr = images.ImageManager(self.api)
|
||||
|
||||
def test_paginated_list(self):
|
||||
images = list(self.mgr.list(page_size=2))
|
||||
expect = [
|
||||
('GET', '/v1/images/detail?limit=2', {}, None),
|
||||
('GET', '/v1/images/detail?limit=2&marker=b', {}, None),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual(3, len(images))
|
||||
self.assertEqual('a', images[0].id)
|
||||
self.assertEqual('b', images[1].id)
|
||||
self.assertEqual('c', images[2].id)
|
||||
|
||||
def test_list_with_limit_less_than_page_size(self):
|
||||
results = list(self.mgr.list(page_size=2, limit=1))
|
||||
expect = [('GET', '/v1/images/detail?limit=2', {}, None)]
|
||||
self.assertEqual(1, len(results))
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_list_with_limit_greater_than_page_size(self):
|
||||
images = list(self.mgr.list(page_size=1, limit=2))
|
||||
expect = [
|
||||
('GET', '/v1/images/detail?limit=1', {}, None),
|
||||
('GET', '/v1/images/detail?limit=1&marker=a', {}, None),
|
||||
]
|
||||
self.assertEqual(2, len(images))
|
||||
self.assertEqual('a', images[0].id)
|
||||
self.assertEqual('b', images[1].id)
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_list_with_marker(self):
|
||||
list(self.mgr.list(marker='a'))
|
||||
url = '/v1/images/detail?limit=20&marker=a'
|
||||
expect = [('GET', url, {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_list_with_filter(self):
|
||||
list(self.mgr.list(filters={'name': "foo"}))
|
||||
url = '/v1/images/detail?limit=20&name=foo'
|
||||
expect = [('GET', url, {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_list_with_property_filters(self):
|
||||
list(self.mgr.list(filters={'properties': {'ping': 'pong'}}))
|
||||
url = '/v1/images/detail?limit=20&property-ping=pong'
|
||||
expect = [('GET', url, {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_list_with_sort_dir(self):
|
||||
list(self.mgr.list(sort_dir='desc'))
|
||||
url = '/v1/images/detail?limit=20&sort_dir=desc'
|
||||
expect = [('GET', url, {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_list_with_sort_key(self):
|
||||
list(self.mgr.list(sort_key='name'))
|
||||
url = '/v1/images/detail?limit=20&sort_key=name'
|
||||
expect = [('GET', url, {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_get(self):
|
||||
image = self.mgr.get('1')
|
||||
expect = [('HEAD', '/v1/images/1', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('1', image.id)
|
||||
self.assertEqual('image-1', image.name)
|
||||
self.assertEqual(False, image.is_public)
|
||||
self.assertEqual(False, image.protected)
|
||||
self.assertEqual(False, image.deleted)
|
||||
self.assertEqual({u'arch': u'x86_64'}, image.properties)
|
||||
|
||||
def test_get_int(self):
|
||||
image = self.mgr.get(1)
|
||||
expect = [('HEAD', '/v1/images/1', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('1', image.id)
|
||||
self.assertEqual('image-1', image.name)
|
||||
self.assertEqual(False, image.is_public)
|
||||
self.assertEqual(False, image.protected)
|
||||
self.assertEqual(False, image.deleted)
|
||||
self.assertEqual({u'arch': u'x86_64'}, image.properties)
|
||||
|
||||
def test_get_encoding(self):
|
||||
image = self.mgr.get('3')
|
||||
self.assertEqual(u"ni\xf1o", image.name)
|
||||
|
||||
def test_get_req_id(self):
|
||||
params = {'return_req_id': []}
|
||||
self.mgr.get('4', **params)
|
||||
expect_req_id = ['req-1234']
|
||||
self.assertEqual(expect_req_id, params['return_req_id'])
|
||||
|
||||
def test_data(self):
|
||||
data = ''.join([b for b in self.mgr.data('1', do_checksum=False)])
|
||||
expect = [('GET', '/v1/images/1', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('XXX', data)
|
||||
|
||||
expect += [('GET', '/v1/images/1', {}, None)]
|
||||
data = ''.join([b for b in self.mgr.data('1')])
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('XXX', data)
|
||||
|
||||
def test_data_with_wrong_checksum(self):
|
||||
data = ''.join([b for b in self.mgr.data('2', do_checksum=False)])
|
||||
expect = [('GET', '/v1/images/2', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('YYY', data)
|
||||
|
||||
expect += [('GET', '/v1/images/2', {}, None)]
|
||||
data = self.mgr.data('2')
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
try:
|
||||
data = ''.join([b for b in data])
|
||||
self.fail('data did not raise an error.')
|
||||
except IOError as e:
|
||||
self.assertEqual(errno.EPIPE, e.errno)
|
||||
msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong'
|
||||
self.assertIn(msg, str(e))
|
||||
|
||||
def test_data_req_id(self):
|
||||
params = {
|
||||
'do_checksum': False,
|
||||
'return_req_id': [],
|
||||
}
|
||||
''.join([b for b in self.mgr.data('4', **params)])
|
||||
expect_req_id = ['req-1234']
|
||||
self.assertEqual(expect_req_id, params['return_req_id'])
|
||||
|
||||
def test_data_with_checksum(self):
|
||||
data = ''.join([b for b in self.mgr.data('3', do_checksum=False)])
|
||||
expect = [('GET', '/v1/images/3', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('ZZZ', data)
|
||||
|
||||
expect += [('GET', '/v1/images/3', {}, None)]
|
||||
data = ''.join([b for b in self.mgr.data('3')])
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('ZZZ', data)
|
||||
|
||||
def test_delete(self):
|
||||
self.mgr.delete('1')
|
||||
expect = [('DELETE', '/v1/images/1', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_delete_req_id(self):
|
||||
params = {
|
||||
'return_req_id': []
|
||||
}
|
||||
self.mgr.delete('4', **params)
|
||||
expect = [('DELETE', '/v1/images/4', {}, None)]
|
||||
self.assertEqual(self.api.calls, expect)
|
||||
expect_req_id = ['req-1234']
|
||||
self.assertEqual(expect_req_id, params['return_req_id'])
|
||||
|
||||
def test_create_without_data(self):
|
||||
params = {
|
||||
'id': '1',
|
||||
'name': 'image-1',
|
||||
'container_format': 'ovf',
|
||||
'disk_format': 'vhd',
|
||||
'owner': 'asdf',
|
||||
'size': 1024,
|
||||
'min_ram': 512,
|
||||
'min_disk': 10,
|
||||
'copy_from': 'http://example.com',
|
||||
'properties': {'a': 'b', 'c': 'd'},
|
||||
}
|
||||
image = self.mgr.create(**params)
|
||||
expect_headers = {
|
||||
'x-image-meta-id': '1',
|
||||
'x-image-meta-name': 'image-1',
|
||||
'x-image-meta-container_format': 'ovf',
|
||||
'x-image-meta-disk_format': 'vhd',
|
||||
'x-image-meta-owner': 'asdf',
|
||||
'x-image-meta-size': '1024',
|
||||
'x-image-meta-min_ram': '512',
|
||||
'x-image-meta-min_disk': '10',
|
||||
'x-glance-api-copy-from': 'http://example.com',
|
||||
'x-image-meta-property-a': 'b',
|
||||
'x-image-meta-property-c': 'd',
|
||||
}
|
||||
expect = [('POST', '/v1/images', expect_headers, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('1', image.id)
|
||||
self.assertEqual('image-1', image.name)
|
||||
self.assertEqual('ovf', image.container_format)
|
||||
self.assertEqual('vhd', image.disk_format)
|
||||
self.assertEqual('asdf', image.owner)
|
||||
self.assertEqual(1024, image.size)
|
||||
self.assertEqual(512, image.min_ram)
|
||||
self.assertEqual(10, image.min_disk)
|
||||
self.assertEqual(False, image.is_public)
|
||||
self.assertEqual(False, image.protected)
|
||||
self.assertEqual(False, image.deleted)
|
||||
self.assertEqual({'a': 'b', 'c': 'd'}, image.properties)
|
||||
|
||||
def test_create_with_data(self):
|
||||
image_data = six.StringIO('XXX')
|
||||
self.mgr.create(data=image_data)
|
||||
expect_headers = {'x-image-meta-size': '3'}
|
||||
expect = [('POST', '/v1/images', expect_headers, image_data)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_create_req_id(self):
|
||||
params = {
|
||||
'id': '4',
|
||||
'name': 'image-4',
|
||||
'container_format': 'ovf',
|
||||
'disk_format': 'vhd',
|
||||
'owner': 'asdf',
|
||||
'size': 1024,
|
||||
'min_ram': 512,
|
||||
'min_disk': 10,
|
||||
'copy_from': 'http://example.com',
|
||||
'properties': {'a': 'b', 'c': 'd'},
|
||||
'return_req_id': [],
|
||||
}
|
||||
image = self.mgr.create(**params)
|
||||
expect_headers = {
|
||||
'x-image-meta-id': '4',
|
||||
'x-image-meta-name': 'image-4',
|
||||
'x-image-meta-container_format': 'ovf',
|
||||
'x-image-meta-disk_format': 'vhd',
|
||||
'x-image-meta-owner': 'asdf',
|
||||
'x-image-meta-size': '1024',
|
||||
'x-image-meta-min_ram': '512',
|
||||
'x-image-meta-min_disk': '10',
|
||||
'x-glance-api-copy-from': 'http://example.com',
|
||||
'x-image-meta-property-a': 'b',
|
||||
'x-image-meta-property-c': 'd',
|
||||
}
|
||||
expect = [('POST', '/v1/images', expect_headers, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('1', image.id)
|
||||
expect_req_id = ['req-1234']
|
||||
self.assertEqual(expect_req_id, params['return_req_id'])
|
||||
|
||||
def test_update(self):
|
||||
fields = {
|
||||
'name': 'image-2',
|
||||
'container_format': 'ovf',
|
||||
'disk_format': 'vhd',
|
||||
'owner': 'asdf',
|
||||
'size': 1024,
|
||||
'min_ram': 512,
|
||||
'min_disk': 10,
|
||||
'copy_from': 'http://example.com',
|
||||
'properties': {'a': 'b', 'c': 'd'},
|
||||
'deleted': False,
|
||||
}
|
||||
image = self.mgr.update('1', **fields)
|
||||
expect_hdrs = {
|
||||
'x-image-meta-name': 'image-2',
|
||||
'x-image-meta-container_format': 'ovf',
|
||||
'x-image-meta-disk_format': 'vhd',
|
||||
'x-image-meta-owner': 'asdf',
|
||||
'x-image-meta-size': '1024',
|
||||
'x-image-meta-min_ram': '512',
|
||||
'x-image-meta-min_disk': '10',
|
||||
'x-glance-api-copy-from': 'http://example.com',
|
||||
'x-image-meta-property-a': 'b',
|
||||
'x-image-meta-property-c': 'd',
|
||||
'x-image-meta-deleted': 'False',
|
||||
'x-glance-registry-purge-props': 'false',
|
||||
}
|
||||
expect = [('PUT', '/v1/images/1', expect_hdrs, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('1', image.id)
|
||||
self.assertEqual('image-2', image.name)
|
||||
self.assertEqual(1024, image.size)
|
||||
self.assertEqual(512, image.min_ram)
|
||||
self.assertEqual(10, image.min_disk)
|
||||
|
||||
def test_update_with_data(self):
|
||||
image_data = six.StringIO('XXX')
|
||||
self.mgr.update('1', data=image_data)
|
||||
expect_headers = {'x-image-meta-size': '3',
|
||||
'x-glance-registry-purge-props': 'false'}
|
||||
expect = [('PUT', '/v1/images/1', expect_headers, image_data)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_update_with_purge_props(self):
|
||||
self.mgr.update('1', purge_props=True)
|
||||
expect_headers = {'x-glance-registry-purge-props': 'true'}
|
||||
expect = [('PUT', '/v1/images/1', expect_headers, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_update_with_purge_props_false(self):
|
||||
self.mgr.update('1', purge_props=False)
|
||||
expect_headers = {'x-glance-registry-purge-props': 'false'}
|
||||
expect = [('PUT', '/v1/images/1', expect_headers, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_update_req_id(self):
|
||||
fields = {
|
||||
'purge_props': True,
|
||||
'return_req_id': [],
|
||||
}
|
||||
self.mgr.update('4', **fields)
|
||||
expect_headers = {'x-glance-registry-purge-props': 'true'}
|
||||
expect = [('PUT', '/v1/images/4', expect_headers, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
expect_req_id = ['req-1234']
|
||||
self.assertEqual(expect_req_id, fields['return_req_id'])
|
||||
|
||||
def test_image_meta_from_headers_encoding(self):
|
||||
value = u"ni\xf1o"
|
||||
if six.PY2:
|
||||
fields = {"x-image-meta-name": "ni\xc3\xb1o"}
|
||||
else:
|
||||
fields = {"x-image-meta-name": value}
|
||||
headers = self.mgr._image_meta_from_headers(fields)
|
||||
self.assertEqual(value, headers["name"])
|
||||
|
||||
def test_image_list_with_owner(self):
|
||||
images = self.mgr.list(owner='A', page_size=20)
|
||||
image_list = list(images)
|
||||
self.assertEqual('A', image_list[0].owner)
|
||||
self.assertEqual('a', image_list[0].id)
|
||||
self.assertEqual(1, len(image_list))
|
||||
|
||||
def test_image_list_with_owner_req_id(self):
|
||||
fields = {
|
||||
'owner': 'A',
|
||||
'return_req_id': [],
|
||||
}
|
||||
images = self.mgr.list(**fields)
|
||||
next(images)
|
||||
self.assertEqual(['req-1234'], fields['return_req_id'])
|
||||
|
||||
def test_image_list_with_notfound_owner(self):
|
||||
images = self.mgr.list(owner='X', page_size=20)
|
||||
self.assertEqual(0, len(list(images)))
|
||||
|
||||
def test_image_list_with_empty_string_owner(self):
|
||||
images = self.mgr.list(owner='', page_size=20)
|
||||
image_list = list(images)
|
||||
self.assertRaises(AttributeError, lambda: image_list[0].owner)
|
||||
self.assertEqual('c', image_list[0].id)
|
||||
self.assertEqual(1, len(image_list))
|
||||
|
||||
def test_image_list_with_unspecified_owner(self):
|
||||
images = self.mgr.list(owner=None, page_size=5)
|
||||
image_list = list(images)
|
||||
self.assertEqual('A', image_list[0].owner)
|
||||
self.assertEqual('a', image_list[0].id)
|
||||
self.assertEqual('A', image_list[1].owner)
|
||||
self.assertEqual('b', image_list[1].id)
|
||||
self.assertEqual('B', image_list[2].owner)
|
||||
self.assertEqual('b2', image_list[2].id)
|
||||
self.assertRaises(AttributeError, lambda: image_list[3].owner)
|
||||
self.assertEqual('c', image_list[3].id)
|
||||
self.assertEqual(4, len(image_list))
|
||||
|
||||
def test_image_list_with_owner_and_limit(self):
|
||||
images = self.mgr.list(owner='B', page_size=5, limit=1)
|
||||
image_list = list(images)
|
||||
self.assertEqual('B', image_list[0].owner)
|
||||
self.assertEqual('b', image_list[0].id)
|
||||
self.assertEqual(1, len(image_list))
|
||||
|
||||
def test_image_list_all_tenants(self):
|
||||
images = self.mgr.list(is_public=None, page_size=5)
|
||||
image_list = list(images)
|
||||
self.assertEqual('A', image_list[0].owner)
|
||||
self.assertEqual('a', image_list[0].id)
|
||||
self.assertEqual('B', image_list[1].owner)
|
||||
self.assertEqual('b', image_list[1].id)
|
||||
self.assertEqual('B', image_list[2].owner)
|
||||
self.assertEqual('b2', image_list[2].id)
|
||||
self.assertRaises(AttributeError, lambda: image_list[3].owner)
|
||||
self.assertEqual('c', image_list[3].id)
|
||||
self.assertEqual(4, len(image_list))
|
||||
|
||||
def test_update_v2_created_image_using_v1(self):
|
||||
fields_to_update = {
|
||||
'name': 'bar',
|
||||
'container_format': 'bare',
|
||||
'disk_format': 'qcow2',
|
||||
}
|
||||
image = self.mgr.update('v2_created_img', **fields_to_update)
|
||||
expect_hdrs = {
|
||||
'x-image-meta-name': 'bar',
|
||||
'x-image-meta-container_format': 'bare',
|
||||
'x-image-meta-disk_format': 'qcow2',
|
||||
'x-glance-registry-purge-props': 'false',
|
||||
}
|
||||
expect = [('PUT', '/v1/images/v2_created_img', expect_hdrs, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('v2_created_img', image.id)
|
||||
self.assertEqual('bar', image.name)
|
||||
self.assertEqual(0, image.size)
|
||||
self.assertEqual('bare', image.container_format)
|
||||
self.assertEqual('qcow2', image.disk_format)
|
||||
|
||||
|
||||
class ImageTest(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(ImageTest, self).setUp()
|
||||
self.api = utils.FakeAPI(fixtures)
|
||||
self.mgr = images.ImageManager(self.api)
|
||||
|
||||
def test_delete(self):
|
||||
image = self.mgr.get('1')
|
||||
image.delete()
|
||||
expect = [
|
||||
('HEAD', '/v1/images/1', {}, None),
|
||||
('HEAD', '/v1/images/1', {}, None),
|
||||
('DELETE', '/v1/images/1', {}, None),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_update(self):
|
||||
image = self.mgr.get('1')
|
||||
image.update(name='image-5')
|
||||
expect = [
|
||||
('HEAD', '/v1/images/1', {}, None),
|
||||
('HEAD', '/v1/images/1', {}, None),
|
||||
('PUT', '/v1/images/1',
|
||||
{'x-image-meta-name': 'image-5',
|
||||
'x-glance-registry-purge-props': 'false'}, None),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_data(self):
|
||||
image = self.mgr.get('1')
|
||||
data = ''.join([b for b in image.data()])
|
||||
expect = [
|
||||
('HEAD', '/v1/images/1', {}, None),
|
||||
('HEAD', '/v1/images/1', {}, None),
|
||||
('GET', '/v1/images/1', {}, None),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('XXX', data)
|
||||
|
||||
data = ''.join([b for b in image.data(do_checksum=False)])
|
||||
expect += [('GET', '/v1/images/1', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('XXX', data)
|
||||
|
||||
def test_data_with_wrong_checksum(self):
|
||||
image = self.mgr.get('2')
|
||||
data = ''.join([b for b in image.data(do_checksum=False)])
|
||||
expect = [
|
||||
('HEAD', '/v1/images/2', {}, None),
|
||||
('HEAD', '/v1/images/2', {}, None),
|
||||
('GET', '/v1/images/2', {}, None),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('YYY', data)
|
||||
|
||||
data = image.data()
|
||||
expect += [('GET', '/v1/images/2', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
try:
|
||||
data = ''.join([b for b in image.data()])
|
||||
self.fail('data did not raise an error.')
|
||||
except IOError as e:
|
||||
self.assertEqual(errno.EPIPE, e.errno)
|
||||
msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong'
|
||||
self.assertIn(msg, str(e))
|
||||
|
||||
def test_data_with_checksum(self):
|
||||
image = self.mgr.get('3')
|
||||
data = ''.join([b for b in image.data(do_checksum=False)])
|
||||
expect = [
|
||||
('HEAD', '/v1/images/3', {}, None),
|
||||
('HEAD', '/v1/images/3', {}, None),
|
||||
('GET', '/v1/images/3', {}, None),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('ZZZ', data)
|
||||
|
||||
data = ''.join([b for b in image.data()])
|
||||
expect += [('GET', '/v1/images/3', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual('ZZZ', data)
|
||||
|
||||
|
||||
class ParameterFakeAPI(utils.FakeAPI):
|
||||
image_list = {'images': [
|
||||
{
|
||||
'id': 'a',
|
||||
'name': 'image-1',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
{
|
||||
'id': 'b',
|
||||
'name': 'image-2',
|
||||
'properties': {'arch': 'x86_64'},
|
||||
},
|
||||
]}
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
self.url = url
|
||||
return utils.FakeResponse({}), ParameterFakeAPI.image_list
|
||||
|
||||
|
||||
class FakeArg(object):
|
||||
def __init__(self, arg_dict):
|
||||
self.arg_dict = arg_dict
|
||||
self.fields = arg_dict.keys()
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self.arg_dict:
|
||||
return self.arg_dict[name]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class UrlParameterTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(UrlParameterTest, self).setUp()
|
||||
self.api = ParameterFakeAPI({})
|
||||
self.gc = client.Client("http://fakeaddress.com")
|
||||
self.gc.images = images.ImageManager(self.api)
|
||||
|
||||
def test_is_public_list(self):
|
||||
shell.do_image_list(self.gc, FakeArg({"is_public": "True"}))
|
||||
parts = parse.urlparse(self.api.url)
|
||||
qs_dict = parse.parse_qs(parts.query)
|
||||
self.assertIn('is_public', qs_dict)
|
||||
self.assertTrue(qs_dict['is_public'][0].lower() == "true")
|
@ -1,609 +0,0 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright (C) 2013 Yahoo! 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 argparse
|
||||
import json
|
||||
import os
|
||||
import six
|
||||
import subprocess
|
||||
import tempfile
|
||||
import testtools
|
||||
|
||||
import mock
|
||||
|
||||
from glanceclient import exc
|
||||
from glanceclient import shell
|
||||
|
||||
import glanceclient.v1.client as client
|
||||
import glanceclient.v1.images
|
||||
import glanceclient.v1.shell as v1shell
|
||||
|
||||
from glanceclient.tests import utils
|
||||
|
||||
if six.PY3:
|
||||
import io
|
||||
file_type = io.IOBase
|
||||
else:
|
||||
file_type = file
|
||||
|
||||
fixtures = {
|
||||
'/v1/images/96d2c7e1-de4e-4612-8aa2-ba26610c804e': {
|
||||
'PUT': (
|
||||
{
|
||||
'Location': 'http://fakeaddress.com:9292/v1/images/'
|
||||
'96d2c7e1-de4e-4612-8aa2-ba26610c804e',
|
||||
'Etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
||||
'X-Openstack-Request-Id':
|
||||
'req-b645039d-e1c7-43e5-b27b-2d18a173c42b',
|
||||
'Date': 'Mon, 29 Apr 2013 10:24:32 GMT'
|
||||
},
|
||||
json.dumps({
|
||||
'image': {
|
||||
'status': 'active', 'name': 'testimagerename',
|
||||
'deleted': False,
|
||||
'container_format': 'ami',
|
||||
'created_at': '2013-04-25T15:47:43',
|
||||
'disk_format': 'ami',
|
||||
'updated_at': '2013-04-29T10:24:32',
|
||||
'id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e',
|
||||
'min_disk': 0,
|
||||
'protected': False,
|
||||
'min_ram': 0,
|
||||
'checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
||||
'owner': '1310db0cce8f40b0987a5acbe139765a',
|
||||
'is_public': True,
|
||||
'deleted_at': None,
|
||||
'properties': {
|
||||
'kernel_id': '1b108400-65d8-4762-9ea4-1bf6c7be7568',
|
||||
'ramdisk_id': 'b759bee9-0669-4394-a05c-fa2529b1c114'
|
||||
},
|
||||
'size': 25165824
|
||||
}
|
||||
})
|
||||
),
|
||||
'HEAD': (
|
||||
{
|
||||
'x-image-meta-id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e',
|
||||
'x-image-meta-status': 'active'
|
||||
},
|
||||
None
|
||||
),
|
||||
'GET': (
|
||||
{
|
||||
'x-image-meta-status': 'active',
|
||||
'x-image-meta-owner': '1310db0cce8f40b0987a5acbe139765a',
|
||||
'x-image-meta-name': 'cirros-0.3.1-x86_64-uec',
|
||||
'x-image-meta-container_format': 'ami',
|
||||
'x-image-meta-created_at': '2013-04-25T15:47:43',
|
||||
'etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
||||
'location': 'http://fakeaddress.com:9292/v1/images/'
|
||||
'96d2c7e1-de4e-4612-8aa2-ba26610c804e',
|
||||
'x-image-meta-min_ram': '0',
|
||||
'x-image-meta-updated_at': '2013-04-25T15:47:43',
|
||||
'x-image-meta-id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e',
|
||||
'x-image-meta-property-ramdisk_id':
|
||||
'b759bee9-0669-4394-a05c-fa2529b1c114',
|
||||
'date': 'Mon, 29 Apr 2013 09:25:17 GMT',
|
||||
'x-image-meta-property-kernel_id':
|
||||
'1b108400-65d8-4762-9ea4-1bf6c7be7568',
|
||||
'x-openstack-request-id':
|
||||
'req-842735bf-77e8-44a7-bfd1-7d95c52cec7f',
|
||||
'x-image-meta-deleted': 'False',
|
||||
'x-image-meta-checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
||||
'x-image-meta-protected': 'False',
|
||||
'x-image-meta-min_disk': '0',
|
||||
'x-image-meta-size': '25165824',
|
||||
'x-image-meta-is_public': 'True',
|
||||
'content-type': 'text/html; charset=UTF-8',
|
||||
'x-image-meta-disk_format': 'ami',
|
||||
},
|
||||
None
|
||||
)
|
||||
},
|
||||
'/v1/images/44d2c7e1-de4e-4612-8aa2-ba26610c444f': {
|
||||
'PUT': (
|
||||
{
|
||||
'Location': 'http://fakeaddress.com:9292/v1/images/'
|
||||
'44d2c7e1-de4e-4612-8aa2-ba26610c444f',
|
||||
'Etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
||||
'X-Openstack-Request-Id':
|
||||
'req-b645039d-e1c7-43e5-b27b-2d18a173c42b',
|
||||
'Date': 'Mon, 29 Apr 2013 10:24:32 GMT'
|
||||
},
|
||||
json.dumps({
|
||||
'image': {
|
||||
'status': 'queued', 'name': 'testimagerename',
|
||||
'deleted': False,
|
||||
'container_format': 'ami',
|
||||
'created_at': '2013-04-25T15:47:43',
|
||||
'disk_format': 'ami',
|
||||
'updated_at': '2013-04-29T10:24:32',
|
||||
'id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f',
|
||||
'min_disk': 0,
|
||||
'protected': False,
|
||||
'min_ram': 0,
|
||||
'checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
||||
'owner': '1310db0cce8f40b0987a5acbe139765a',
|
||||
'is_public': True,
|
||||
'deleted_at': None,
|
||||
'properties': {
|
||||
'kernel_id':
|
||||
'1b108400-65d8-4762-9ea4-1bf6c7be7568',
|
||||
'ramdisk_id':
|
||||
'b759bee9-0669-4394-a05c-fa2529b1c114'
|
||||
},
|
||||
'size': 25165824
|
||||
}
|
||||
})
|
||||
),
|
||||
'HEAD': (
|
||||
{
|
||||
'x-image-meta-id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f',
|
||||
'x-image-meta-status': 'queued'
|
||||
},
|
||||
None
|
||||
),
|
||||
'GET': (
|
||||
{
|
||||
'x-image-meta-status': 'queued',
|
||||
'x-image-meta-owner': '1310db0cce8f40b0987a5acbe139765a',
|
||||
'x-image-meta-name': 'cirros-0.3.1-x86_64-uec',
|
||||
'x-image-meta-container_format': 'ami',
|
||||
'x-image-meta-created_at': '2013-04-25T15:47:43',
|
||||
'etag': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
||||
'location': 'http://fakeaddress.com:9292/v1/images/'
|
||||
'44d2c7e1-de4e-4612-8aa2-ba26610c444f',
|
||||
'x-image-meta-min_ram': '0',
|
||||
'x-image-meta-updated_at': '2013-04-25T15:47:43',
|
||||
'x-image-meta-id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f',
|
||||
'x-image-meta-property-ramdisk_id':
|
||||
'b759bee9-0669-4394-a05c-fa2529b1c114',
|
||||
'date': 'Mon, 29 Apr 2013 09:25:17 GMT',
|
||||
'x-image-meta-property-kernel_id':
|
||||
'1b108400-65d8-4762-9ea4-1bf6c7be7568',
|
||||
'x-openstack-request-id':
|
||||
'req-842735bf-77e8-44a7-bfd1-7d95c52cec7f',
|
||||
'x-image-meta-deleted': 'False',
|
||||
'x-image-meta-checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83',
|
||||
'x-image-meta-protected': 'False',
|
||||
'x-image-meta-min_disk': '0',
|
||||
'x-image-meta-size': '25165824',
|
||||
'x-image-meta-is_public': 'True',
|
||||
'content-type': 'text/html; charset=UTF-8',
|
||||
'x-image-meta-disk_format': 'ami',
|
||||
},
|
||||
None
|
||||
)
|
||||
},
|
||||
'/v1/images/detail?limit=20&name=70aa106f-3750-4d7c-a5ce-0a535ac08d0a': {
|
||||
'GET': (
|
||||
{},
|
||||
{'images': [
|
||||
{
|
||||
'id': '70aa106f-3750-4d7c-a5ce-0a535ac08d0a',
|
||||
'name': 'imagedeleted',
|
||||
'deleted': True,
|
||||
'status': 'deleted',
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v1/images/70aa106f-3750-4d7c-a5ce-0a535ac08d0a': {
|
||||
'HEAD': (
|
||||
{
|
||||
'x-image-meta-id': '70aa106f-3750-4d7c-a5ce-0a535ac08d0a',
|
||||
'x-image-meta-status': 'deleted'
|
||||
},
|
||||
None
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ShellInvalidEndpointandParameterTest(utils.TestCase):
|
||||
|
||||
# Patch os.environ to avoid required auth info.
|
||||
def setUp(self):
|
||||
"""Run before each test."""
|
||||
super(ShellInvalidEndpointandParameterTest, self).setUp()
|
||||
self.old_environment = os.environ.copy()
|
||||
os.environ = {
|
||||
'OS_USERNAME': 'username',
|
||||
'OS_PASSWORD': 'password',
|
||||
'OS_TENANT_ID': 'tenant_id',
|
||||
'OS_TOKEN_ID': 'test',
|
||||
'OS_AUTH_URL': 'http://127.0.0.1:5000/v2.0/',
|
||||
'OS_AUTH_TOKEN': 'pass',
|
||||
'OS_IMAGE_API_VERSION': '1',
|
||||
'OS_REGION_NAME': 'test',
|
||||
'OS_IMAGE_URL': 'http://is.invalid'}
|
||||
|
||||
self.shell = shell.OpenStackImagesShell()
|
||||
self.patched = mock.patch('glanceclient.common.utils.get_data_file',
|
||||
autospec=True, return_value=None)
|
||||
self.mock_get_data_file = self.patched.start()
|
||||
|
||||
self.gc = self._mock_glance_client()
|
||||
|
||||
def _make_args(self, args):
|
||||
# NOTE(venkatesh): this conversion from a dict to an object
|
||||
# is required because the test_shell.do_xxx(gc, args) methods
|
||||
# expects the args to be attributes of an object. If passed as
|
||||
# dict directly, it throws an AttributeError.
|
||||
class Args(object):
|
||||
def __init__(self, entries):
|
||||
self.__dict__.update(entries)
|
||||
|
||||
return Args(args)
|
||||
|
||||
def _mock_glance_client(self):
|
||||
my_mocked_gc = mock.Mock()
|
||||
my_mocked_gc.get.return_value = {}
|
||||
return my_mocked_gc
|
||||
|
||||
def tearDown(self):
|
||||
super(ShellInvalidEndpointandParameterTest, self).tearDown()
|
||||
os.environ = self.old_environment
|
||||
self.patched.stop()
|
||||
|
||||
def run_command(self, cmd):
|
||||
self.shell.main(cmd.split())
|
||||
|
||||
def assert_called(self, method, url, body=None, **kwargs):
|
||||
return self.shell.cs.assert_called(method, url, body, **kwargs)
|
||||
|
||||
def assert_called_anytime(self, method, url, body=None):
|
||||
return self.shell.cs.assert_called_anytime(method, url, body)
|
||||
|
||||
def test_image_list_invalid_endpoint(self):
|
||||
self.assertRaises(
|
||||
exc.CommunicationError, self.run_command, 'image-list')
|
||||
|
||||
def test_image_create_invalid_endpoint(self):
|
||||
self.assertRaises(
|
||||
exc.CommunicationError,
|
||||
self.run_command, 'image-create')
|
||||
|
||||
def test_image_delete_invalid_endpoint(self):
|
||||
self.assertRaises(
|
||||
exc.CommunicationError,
|
||||
self.run_command, 'image-delete <fake>')
|
||||
|
||||
def test_image_download_invalid_endpoint(self):
|
||||
self.assertRaises(
|
||||
exc.CommunicationError,
|
||||
self.run_command, 'image-download <fake>')
|
||||
|
||||
def test_members_list_invalid_endpoint(self):
|
||||
self.assertRaises(
|
||||
exc.CommunicationError,
|
||||
self.run_command, 'member-list --image-id fake')
|
||||
|
||||
def test_image_show_invalid_endpoint(self):
|
||||
self.assertRaises(
|
||||
exc.CommunicationError,
|
||||
self.run_command, 'image-show --human-readable <IMAGE_ID>')
|
||||
|
||||
def test_member_create_invalid_endpoint(self):
|
||||
self.assertRaises(
|
||||
exc.CommunicationError,
|
||||
self.run_command,
|
||||
'member-create --can-share <IMAGE_ID> <TENANT_ID>')
|
||||
|
||||
def test_member_delete_invalid_endpoint(self):
|
||||
self.assertRaises(
|
||||
exc.CommunicationError,
|
||||
self.run_command,
|
||||
'member-delete <IMAGE_ID> <TENANT_ID>')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_create_invalid_size_parameter(self, __):
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
self.run_command, 'image-create --size 10gb')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_create_invalid_ram_parameter(self, __):
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
self.run_command, 'image-create --min-ram 10gb')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_create_invalid_min_disk_parameter(self, __):
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
self.run_command, 'image-create --min-disk 10gb')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_create_missing_disk_format(self, __):
|
||||
# We test for all possible sources
|
||||
for origin in ('--file', '--location', '--copy-from'):
|
||||
e = self.assertRaises(exc.CommandError, self.run_command,
|
||||
'--os-image-api-version 1 image-create ' +
|
||||
origin + ' fake_src --container-format bare')
|
||||
self.assertEqual('error: Must provide --disk-format when using '
|
||||
+ origin + '.', e.message)
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_create_missing_container_format(self, __):
|
||||
# We test for all possible sources
|
||||
for origin in ('--file', '--location', '--copy-from'):
|
||||
e = self.assertRaises(exc.CommandError, self.run_command,
|
||||
'--os-image-api-version 1 image-create ' +
|
||||
origin + ' fake_src --disk-format qcow2')
|
||||
self.assertEqual('error: Must provide --container-format when '
|
||||
'using ' + origin + '.', e.message)
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_create_missing_container_format_stdin_data(self, __):
|
||||
# Fake that get_data_file method returns data
|
||||
self.mock_get_data_file.return_value = six.StringIO()
|
||||
e = self.assertRaises(exc.CommandError, self.run_command,
|
||||
'--os-image-api-version 1 image-create'
|
||||
' --disk-format qcow2')
|
||||
self.assertEqual('error: Must provide --container-format when '
|
||||
'using stdin.', e.message)
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_create_missing_disk_format_stdin_data(self, __):
|
||||
# Fake that get_data_file method returns data
|
||||
self.mock_get_data_file.return_value = six.StringIO()
|
||||
e = self.assertRaises(exc.CommandError, self.run_command,
|
||||
'--os-image-api-version 1 image-create'
|
||||
' --container-format bare')
|
||||
self.assertEqual('error: Must provide --disk-format when using stdin.',
|
||||
e.message)
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_update_invalid_size_parameter(self, __):
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
self.run_command, 'image-update --size 10gb')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_update_invalid_min_disk_parameter(self, __):
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
self.run_command, 'image-update --min-disk 10gb')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_update_invalid_ram_parameter(self, __):
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
self.run_command, 'image-update --min-ram 10gb')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_list_invalid_min_size_parameter(self, __):
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
self.run_command, 'image-list --size-min 10gb')
|
||||
|
||||
@mock.patch('sys.stderr')
|
||||
def test_image_list_invalid_max_size_parameter(self, __):
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
self.run_command, 'image-list --size-max 10gb')
|
||||
|
||||
def test_do_image_list_with_changes_since(self):
|
||||
input = {
|
||||
'name': None,
|
||||
'limit': None,
|
||||
'status': None,
|
||||
'container_format': 'bare',
|
||||
'size_min': None,
|
||||
'size_max': None,
|
||||
'is_public': True,
|
||||
'disk_format': 'raw',
|
||||
'page_size': 20,
|
||||
'visibility': True,
|
||||
'member_status': 'Fake',
|
||||
'owner': 'test',
|
||||
'checksum': 'fake_checksum',
|
||||
'tag': 'fake tag',
|
||||
'properties': [],
|
||||
'sort_key': None,
|
||||
'sort_dir': None,
|
||||
'all_tenants': False,
|
||||
'human_readable': True,
|
||||
'changes_since': '2011-1-1'
|
||||
}
|
||||
args = self._make_args(input)
|
||||
with mock.patch.object(self.gc.images, 'list') as mocked_list:
|
||||
mocked_list.return_value = {}
|
||||
|
||||
v1shell.do_image_list(self.gc, args)
|
||||
|
||||
exp_img_filters = {'container_format': 'bare',
|
||||
'changes-since': '2011-1-1',
|
||||
'disk_format': 'raw',
|
||||
'is_public': True}
|
||||
mocked_list.assert_called_once_with(sort_dir=None,
|
||||
sort_key=None,
|
||||
owner='test',
|
||||
page_size=20,
|
||||
filters=exp_img_filters)
|
||||
|
||||
|
||||
class ShellStdinHandlingTests(testtools.TestCase):
|
||||
|
||||
def _fake_update_func(self, *args, **kwargs):
|
||||
"""Replace glanceclient.images.update with a fake.
|
||||
|
||||
To determine the parameters that would be supplied with the update
|
||||
request.
|
||||
"""
|
||||
|
||||
# Store passed in args
|
||||
self.collected_args = (args, kwargs)
|
||||
|
||||
# Return the first arg, which is an image,
|
||||
# as do_image_update expects this.
|
||||
return args[0]
|
||||
|
||||
def setUp(self):
|
||||
super(ShellStdinHandlingTests, self).setUp()
|
||||
self.api = utils.FakeAPI(fixtures)
|
||||
self.gc = client.Client("http://fakeaddress.com")
|
||||
self.gc.images = glanceclient.v1.images.ImageManager(self.api)
|
||||
|
||||
# Store real stdin, so it can be restored in tearDown.
|
||||
self.real_sys_stdin_fd = os.dup(0)
|
||||
|
||||
# Replace stdin with a FD that points to /dev/null.
|
||||
dev_null = open('/dev/null')
|
||||
self.dev_null_fd = dev_null.fileno()
|
||||
os.dup2(dev_null.fileno(), 0)
|
||||
|
||||
# Replace the image update function with a fake,
|
||||
# so that we can tell if the data field was set correctly.
|
||||
self.real_update_func = self.gc.images.update
|
||||
self.collected_args = []
|
||||
self.gc.images.update = self._fake_update_func
|
||||
|
||||
def tearDown(self):
|
||||
"""Restore stdin and gc.images.update to their pretest states."""
|
||||
super(ShellStdinHandlingTests, self).tearDown()
|
||||
|
||||
def try_close(fd):
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
# Already closed
|
||||
pass
|
||||
|
||||
# Restore stdin
|
||||
os.dup2(self.real_sys_stdin_fd, 0)
|
||||
|
||||
# Close duplicate stdin handle
|
||||
try_close(self.real_sys_stdin_fd)
|
||||
|
||||
# Close /dev/null handle
|
||||
try_close(self.dev_null_fd)
|
||||
|
||||
# Restore the real image update function
|
||||
self.gc.images.update = self.real_update_func
|
||||
|
||||
def _do_update(self, image='96d2c7e1-de4e-4612-8aa2-ba26610c804e'):
|
||||
"""call v1/shell's do_image_update function."""
|
||||
|
||||
v1shell.do_image_update(
|
||||
self.gc, argparse.Namespace(
|
||||
image=image,
|
||||
name='testimagerename',
|
||||
property={},
|
||||
purge_props=False,
|
||||
human_readable=False,
|
||||
file=None,
|
||||
progress=False
|
||||
)
|
||||
)
|
||||
|
||||
def test_image_delete_deleted(self):
|
||||
self.assertRaises(
|
||||
exc.CommandError,
|
||||
v1shell.do_image_delete,
|
||||
self.gc,
|
||||
argparse.Namespace(
|
||||
images=['70aa106f-3750-4d7c-a5ce-0a535ac08d0a']
|
||||
)
|
||||
)
|
||||
|
||||
def test_image_update_closed_stdin(self):
|
||||
"""Test image update with a closed stdin.
|
||||
|
||||
Supply glanceclient with a closed stdin, and perform an image
|
||||
update to an active image. Glanceclient should not attempt to read
|
||||
stdin.
|
||||
"""
|
||||
|
||||
# NOTE(hughsaunders) Close stdin, which is repointed to /dev/null by
|
||||
# setUp()
|
||||
os.close(0)
|
||||
|
||||
self._do_update()
|
||||
|
||||
self.assertTrue(
|
||||
'data' not in self.collected_args[1]
|
||||
or self.collected_args[1]['data'] is None
|
||||
)
|
||||
|
||||
def test_image_update_opened_stdin(self):
|
||||
"""Test image update with an opened stdin.
|
||||
|
||||
Supply glanceclient with a stdin, and perform an image
|
||||
update to an active image. Glanceclient should not allow it.
|
||||
"""
|
||||
|
||||
self.assertRaises(
|
||||
SystemExit,
|
||||
v1shell.do_image_update,
|
||||
self.gc,
|
||||
argparse.Namespace(
|
||||
image='96d2c7e1-de4e-4612-8aa2-ba26610c804e',
|
||||
property={},
|
||||
)
|
||||
)
|
||||
|
||||
def test_image_update_data_is_read_from_file(self):
|
||||
"""Ensure that data is read from a file."""
|
||||
|
||||
try:
|
||||
|
||||
# NOTE(hughsaunders) Create a tmpfile, write some data to it and
|
||||
# set it as stdin
|
||||
f = open(tempfile.mktemp(), 'w+')
|
||||
f.write('Some Data')
|
||||
f.flush()
|
||||
f.seek(0)
|
||||
os.dup2(f.fileno(), 0)
|
||||
|
||||
self._do_update('44d2c7e1-de4e-4612-8aa2-ba26610c444f')
|
||||
|
||||
self.assertIn('data', self.collected_args[1])
|
||||
self.assertIsInstance(self.collected_args[1]['data'], file_type)
|
||||
self.assertEqual(b'Some Data',
|
||||
self.collected_args[1]['data'].read())
|
||||
|
||||
finally:
|
||||
try:
|
||||
f.close()
|
||||
os.remove(f.name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def test_image_update_data_is_read_from_pipe(self):
|
||||
"""Ensure that data is read from a pipe."""
|
||||
|
||||
try:
|
||||
|
||||
# NOTE(hughsaunders): Setup a pipe, duplicate it to stdin
|
||||
# ensure it is read.
|
||||
process = subprocess.Popen(['/bin/echo', 'Some Data'],
|
||||
stdout=subprocess.PIPE)
|
||||
os.dup2(process.stdout.fileno(), 0)
|
||||
|
||||
self._do_update('44d2c7e1-de4e-4612-8aa2-ba26610c444f')
|
||||
|
||||
self.assertIn('data', self.collected_args[1])
|
||||
self.assertIsInstance(self.collected_args[1]['data'], file_type)
|
||||
self.assertEqual(b'Some Data\n',
|
||||
self.collected_args[1]['data'].read())
|
||||
|
||||
finally:
|
||||
try:
|
||||
process.stdout.close()
|
||||
except OSError:
|
||||
pass
|
@ -1,79 +0,0 @@
|
||||
# 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.
|
||||
|
||||
import testtools
|
||||
|
||||
from glanceclient.tests import utils
|
||||
import glanceclient.v1.versions
|
||||
|
||||
|
||||
fixtures = {
|
||||
'/versions': {
|
||||
'GET': (
|
||||
{},
|
||||
{"versions": [
|
||||
{
|
||||
"status": "EXPERIMENTAL",
|
||||
"id": "v3.0",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://10.229.45.145:9292/v3/",
|
||||
"rel": "self"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"status": "CURRENT",
|
||||
"id": "v2.3",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://10.229.45.145:9292/v2/",
|
||||
"rel": "self"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"status": "SUPPORTED",
|
||||
"id": "v1.0",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://10.229.45.145:9292/v1/",
|
||||
"rel": "self"
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestVersions(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestVersions, self).setUp()
|
||||
self.api = utils.FakeAPI(fixtures)
|
||||
self.mgr = glanceclient.v1.versions.VersionManager(self.api)
|
||||
|
||||
def test_version_list(self):
|
||||
versions = self.mgr.list()
|
||||
expect = [('GET', '/versions', {}, None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual(3, len(versions))
|
||||
self.assertEqual('v3.0', versions[0]['id'])
|
||||
self.assertEqual('EXPERIMENTAL', versions[0]['status'])
|
||||
self.assertEqual([{"href": "http://10.229.45.145:9292/v3/",
|
||||
"rel": "self"}], versions[0]['links'])
|
@ -1,119 +0,0 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
#
|
||||
# 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
|
||||
|
||||
|
||||
class BaseController(testtools.TestCase):
|
||||
def __init__(self, api, schema_api, controller_class):
|
||||
self.controller = controller_class(api, schema_api)
|
||||
|
||||
def _assertRequestId(self, obj):
|
||||
self.assertIsNotNone(getattr(obj, 'request_ids', None))
|
||||
self.assertEqual(['req-1234'], obj.request_ids)
|
||||
|
||||
def list(self, *args, **kwargs):
|
||||
gen_obj = self.controller.list(*args, **kwargs)
|
||||
# For generator cases the request_ids property will be an empty list
|
||||
# until the underlying generator is invoked at-least once.
|
||||
resources = list(gen_obj)
|
||||
if len(resources) > 0:
|
||||
self._assertRequestId(gen_obj)
|
||||
else:
|
||||
# If list is empty that means geneator object has raised
|
||||
# StopIteration for first iteration and will not contain the
|
||||
# request_id in it.
|
||||
self.assertEqual([], gen_obj.request_ids)
|
||||
|
||||
return resources
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
resource = self.controller.get(*args, **kwargs)
|
||||
|
||||
self._assertRequestId(resource)
|
||||
return resource
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
resource = self.controller.create(*args, **kwargs)
|
||||
self._assertRequestId(resource)
|
||||
return resource
|
||||
|
||||
def create_multiple(self, *args, **kwargs):
|
||||
tags = self.controller.create_multiple(*args, **kwargs)
|
||||
actual = [tag.name for tag in tags]
|
||||
self._assertRequestId(tags)
|
||||
return actual
|
||||
|
||||
def update(self, *args, **properties):
|
||||
resource = self.controller.update(*args, **properties)
|
||||
self._assertRequestId(resource)
|
||||
return resource
|
||||
|
||||
def delete(self, *args):
|
||||
resp = self.controller.delete(*args)
|
||||
self._assertRequestId(resp)
|
||||
|
||||
def delete_all(self, *args):
|
||||
resp = self.controller.delete_all(*args)
|
||||
self._assertRequestId(resp)
|
||||
|
||||
def deactivate(self, *args):
|
||||
resp = self.controller.deactivate(*args)
|
||||
self._assertRequestId(resp)
|
||||
|
||||
def reactivate(self, *args):
|
||||
resp = self.controller.reactivate(*args)
|
||||
self._assertRequestId(resp)
|
||||
|
||||
def upload(self, *args, **kwargs):
|
||||
resp = self.controller.upload(*args, **kwargs)
|
||||
self._assertRequestId(resp)
|
||||
|
||||
def data(self, *args, **kwargs):
|
||||
body = self.controller.data(*args, **kwargs)
|
||||
self._assertRequestId(body)
|
||||
return body
|
||||
|
||||
def delete_locations(self, *args):
|
||||
resp = self.controller.delete_locations(*args)
|
||||
self._assertRequestId(resp)
|
||||
|
||||
def add_location(self, *args, **kwargs):
|
||||
resp = self.controller.add_location(*args, **kwargs)
|
||||
self._assertRequestId(resp)
|
||||
|
||||
def update_location(self, *args, **kwargs):
|
||||
resp = self.controller.update_location(*args, **kwargs)
|
||||
self._assertRequestId(resp)
|
||||
|
||||
def associate(self, *args, **kwargs):
|
||||
resource_types = self.controller.associate(*args, **kwargs)
|
||||
self._assertRequestId(resource_types)
|
||||
return resource_types
|
||||
|
||||
def deassociate(self, *args):
|
||||
resp = self.controller.deassociate(*args)
|
||||
self._assertRequestId(resp)
|
||||
|
||||
|
||||
class BaseResourceTypeController(BaseController):
|
||||
def __init__(self, api, schema_api, controller_class):
|
||||
super(BaseResourceTypeController, self).__init__(api, schema_api,
|
||||
controller_class)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
resource_types = self.controller.get(*args)
|
||||
names = [rt.name for rt in resource_types]
|
||||
self._assertRequestId(resource_types)
|
||||
return names
|
@ -1,413 +0,0 @@
|
||||
# Copyright (c) 2015 OpenStack Foundation
|
||||
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
UUID = "3fc2ba62-9a02-433e-b565-d493ffc69034"
|
||||
|
||||
image_list_fixture = {
|
||||
"images": [
|
||||
{
|
||||
"checksum": "9cb02fe7fcac26f8a25d6db3109063ae",
|
||||
"container_format": "bare",
|
||||
"created_at": "2015-07-23T16:58:50.000000",
|
||||
"deleted": "false",
|
||||
"deleted_at": "null",
|
||||
"disk_format": "raw",
|
||||
"id": UUID,
|
||||
"is_public": "false",
|
||||
"min_disk": 0,
|
||||
"min_ram": 0,
|
||||
"name": "test",
|
||||
"owner": "3447cea05d6947658d73791ed9e0ed9f",
|
||||
"properties": {
|
||||
"kernel_id": 1234,
|
||||
"ramdisk_id": 5678
|
||||
},
|
||||
"protected": "false",
|
||||
"size": 145,
|
||||
"status": "active",
|
||||
"updated_at": "2015-07-23T16:58:51.000000",
|
||||
"virtual_size": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
image_show_fixture = {
|
||||
"checksum": "9cb02fe7fcac26f8a25d6db3109063ae",
|
||||
"container_format": "bare",
|
||||
"created_at": "2015-07-24T12:18:13Z",
|
||||
"disk_format": "raw",
|
||||
"file": "/v2/images/%s/file" % UUID,
|
||||
"id": UUID,
|
||||
"kernel_id": "1234",
|
||||
"min_disk": 0,
|
||||
"min_ram": 0,
|
||||
"name": "img1",
|
||||
"owner": "411423405e10431fb9c47ac5b2446557",
|
||||
"protected": "false",
|
||||
"ramdisk_id": "5678",
|
||||
"schema": "/v2/schemas/image",
|
||||
"self": "/v2/images/%s" % UUID,
|
||||
"size": 145,
|
||||
"status": "active",
|
||||
"tags": [],
|
||||
"updated_at": "2015-07-24T12:18:13Z",
|
||||
"virtual_size": "null",
|
||||
"visibility": "shared"
|
||||
}
|
||||
|
||||
image_create_fixture = {
|
||||
"checksum": "9cb02fe7fcac26f8a25d6db3109063ae",
|
||||
"container_format": "bare",
|
||||
"created_at": "2015-07-24T12:18:13Z",
|
||||
"disk_format": "raw",
|
||||
"file": "/v2/images/%s/file" % UUID,
|
||||
"id": UUID,
|
||||
"kernel_id": "af81fccd-b2e8-4232-886c-aa98dda22882",
|
||||
"min_disk": 0,
|
||||
"min_ram": 0,
|
||||
"name": "img1",
|
||||
"owner": "411423405e10431fb9c47ac5b2446557",
|
||||
"protected": False,
|
||||
"ramdisk_id": "fdb3f864-9458-4185-bd26-5d27fe6b6adf",
|
||||
"schema": "/v2/schemas/image",
|
||||
"self": "/v2/images/%s" % UUID,
|
||||
"size": 145,
|
||||
"status": "active",
|
||||
"tags": [],
|
||||
"updated_at": "2015-07-24T12:18:13Z",
|
||||
"virtual_size": 123,
|
||||
"visibility": "private"
|
||||
}
|
||||
|
||||
schema_fixture = {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"href": "{self}",
|
||||
"rel": "self"
|
||||
},
|
||||
{
|
||||
"href": "{file}",
|
||||
"rel": "enclosure"
|
||||
},
|
||||
{
|
||||
"href": "{schema}",
|
||||
"rel": "describedby"
|
||||
}
|
||||
],
|
||||
"name": "image",
|
||||
"properties": {
|
||||
"architecture": {
|
||||
"description": "Operating system architecture as specified in "
|
||||
"http://docs.openstack.org/user-guide/common"
|
||||
"/cli_manage_images.html",
|
||||
"is_base": "false",
|
||||
"type": "string"
|
||||
},
|
||||
"checksum": {
|
||||
"readOnly": True,
|
||||
"description": "md5 hash of image contents.",
|
||||
"maxLength": 32,
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"container_format": {
|
||||
"description": "Format of the container",
|
||||
"enum": [
|
||||
"null",
|
||||
"ami",
|
||||
"ari",
|
||||
"aki",
|
||||
"bare",
|
||||
"ovf",
|
||||
"ova",
|
||||
"docker"
|
||||
],
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"created_at": {
|
||||
"readOnly": True,
|
||||
"description": "Date and time of image registration",
|
||||
"type": "string"
|
||||
},
|
||||
"direct_url": {
|
||||
"readOnly": True,
|
||||
"description": "URL to access the image file kept in external "
|
||||
"store",
|
||||
"type": "string"
|
||||
},
|
||||
"disk_format": {
|
||||
"description": "Format of the disk",
|
||||
"enum": [
|
||||
"null",
|
||||
"ami",
|
||||
"ari",
|
||||
"aki",
|
||||
"vhd",
|
||||
"vmdk",
|
||||
"raw",
|
||||
"qcow2",
|
||||
"vdi",
|
||||
"iso",
|
||||
"ploop"
|
||||
],
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"file": {
|
||||
"readOnly": True,
|
||||
"description": "An image file url",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "An identifier for the image",
|
||||
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])"
|
||||
"{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
|
||||
"type": "string"
|
||||
},
|
||||
"instance_uuid": {
|
||||
"description": ("Metadata which can be used to record which "
|
||||
"instance this image is associated with. "
|
||||
"(Informational only, does not create an instance "
|
||||
"snapshot.)"),
|
||||
"is_base": "false",
|
||||
"type": "string"
|
||||
},
|
||||
"kernel_id": {
|
||||
"description": "ID of image stored in Glance that should be used "
|
||||
"as the kernel when booting an AMI-style image.",
|
||||
"is_base": "false",
|
||||
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])"
|
||||
"{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"locations": {
|
||||
"description": "A set of URLs to access the image file kept "
|
||||
"in external store",
|
||||
"items": {
|
||||
"properties": {
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"url": {
|
||||
"maxLength": 255,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"url",
|
||||
"metadata"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"min_disk": {
|
||||
"description": "Amount of disk space (in GB) required to "
|
||||
"boot image.",
|
||||
"type": "integer"
|
||||
},
|
||||
"min_ram": {
|
||||
"description": "Amount of ram (in MB) required to boot image.",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "Descriptive name for the image",
|
||||
"maxLength": 255,
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"os_distro": {
|
||||
"description": "Common name of operating system distribution as "
|
||||
"specified in http://docs.openstack.org/trunk/"
|
||||
"openstack-compute/admin/content/"
|
||||
"adding-images.html",
|
||||
"is_base": "false",
|
||||
"type": "string"
|
||||
},
|
||||
"os_version": {
|
||||
"description": "Operating system version as specified "
|
||||
"by the distributor",
|
||||
"is_base": "false",
|
||||
"type": "string"
|
||||
},
|
||||
"owner": {
|
||||
"description": "Owner of the image",
|
||||
"maxLength": 255,
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"protected": {
|
||||
"description": "If true, image will not be deletable.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"ramdisk_id": {
|
||||
"description": "ID of image stored in Glance that should be used "
|
||||
"as the ramdisk when booting an AMI-style image.",
|
||||
"is_base": "false",
|
||||
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])"
|
||||
"{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"schema": {
|
||||
"readOnly": True,
|
||||
"description": "An image schema url",
|
||||
"type": "string"
|
||||
},
|
||||
"self": {
|
||||
"readOnly": True,
|
||||
"description": "An image self url",
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"readOnly": True,
|
||||
"description": "Size of image file in bytes",
|
||||
"type": [
|
||||
"null",
|
||||
"integer"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"readOnly": True,
|
||||
"description": "Status of the image",
|
||||
"enum": [
|
||||
"queued",
|
||||
"saving",
|
||||
"active",
|
||||
"killed",
|
||||
"deleted",
|
||||
"pending_delete"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"description": "List of strings related to the image",
|
||||
"items": {
|
||||
"maxLength": 255,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"updated_at": {
|
||||
"readOnly": True,
|
||||
"description": "Date and time of the last image "
|
||||
"modification",
|
||||
"type": "string"
|
||||
},
|
||||
"virtual_size": {
|
||||
"readOnly": True,
|
||||
"description": "Virtual size of image in bytes",
|
||||
"type": [
|
||||
"null",
|
||||
"integer"
|
||||
]
|
||||
},
|
||||
"visibility": {
|
||||
"description": "Scope of image accessibility",
|
||||
"enum": [
|
||||
"public",
|
||||
"private",
|
||||
"community",
|
||||
"shared"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
image_versions_fixture = {
|
||||
"versions": [
|
||||
{
|
||||
"id": "v2.3",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://localhost:9292/v2/",
|
||||
"rel": "self"
|
||||
}
|
||||
],
|
||||
"status": "CURRENT"
|
||||
},
|
||||
{
|
||||
"id": "v2.2",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://localhost:9292/v2/",
|
||||
"rel": "self"
|
||||
}
|
||||
],
|
||||
"status": "SUPPORTED"
|
||||
},
|
||||
{
|
||||
"id": "v2.1",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://localhost:9292/v2/",
|
||||
"rel": "self"
|
||||
}
|
||||
],
|
||||
"status": "SUPPORTED"
|
||||
},
|
||||
{
|
||||
"id": "v2.0",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://localhost:9292/v2/",
|
||||
"rel": "self"
|
||||
}
|
||||
],
|
||||
"status": "SUPPORTED"
|
||||
},
|
||||
{
|
||||
"id": "v1.1",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://localhost:9292/v1/",
|
||||
"rel": "self"
|
||||
}
|
||||
],
|
||||
"status": "SUPPORTED"
|
||||
},
|
||||
{
|
||||
"id": "v1.0",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://localhost:9292/v1/",
|
||||
"rel": "self"
|
||||
}
|
||||
],
|
||||
"status": "SUPPORTED"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
# Copyright (c) 2015 OpenStack Foundation
|
||||
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from requests_mock.contrib import fixture as rm_fixture
|
||||
|
||||
from glanceclient import client
|
||||
from glanceclient.tests.unit.v2.fixtures import image_create_fixture
|
||||
from glanceclient.tests.unit.v2.fixtures import image_list_fixture
|
||||
from glanceclient.tests.unit.v2.fixtures import image_show_fixture
|
||||
from glanceclient.tests.unit.v2.fixtures import schema_fixture
|
||||
from glanceclient.tests import utils as testutils
|
||||
from glanceclient.v2.image_schema import _BASE_SCHEMA
|
||||
|
||||
|
||||
class ClientTestRequests(testutils.TestCase):
|
||||
"""Client tests using the requests mock library."""
|
||||
|
||||
def test_list_bad_image_schema(self):
|
||||
# if kernel_id or ramdisk_id are not uuids, verify we can
|
||||
# still perform an image listing. Regression test for bug
|
||||
# 1477910
|
||||
self.requests = self.useFixture(rm_fixture.Fixture())
|
||||
self.requests.get('http://example.com/v2/schemas/image',
|
||||
json=schema_fixture)
|
||||
self.requests.get('http://example.com/v2/images?limit=20',
|
||||
json=image_list_fixture)
|
||||
gc = client.Client(2.2, "http://example.com/v2.1")
|
||||
images = gc.images.list()
|
||||
for image in images:
|
||||
pass
|
||||
|
||||
def test_show_bad_image_schema(self):
|
||||
# if kernel_id or ramdisk_id are not uuids, verify we
|
||||
# don't fail due to schema validation
|
||||
self.requests = self.useFixture(rm_fixture.Fixture())
|
||||
self.requests.get('http://example.com/v2/schemas/image',
|
||||
json=schema_fixture)
|
||||
self.requests.get('http://example.com/v2/images/%s'
|
||||
% image_show_fixture['id'],
|
||||
json=image_show_fixture)
|
||||
gc = client.Client(2.2, "http://example.com/v2.1")
|
||||
img = gc.images.get(image_show_fixture['id'])
|
||||
self.assertEqual(image_show_fixture['checksum'], img['checksum'])
|
||||
|
||||
def test_invalid_disk_format(self):
|
||||
self.requests = self.useFixture(rm_fixture.Fixture())
|
||||
self.requests.get('http://example.com/v2/schemas/image',
|
||||
json=_BASE_SCHEMA)
|
||||
self.requests.post('http://example.com/v2/images',
|
||||
json=image_create_fixture)
|
||||
self.requests.get('http://example.com/v2/images/%s'
|
||||
% image_show_fixture['id'],
|
||||
json=image_show_fixture)
|
||||
gc = client.Client(2.2, "http://example.com/v2.1")
|
||||
fields = {"disk_format": "qbull2"}
|
||||
try:
|
||||
gc.images.create(**fields)
|
||||
self.fail("Failed to raise exception when using bad disk format")
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def test_valid_disk_format(self):
|
||||
self.requests = self.useFixture(rm_fixture.Fixture())
|
||||
self.requests.get('http://example.com/v2/schemas/image',
|
||||
json=_BASE_SCHEMA)
|
||||
self.requests.post('http://example.com/v2/images',
|
||||
json=image_create_fixture)
|
||||
self.requests.get('http://example.com/v2/images/%s'
|
||||
% image_show_fixture['id'],
|
||||
json=image_show_fixture)
|
||||
gc = client.Client(2.2, "http://example.com/v2.1")
|
||||
fields = {"disk_format": "vhdx"}
|
||||
gc.images.create(**fields)
|
File diff suppressed because it is too large
Load Diff
@ -1,121 +0,0 @@
|
||||
# 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 testtools
|
||||
|
||||
from glanceclient.tests.unit.v2 import base
|
||||
from glanceclient.tests import utils
|
||||
from glanceclient.v2 import image_members
|
||||
|
||||
|
||||
IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1'
|
||||
MEMBER = '11223344-5566-7788-9911-223344556677'
|
||||
|
||||
|
||||
data_fixtures = {
|
||||
'/v2/images/{image}/members'.format(image=IMAGE): {
|
||||
'GET': (
|
||||
{},
|
||||
{'members': [
|
||||
{
|
||||
'image_id': IMAGE,
|
||||
'member_id': MEMBER,
|
||||
},
|
||||
]},
|
||||
),
|
||||
'POST': (
|
||||
{},
|
||||
{
|
||||
'image_id': IMAGE,
|
||||
'member_id': MEMBER,
|
||||
'status': 'pending'
|
||||
}
|
||||
)
|
||||
},
|
||||
'/v2/images/{image}/members/{mem}'.format(image=IMAGE, mem=MEMBER): {
|
||||
'DELETE': (
|
||||
{},
|
||||
None,
|
||||
),
|
||||
'PUT': (
|
||||
{},
|
||||
{
|
||||
'image_id': IMAGE,
|
||||
'member_id': MEMBER,
|
||||
'status': 'accepted'
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
schema_fixtures = {
|
||||
'member': {
|
||||
'GET': (
|
||||
{},
|
||||
{
|
||||
'name': 'member',
|
||||
'properties': {
|
||||
'image_id': {},
|
||||
'member_id': {}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestController(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestController, self).setUp()
|
||||
self.api = utils.FakeAPI(data_fixtures)
|
||||
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
|
||||
self.controller = base.BaseController(self.api, self.schema_api,
|
||||
image_members.Controller)
|
||||
|
||||
def test_list_image_members(self):
|
||||
image_id = IMAGE
|
||||
image_members = self.controller.list(image_id)
|
||||
self.assertEqual(IMAGE, image_members[0].image_id)
|
||||
self.assertEqual(MEMBER, image_members[0].member_id)
|
||||
|
||||
def test_delete_image_member(self):
|
||||
image_id = IMAGE
|
||||
member_id = MEMBER
|
||||
self.controller.delete(image_id, member_id)
|
||||
expect = [
|
||||
('DELETE',
|
||||
'/v2/images/{image}/members/{mem}'.format(image=IMAGE,
|
||||
mem=MEMBER),
|
||||
{},
|
||||
None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_update_image_members(self):
|
||||
image_id = IMAGE
|
||||
member_id = MEMBER
|
||||
status = 'accepted'
|
||||
image_member = self.controller.update(image_id, member_id, status)
|
||||
self.assertEqual(IMAGE, image_member.image_id)
|
||||
self.assertEqual(MEMBER, image_member.member_id)
|
||||
self.assertEqual(status, image_member.status)
|
||||
|
||||
def test_create_image_members(self):
|
||||
image_id = IMAGE
|
||||
member_id = MEMBER
|
||||
status = 'pending'
|
||||
image_member = self.controller.create(image_id, member_id)
|
||||
self.assertEqual(IMAGE, image_member.image_id)
|
||||
self.assertEqual(MEMBER, image_member.member_id)
|
||||
self.assertEqual(status, image_member.status)
|
@ -1,680 +0,0 @@
|
||||
# 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 testtools
|
||||
|
||||
from glanceclient.tests.unit.v2 import base
|
||||
from glanceclient.tests import utils
|
||||
from glanceclient.v2 import metadefs
|
||||
|
||||
NAMESPACE1 = 'Namespace1'
|
||||
NAMESPACE2 = 'Namespace2'
|
||||
NAMESPACE3 = 'Namespace3'
|
||||
NAMESPACE4 = 'Namespace4'
|
||||
NAMESPACE5 = 'Namespace5'
|
||||
NAMESPACE6 = 'Namespace6'
|
||||
NAMESPACE7 = 'Namespace7'
|
||||
NAMESPACE8 = 'Namespace8'
|
||||
NAMESPACENEW = 'NamespaceNew'
|
||||
RESOURCE_TYPE1 = 'ResourceType1'
|
||||
RESOURCE_TYPE2 = 'ResourceType2'
|
||||
OBJECT1 = 'Object1'
|
||||
PROPERTY1 = 'Property1'
|
||||
PROPERTY2 = 'Property2'
|
||||
|
||||
|
||||
def _get_namespace_fixture(ns_name, rt_name=RESOURCE_TYPE1, **kwargs):
|
||||
ns = {
|
||||
"display_name": "Flavor Quota",
|
||||
"description": "DESCRIPTION1",
|
||||
"self": "/v2/metadefs/namespaces/%s" % ns_name,
|
||||
"namespace": ns_name,
|
||||
"visibility": "public",
|
||||
"protected": True,
|
||||
"owner": "admin",
|
||||
"resource_types": [
|
||||
{
|
||||
"name": rt_name
|
||||
}
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespace",
|
||||
"created_at": "2014-08-14T09:07:06Z",
|
||||
"updated_at": "2014-08-14T09:07:06Z",
|
||||
}
|
||||
|
||||
ns.update(kwargs)
|
||||
|
||||
return ns
|
||||
|
||||
data_fixtures = {
|
||||
"/v2/metadefs/namespaces?limit=20": {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"first": "/v2/metadefs/namespaces?limit=20",
|
||||
"namespaces": [
|
||||
_get_namespace_fixture(NAMESPACE1),
|
||||
_get_namespace_fixture(NAMESPACE2),
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespaces"
|
||||
}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces?limit=1": {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"first": "/v2/metadefs/namespaces?limit=1",
|
||||
"namespaces": [
|
||||
_get_namespace_fixture(NAMESPACE7),
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespaces",
|
||||
"next": "/v2/metadefs/namespaces?marker=%s&limit=1"
|
||||
% NAMESPACE7,
|
||||
}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces?limit=1&marker=%s" % NAMESPACE7: {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"first": "/v2/metadefs/namespaces?limit=2",
|
||||
"namespaces": [
|
||||
_get_namespace_fixture(NAMESPACE8),
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespaces"
|
||||
}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces?limit=2&marker=%s" % NAMESPACE6: {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"first": "/v2/metadefs/namespaces?limit=2",
|
||||
"namespaces": [
|
||||
_get_namespace_fixture(NAMESPACE7),
|
||||
_get_namespace_fixture(NAMESPACE8),
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespaces"
|
||||
}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces?limit=20&sort_dir=asc": {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"first": "/v2/metadefs/namespaces?limit=1",
|
||||
"namespaces": [
|
||||
_get_namespace_fixture(NAMESPACE1),
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespaces"
|
||||
}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces?limit=20&sort_key=created_at": {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"first": "/v2/metadefs/namespaces?limit=1",
|
||||
"namespaces": [
|
||||
_get_namespace_fixture(NAMESPACE1),
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespaces"
|
||||
}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces?limit=20&resource_types=%s" % RESOURCE_TYPE1: {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"first": "/v2/metadefs/namespaces?limit=20",
|
||||
"namespaces": [
|
||||
_get_namespace_fixture(NAMESPACE3),
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespaces"
|
||||
}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces?limit=20&resource_types="
|
||||
"%s%%2C%s" % (RESOURCE_TYPE1, RESOURCE_TYPE2): {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"first": "/v2/metadefs/namespaces?limit=20",
|
||||
"namespaces": [
|
||||
_get_namespace_fixture(NAMESPACE4),
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespaces"
|
||||
}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces?limit=20&visibility=private": {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"first": "/v2/metadefs/namespaces?limit=20",
|
||||
"namespaces": [
|
||||
_get_namespace_fixture(NAMESPACE5),
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespaces"
|
||||
}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces": {
|
||||
"POST": (
|
||||
{},
|
||||
{
|
||||
"display_name": "Flavor Quota",
|
||||
"description": "DESCRIPTION1",
|
||||
"self": "/v2/metadefs/namespaces/%s" % 'NamespaceNew',
|
||||
"namespace": 'NamespaceNew',
|
||||
"visibility": "public",
|
||||
"protected": True,
|
||||
"owner": "admin",
|
||||
"schema": "/v2/schemas/metadefs/namespace",
|
||||
"created_at": "2014-08-14T09:07:06Z",
|
||||
"updated_at": "2014-08-14T09:07:06Z",
|
||||
}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces/%s" % NAMESPACE1: {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"display_name": "Flavor Quota",
|
||||
"description": "DESCRIPTION1",
|
||||
"objects": [
|
||||
{
|
||||
"description": "DESCRIPTION2",
|
||||
"name": "OBJECT1",
|
||||
"self": "/v2/metadefs/namespaces/%s/objects/" %
|
||||
OBJECT1,
|
||||
"required": [],
|
||||
"properties": {
|
||||
PROPERTY1: {
|
||||
"type": "integer",
|
||||
"description": "DESCRIPTION3",
|
||||
"title": "Quota: CPU Shares"
|
||||
},
|
||||
PROPERTY2: {
|
||||
"minimum": 1000,
|
||||
"type": "integer",
|
||||
"description": "DESCRIPTION4",
|
||||
"maximum": 1000000,
|
||||
"title": "Quota: CPU Period"
|
||||
},
|
||||
},
|
||||
"schema": "/v2/schemas/metadefs/object"
|
||||
}
|
||||
],
|
||||
"self": "/v2/metadefs/namespaces/%s" % NAMESPACE1,
|
||||
"namespace": NAMESPACE1,
|
||||
"visibility": "public",
|
||||
"protected": True,
|
||||
"owner": "admin",
|
||||
"resource_types": [
|
||||
{
|
||||
"name": RESOURCE_TYPE1
|
||||
}
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespace",
|
||||
"created_at": "2014-08-14T09:07:06Z",
|
||||
"updated_at": "2014-08-14T09:07:06Z",
|
||||
}
|
||||
),
|
||||
"PUT": (
|
||||
{},
|
||||
{
|
||||
"display_name": "Flavor Quota",
|
||||
"description": "DESCRIPTION1",
|
||||
"objects": [
|
||||
{
|
||||
"description": "DESCRIPTION2",
|
||||
"name": "OBJECT1",
|
||||
"self": "/v2/metadefs/namespaces/%s/objects/" %
|
||||
OBJECT1,
|
||||
"required": [],
|
||||
"properties": {
|
||||
PROPERTY1: {
|
||||
"type": "integer",
|
||||
"description": "DESCRIPTION3",
|
||||
"title": "Quota: CPU Shares"
|
||||
},
|
||||
PROPERTY2: {
|
||||
"minimum": 1000,
|
||||
"type": "integer",
|
||||
"description": "DESCRIPTION4",
|
||||
"maximum": 1000000,
|
||||
"title": "Quota: CPU Period"
|
||||
},
|
||||
},
|
||||
"schema": "/v2/schemas/metadefs/object"
|
||||
}
|
||||
],
|
||||
"self": "/v2/metadefs/namespaces/%s" % NAMESPACENEW,
|
||||
"namespace": NAMESPACENEW,
|
||||
"visibility": "public",
|
||||
"protected": True,
|
||||
"owner": "admin",
|
||||
"resource_types": [
|
||||
{
|
||||
"name": RESOURCE_TYPE1
|
||||
}
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespace",
|
||||
"created_at": "2014-08-14T09:07:06Z",
|
||||
"updated_at": "2014-08-14T09:07:06Z",
|
||||
}
|
||||
),
|
||||
"DELETE": (
|
||||
{},
|
||||
{}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces/%s?resource_type=%s" % (NAMESPACE6,
|
||||
RESOURCE_TYPE1):
|
||||
{
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"display_name": "Flavor Quota",
|
||||
"description": "DESCRIPTION1",
|
||||
"objects": [],
|
||||
"self": "/v2/metadefs/namespaces/%s" % NAMESPACE1,
|
||||
"namespace": NAMESPACE6,
|
||||
"visibility": "public",
|
||||
"protected": True,
|
||||
"owner": "admin",
|
||||
"resource_types": [
|
||||
{
|
||||
"name": RESOURCE_TYPE1
|
||||
}
|
||||
],
|
||||
"schema": "/v2/schemas/metadefs/namespace",
|
||||
"created_at": "2014-08-14T09:07:06Z",
|
||||
"updated_at": "2014-08-14T09:07:06Z",
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
schema_fixtures = {
|
||||
"metadefs/namespace":
|
||||
{
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"additionalProperties": False,
|
||||
"definitions": {
|
||||
"property": {
|
||||
"additionalProperties": {
|
||||
"required": [
|
||||
"title",
|
||||
"type"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"additionalItems": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enum": {
|
||||
"type": "array"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": {},
|
||||
"minLength": {
|
||||
"$ref": "#/definitions/"
|
||||
"positiveIntegerDefault0"
|
||||
},
|
||||
"required": {
|
||||
"$ref": "#/definitions/stringArray"
|
||||
},
|
||||
"maximum": {
|
||||
"type": "number"
|
||||
},
|
||||
"minItems": {
|
||||
"$ref": "#/definitions/"
|
||||
"positiveIntegerDefault0"
|
||||
},
|
||||
"readonly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"minimum": {
|
||||
"type": "number"
|
||||
},
|
||||
"maxItems": {
|
||||
"$ref": "#/definitions/"
|
||||
"positiveInteger"
|
||||
},
|
||||
"maxLength": {
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
},
|
||||
"uniqueItems": {
|
||||
"default": False,
|
||||
"type": "boolean"
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"format": "regex"
|
||||
},
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enum": {
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"array",
|
||||
"boolean",
|
||||
"integer",
|
||||
"number",
|
||||
"object",
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"array",
|
||||
"boolean",
|
||||
"integer",
|
||||
"number",
|
||||
"object",
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"positiveIntegerDefault0": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
},
|
||||
{
|
||||
"default": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"stringArray": {
|
||||
"uniqueItems": True,
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"positiveInteger": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"namespace"
|
||||
],
|
||||
"name": "namespace",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Provides a user friendly description "
|
||||
"of the namespace.",
|
||||
"maxLength": 500
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": "Date and time of the last namespace "
|
||||
"modification",
|
||||
"format": "date-time"
|
||||
},
|
||||
"visibility": {
|
||||
"enum": [
|
||||
"public",
|
||||
"private"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Scope of namespace accessibility."
|
||||
},
|
||||
"self": {
|
||||
"type": "string"
|
||||
},
|
||||
"objects": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"$ref": "#/definitions/property"
|
||||
},
|
||||
"required": {
|
||||
"$ref": "#/definitions/stringArray"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Owner of the namespace.",
|
||||
"maxLength": 255
|
||||
},
|
||||
"resource_types": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"metadata_type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/property"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"description": "The user friendly name for the "
|
||||
"namespace. Used by UI if available.",
|
||||
"maxLength": 80
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": "Date and time of namespace creation ",
|
||||
"format": "date-time"
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "The unique namespace text.",
|
||||
"maxLength": 80
|
||||
},
|
||||
"protected": {
|
||||
"type": "boolean",
|
||||
"description": "If true, namespace will not be "
|
||||
"deletable."
|
||||
},
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestNamespaceController(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestNamespaceController, self).setUp()
|
||||
self.api = utils.FakeAPI(data_fixtures)
|
||||
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
|
||||
self.controller = base.BaseController(self.api, self.schema_api,
|
||||
metadefs.NamespaceController)
|
||||
|
||||
def test_list_namespaces(self):
|
||||
namespaces = self.controller.list()
|
||||
self.assertEqual(2, len(namespaces))
|
||||
self.assertEqual(NAMESPACE1, namespaces[0]['namespace'])
|
||||
self.assertEqual(NAMESPACE2, namespaces[1]['namespace'])
|
||||
|
||||
def test_list_namespaces_paginate(self):
|
||||
namespaces = self.controller.list(page_size=1)
|
||||
self.assertEqual(2, len(namespaces))
|
||||
self.assertEqual(NAMESPACE7, namespaces[0]['namespace'])
|
||||
self.assertEqual(NAMESPACE8, namespaces[1]['namespace'])
|
||||
|
||||
def test_list_with_limit_greater_than_page_size(self):
|
||||
namespaces = self.controller.list(page_size=1, limit=2)
|
||||
self.assertEqual(2, len(namespaces))
|
||||
self.assertEqual(NAMESPACE7, namespaces[0]['namespace'])
|
||||
self.assertEqual(NAMESPACE8, namespaces[1]['namespace'])
|
||||
|
||||
def test_list_with_marker(self):
|
||||
namespaces = self.controller.list(marker=NAMESPACE6, page_size=2)
|
||||
self.assertEqual(2, len(namespaces))
|
||||
self.assertEqual(NAMESPACE7, namespaces[0]['namespace'])
|
||||
self.assertEqual(NAMESPACE8, namespaces[1]['namespace'])
|
||||
|
||||
def test_list_with_sort_dir(self):
|
||||
namespaces = self.controller.list(sort_dir='asc', limit=1)
|
||||
self.assertEqual(1, len(namespaces))
|
||||
self.assertEqual(NAMESPACE1, namespaces[0]['namespace'])
|
||||
|
||||
def test_list_with_sort_dir_invalid(self):
|
||||
# NOTE(TravT): The clients work by returning an iterator.
|
||||
# Invoking the iterator is what actually executes the logic.
|
||||
self.assertRaises(ValueError, self.controller.list, sort_dir='foo')
|
||||
|
||||
def test_list_with_sort_key(self):
|
||||
namespaces = self.controller.list(sort_key='created_at', limit=1)
|
||||
self.assertEqual(1, len(namespaces))
|
||||
self.assertEqual(NAMESPACE1, namespaces[0]['namespace'])
|
||||
|
||||
def test_list_with_sort_key_invalid(self):
|
||||
# NOTE(TravT): The clients work by returning an iterator.
|
||||
# Invoking the iterator is what actually executes the logic.
|
||||
self.assertRaises(ValueError, self.controller.list, sort_key='foo')
|
||||
|
||||
def test_list_namespaces_with_one_resource_type_filter(self):
|
||||
namespaces = self.controller.list(
|
||||
filters={
|
||||
'resource_types': [RESOURCE_TYPE1]
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(1, len(namespaces))
|
||||
self.assertEqual(NAMESPACE3, namespaces[0]['namespace'])
|
||||
|
||||
def test_list_namespaces_with_multiple_resource_types_filter(self):
|
||||
namespaces = self.controller.list(
|
||||
filters={
|
||||
'resource_types': [RESOURCE_TYPE1, RESOURCE_TYPE2]
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(1, len(namespaces))
|
||||
self.assertEqual(NAMESPACE4, namespaces[0]['namespace'])
|
||||
|
||||
def test_list_namespaces_with_visibility_filter(self):
|
||||
namespaces = self.controller.list(
|
||||
filters={
|
||||
'visibility': 'private'
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(1, len(namespaces))
|
||||
self.assertEqual(NAMESPACE5, namespaces[0]['namespace'])
|
||||
|
||||
def test_get_namespace(self):
|
||||
namespace = self.controller.get(NAMESPACE1)
|
||||
self.assertEqual(NAMESPACE1, namespace.namespace)
|
||||
self.assertTrue(namespace.protected)
|
||||
|
||||
def test_get_namespace_with_resource_type(self):
|
||||
namespace = self.controller.get(NAMESPACE6,
|
||||
resource_type=RESOURCE_TYPE1)
|
||||
self.assertEqual(NAMESPACE6, namespace.namespace)
|
||||
self.assertTrue(namespace.protected)
|
||||
|
||||
def test_create_namespace(self):
|
||||
properties = {
|
||||
'namespace': NAMESPACENEW
|
||||
}
|
||||
namespace = self.controller.create(**properties)
|
||||
|
||||
self.assertEqual(NAMESPACENEW, namespace.namespace)
|
||||
self.assertTrue(namespace.protected)
|
||||
|
||||
def test_create_namespace_invalid_data(self):
|
||||
properties = {}
|
||||
|
||||
self.assertRaises(TypeError, self.controller.create, **properties)
|
||||
|
||||
def test_create_namespace_invalid_property(self):
|
||||
properties = {'namespace': 'NewNamespace', 'protected': '123'}
|
||||
|
||||
self.assertRaises(TypeError, self.controller.create, **properties)
|
||||
|
||||
def test_update_namespace(self):
|
||||
properties = {'display_name': 'My Updated Name'}
|
||||
namespace = self.controller.update(NAMESPACE1, **properties)
|
||||
|
||||
self.assertEqual(NAMESPACE1, namespace.namespace)
|
||||
|
||||
def test_update_namespace_invalid_property(self):
|
||||
properties = {'protected': '123'}
|
||||
|
||||
self.assertRaises(TypeError, self.controller.update, NAMESPACE1,
|
||||
**properties)
|
||||
|
||||
def test_update_namespace_disallowed_fields(self):
|
||||
properties = {'display_name': 'My Updated Name'}
|
||||
self.controller.update(NAMESPACE1, **properties)
|
||||
actual = self.api.calls
|
||||
_disallowed_fields = ['self', 'schema', 'created_at', 'updated_at']
|
||||
for key in actual[1][3]:
|
||||
self.assertNotIn(key, _disallowed_fields)
|
||||
|
||||
def test_delete_namespace(self):
|
||||
self.controller.delete(NAMESPACE1)
|
||||
expect = [
|
||||
('DELETE',
|
||||
'/v2/metadefs/namespaces/%s' % NAMESPACE1,
|
||||
{},
|
||||
None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
@ -1,342 +0,0 @@
|
||||
# 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 testtools
|
||||
|
||||
from glanceclient.tests.unit.v2 import base
|
||||
from glanceclient.tests import utils
|
||||
from glanceclient.v2 import metadefs
|
||||
|
||||
NAMESPACE1 = 'Namespace1'
|
||||
OBJECT1 = 'Object1'
|
||||
OBJECT2 = 'Object2'
|
||||
OBJECTNEW = 'ObjectNew'
|
||||
PROPERTY1 = 'Property1'
|
||||
PROPERTY2 = 'Property2'
|
||||
PROPERTY3 = 'Property3'
|
||||
PROPERTY4 = 'Property4'
|
||||
|
||||
|
||||
def _get_object_fixture(ns_name, obj_name, **kwargs):
|
||||
obj = {
|
||||
"description": "DESCRIPTION",
|
||||
"name": obj_name,
|
||||
"self": "/v2/metadefs/namespaces/%s/objects/%s" %
|
||||
(ns_name, obj_name),
|
||||
"required": [],
|
||||
"properties": {
|
||||
PROPERTY1: {
|
||||
"type": "integer",
|
||||
"description": "DESCRIPTION",
|
||||
"title": "Quota: CPU Shares"
|
||||
},
|
||||
PROPERTY2: {
|
||||
"minimum": 1000,
|
||||
"type": "integer",
|
||||
"description": "DESCRIPTION",
|
||||
"maximum": 1000000,
|
||||
"title": "Quota: CPU Period"
|
||||
}},
|
||||
"schema": "/v2/schemas/metadefs/object",
|
||||
"created_at": "2014-08-14T09:07:06Z",
|
||||
"updated_at": "2014-08-14T09:07:06Z",
|
||||
}
|
||||
|
||||
obj.update(kwargs)
|
||||
|
||||
return obj
|
||||
|
||||
data_fixtures = {
|
||||
"/v2/metadefs/namespaces/%s/objects" % NAMESPACE1: {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"objects": [
|
||||
_get_object_fixture(NAMESPACE1, OBJECT1),
|
||||
_get_object_fixture(NAMESPACE1, OBJECT2)
|
||||
],
|
||||
"schema": "v2/schemas/metadefs/objects"
|
||||
}
|
||||
),
|
||||
"POST": (
|
||||
{},
|
||||
_get_object_fixture(NAMESPACE1, OBJECTNEW)
|
||||
),
|
||||
"DELETE": (
|
||||
{},
|
||||
{}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces/%s/objects/%s" % (NAMESPACE1, OBJECT1): {
|
||||
"GET": (
|
||||
{},
|
||||
_get_object_fixture(NAMESPACE1, OBJECT1)
|
||||
),
|
||||
"PUT": (
|
||||
{},
|
||||
_get_object_fixture(NAMESPACE1, OBJECT1)
|
||||
),
|
||||
"DELETE": (
|
||||
{},
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
schema_fixtures = {
|
||||
"metadefs/object": {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"additionalProperties": False,
|
||||
"definitions": {
|
||||
"property": {
|
||||
"additionalProperties": {
|
||||
"required": [
|
||||
"title",
|
||||
"type"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"additionalItems": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enum": {
|
||||
"type": "array"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": {},
|
||||
"minLength": {
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
"Default0"
|
||||
},
|
||||
"required": {
|
||||
"$ref": "#/definitions/stringArray"
|
||||
},
|
||||
"maximum": {
|
||||
"type": "number"
|
||||
},
|
||||
"minItems": {
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
"Default0"
|
||||
},
|
||||
"readonly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"minimum": {
|
||||
"type": "number"
|
||||
},
|
||||
"maxItems": {
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
},
|
||||
"maxLength": {
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
},
|
||||
"uniqueItems": {
|
||||
"default": False,
|
||||
"type": "boolean"
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"format": "regex"
|
||||
},
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enum": {
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"array",
|
||||
"boolean",
|
||||
"integer",
|
||||
"number",
|
||||
"object",
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"array",
|
||||
"boolean",
|
||||
"integer",
|
||||
"number",
|
||||
"object",
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"positiveIntegerDefault0": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
},
|
||||
{
|
||||
"default": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"stringArray": {
|
||||
"uniqueItems": True,
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"positiveInteger": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"name": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": "Date and time of object creation ",
|
||||
"format": "date-time"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"self": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": {
|
||||
"$ref": "#/definitions/stringArray"
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/property"
|
||||
},
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": "Date and time of the last object "
|
||||
"modification",
|
||||
"format": "date-time"
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestObjectController(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestObjectController, self).setUp()
|
||||
self.api = utils.FakeAPI(data_fixtures)
|
||||
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
|
||||
self.controller = base.BaseController(self.api, self.schema_api,
|
||||
metadefs.ObjectController)
|
||||
|
||||
def test_list_object(self):
|
||||
objects = self.controller.list(NAMESPACE1)
|
||||
actual = [obj.name for obj in objects]
|
||||
self.assertEqual([OBJECT1, OBJECT2], actual)
|
||||
|
||||
def test_get_object(self):
|
||||
obj = self.controller.get(NAMESPACE1, OBJECT1)
|
||||
self.assertEqual(OBJECT1, obj.name)
|
||||
self.assertEqual(sorted([PROPERTY1, PROPERTY2]),
|
||||
sorted(list(obj.properties.keys())))
|
||||
|
||||
def test_create_object(self):
|
||||
properties = {
|
||||
'name': OBJECTNEW,
|
||||
'description': 'DESCRIPTION'
|
||||
}
|
||||
obj = self.controller.create(NAMESPACE1, **properties)
|
||||
self.assertEqual(OBJECTNEW, obj.name)
|
||||
|
||||
def test_create_object_invalid_property(self):
|
||||
properties = {
|
||||
'namespace': NAMESPACE1
|
||||
}
|
||||
self.assertRaises(TypeError, self.controller.create, **properties)
|
||||
|
||||
def test_update_object(self):
|
||||
properties = {
|
||||
'description': 'UPDATED_DESCRIPTION'
|
||||
}
|
||||
obj = self.controller.update(NAMESPACE1, OBJECT1, **properties)
|
||||
self.assertEqual(OBJECT1, obj.name)
|
||||
|
||||
def test_update_object_invalid_property(self):
|
||||
properties = {
|
||||
'required': 'INVALID'
|
||||
}
|
||||
self.assertRaises(TypeError, self.controller.update, NAMESPACE1,
|
||||
OBJECT1, **properties)
|
||||
|
||||
def test_update_object_disallowed_fields(self):
|
||||
properties = {
|
||||
'description': 'UPDATED_DESCRIPTION'
|
||||
}
|
||||
self.controller.update(NAMESPACE1, OBJECT1, **properties)
|
||||
actual = self.api.calls
|
||||
# API makes three calls(GET, PUT, GET) for object update.
|
||||
# PUT has the request body in the list
|
||||
'''('PUT', '/v2/metadefs/namespaces/Namespace1/objects/Object1', {},
|
||||
[('description', 'UPDATED_DESCRIPTION'),
|
||||
('name', 'Object1'),
|
||||
('properties', ...),
|
||||
('required', [])])'''
|
||||
|
||||
_disallowed_fields = ['self', 'schema', 'created_at', 'updated_at']
|
||||
for key in actual[1][3]:
|
||||
self.assertNotIn(key, _disallowed_fields)
|
||||
|
||||
def test_delete_object(self):
|
||||
self.controller.delete(NAMESPACE1, OBJECT1)
|
||||
expect = [
|
||||
('DELETE',
|
||||
'/v2/metadefs/namespaces/%s/objects/%s' % (NAMESPACE1, OBJECT1),
|
||||
{},
|
||||
None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_delete_all_objects(self):
|
||||
self.controller.delete_all(NAMESPACE1)
|
||||
expect = [
|
||||
('DELETE',
|
||||
'/v2/metadefs/namespaces/%s/objects' % NAMESPACE1,
|
||||
{},
|
||||
None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
@ -1,310 +0,0 @@
|
||||
# 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 testtools
|
||||
|
||||
from glanceclient.tests.unit.v2 import base
|
||||
from glanceclient.tests import utils
|
||||
from glanceclient.v2 import metadefs
|
||||
|
||||
NAMESPACE1 = 'Namespace1'
|
||||
PROPERTY1 = 'Property1'
|
||||
PROPERTY2 = 'Property2'
|
||||
PROPERTYNEW = 'PropertyNew'
|
||||
|
||||
data_fixtures = {
|
||||
"/v2/metadefs/namespaces/%s/properties" % NAMESPACE1: {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"properties": {
|
||||
PROPERTY1: {
|
||||
"default": "1",
|
||||
"type": "integer",
|
||||
"description": "Number of cores.",
|
||||
"title": "cores"
|
||||
},
|
||||
PROPERTY2: {
|
||||
"items": {
|
||||
"enum": [
|
||||
"Intel",
|
||||
"AMD"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "Specifies the CPU manufacturer.",
|
||||
"title": "Vendor"
|
||||
},
|
||||
}
|
||||
}
|
||||
),
|
||||
"POST": (
|
||||
{},
|
||||
{
|
||||
"items": {
|
||||
"enum": [
|
||||
"Intel",
|
||||
"AMD"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "UPDATED_DESCRIPTION",
|
||||
"title": "Vendor",
|
||||
"name": PROPERTYNEW
|
||||
}
|
||||
),
|
||||
"DELETE": (
|
||||
{},
|
||||
{}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces/%s/properties/%s" % (NAMESPACE1, PROPERTY1): {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"items": {
|
||||
"enum": [
|
||||
"Intel",
|
||||
"AMD"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "Specifies the CPU manufacturer.",
|
||||
"title": "Vendor"
|
||||
}
|
||||
),
|
||||
"PUT": (
|
||||
{},
|
||||
{
|
||||
"items": {
|
||||
"enum": [
|
||||
"Intel",
|
||||
"AMD"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "UPDATED_DESCRIPTION",
|
||||
"title": "Vendor"
|
||||
}
|
||||
),
|
||||
"DELETE": (
|
||||
{},
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
schema_fixtures = {
|
||||
"metadefs/property": {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"additionalProperties": False,
|
||||
"definitions": {
|
||||
"positiveIntegerDefault0": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
},
|
||||
{
|
||||
"default": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"stringArray": {
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"uniqueItems": True,
|
||||
"type": "array"
|
||||
},
|
||||
"positiveInteger": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"title",
|
||||
"type"
|
||||
],
|
||||
"name": "property",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"minLength": {
|
||||
"$ref": "#/definitions/positiveIntegerDefault0"
|
||||
},
|
||||
"enum": {
|
||||
"type": "array"
|
||||
},
|
||||
"minimum": {
|
||||
"type": "number"
|
||||
},
|
||||
"maxItems": {
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
},
|
||||
"maxLength": {
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
},
|
||||
"uniqueItems": {
|
||||
"default": False,
|
||||
"type": "boolean"
|
||||
},
|
||||
"additionalItems": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": {},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"format": "regex"
|
||||
},
|
||||
"required": {
|
||||
"$ref": "#/definitions/stringArray"
|
||||
},
|
||||
"maximum": {
|
||||
"type": "number"
|
||||
},
|
||||
"minItems": {
|
||||
"$ref": "#/definitions/positiveIntegerDefault0"
|
||||
},
|
||||
"readonly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enum": {
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"array",
|
||||
"boolean",
|
||||
"integer",
|
||||
"number",
|
||||
"object",
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"array",
|
||||
"boolean",
|
||||
"integer",
|
||||
"number",
|
||||
"object",
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestPropertyController(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestPropertyController, self).setUp()
|
||||
self.api = utils.FakeAPI(data_fixtures)
|
||||
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
|
||||
self.controller = base.BaseController(self.api, self.schema_api,
|
||||
metadefs.PropertyController)
|
||||
|
||||
def test_list_property(self):
|
||||
properties = self.controller.list(NAMESPACE1)
|
||||
actual = [prop.name for prop in properties]
|
||||
self.assertEqual(sorted([PROPERTY1, PROPERTY2]), sorted(actual))
|
||||
|
||||
def test_get_property(self):
|
||||
prop = self.controller.get(NAMESPACE1, PROPERTY1)
|
||||
self.assertEqual(PROPERTY1, prop.name)
|
||||
|
||||
def test_create_property(self):
|
||||
properties = {
|
||||
'name': PROPERTYNEW,
|
||||
'title': 'TITLE',
|
||||
'type': 'string'
|
||||
}
|
||||
obj = self.controller.create(NAMESPACE1, **properties)
|
||||
self.assertEqual(PROPERTYNEW, obj.name)
|
||||
|
||||
def test_create_property_invalid_property(self):
|
||||
properties = {
|
||||
'namespace': NAMESPACE1
|
||||
}
|
||||
self.assertRaises(TypeError, self.controller.create, **properties)
|
||||
|
||||
def test_update_property(self):
|
||||
properties = {
|
||||
'description': 'UPDATED_DESCRIPTION'
|
||||
}
|
||||
prop = self.controller.update(NAMESPACE1, PROPERTY1, **properties)
|
||||
self.assertEqual(PROPERTY1, prop.name)
|
||||
|
||||
def test_update_property_invalid_property(self):
|
||||
properties = {
|
||||
'type': 'INVALID'
|
||||
}
|
||||
self.assertRaises(TypeError, self.controller.update, NAMESPACE1,
|
||||
PROPERTY1, **properties)
|
||||
|
||||
def test_update_property_disallowed_fields(self):
|
||||
properties = {
|
||||
'description': 'UPDATED_DESCRIPTION'
|
||||
}
|
||||
self.controller.update(NAMESPACE1, PROPERTY1, **properties)
|
||||
actual = self.api.calls
|
||||
_disallowed_fields = ['created_at', 'updated_at']
|
||||
for key in actual[1][3]:
|
||||
self.assertNotIn(key, _disallowed_fields)
|
||||
|
||||
def test_delete_property(self):
|
||||
self.controller.delete(NAMESPACE1, PROPERTY1)
|
||||
expect = [
|
||||
('DELETE',
|
||||
'/v2/metadefs/namespaces/%s/properties/%s' % (NAMESPACE1,
|
||||
PROPERTY1),
|
||||
{},
|
||||
None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_delete_all_properties(self):
|
||||
self.controller.delete_all(NAMESPACE1)
|
||||
expect = [
|
||||
('DELETE',
|
||||
'/v2/metadefs/namespaces/%s/properties' % NAMESPACE1,
|
||||
{},
|
||||
None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
@ -1,187 +0,0 @@
|
||||
# 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 testtools
|
||||
|
||||
from glanceclient.tests.unit.v2 import base
|
||||
from glanceclient.tests import utils
|
||||
from glanceclient.v2 import metadefs
|
||||
|
||||
NAMESPACE1 = 'Namespace1'
|
||||
RESOURCE_TYPE1 = 'ResourceType1'
|
||||
RESOURCE_TYPE2 = 'ResourceType2'
|
||||
RESOURCE_TYPE3 = 'ResourceType3'
|
||||
RESOURCE_TYPE4 = 'ResourceType4'
|
||||
RESOURCE_TYPENEW = 'ResourceTypeNew'
|
||||
|
||||
|
||||
data_fixtures = {
|
||||
"/v2/metadefs/namespaces/%s/resource_types" % NAMESPACE1: {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"resource_type_associations": [
|
||||
{
|
||||
"name": RESOURCE_TYPE3,
|
||||
"created_at": "2014-08-14T09:07:06Z",
|
||||
"updated_at": "2014-08-14T09:07:06Z",
|
||||
},
|
||||
{
|
||||
"name": RESOURCE_TYPE4,
|
||||
"prefix": "PREFIX:",
|
||||
"created_at": "2014-08-14T09:07:06Z",
|
||||
"updated_at": "2014-08-14T09:07:06Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
"POST": (
|
||||
{},
|
||||
{
|
||||
"name": RESOURCE_TYPENEW,
|
||||
"prefix": "PREFIX:",
|
||||
"created_at": "2014-08-14T09:07:06Z",
|
||||
"updated_at": "2014-08-14T09:07:06Z",
|
||||
}
|
||||
),
|
||||
},
|
||||
"/v2/metadefs/namespaces/%s/resource_types/%s" % (NAMESPACE1,
|
||||
RESOURCE_TYPE1):
|
||||
{
|
||||
"DELETE": (
|
||||
{},
|
||||
{}
|
||||
),
|
||||
},
|
||||
"/v2/metadefs/resource_types": {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"resource_types": [
|
||||
{
|
||||
"name": RESOURCE_TYPE1,
|
||||
"created_at": "2014-08-14T09:07:06Z",
|
||||
"updated_at": "2014-08-14T09:07:06Z",
|
||||
},
|
||||
{
|
||||
"name": RESOURCE_TYPE2,
|
||||
"created_at": "2014-08-14T09:07:06Z",
|
||||
"updated_at": "2014-08-14T09:07:06Z",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
schema_fixtures = {
|
||||
"metadefs/resource_type": {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"name": "resource_type",
|
||||
"properties": {
|
||||
"prefix": {
|
||||
"type": "string",
|
||||
"description": "Specifies the prefix to use for the "
|
||||
"given resource type. Any properties "
|
||||
"in the namespace should be prefixed "
|
||||
"with this prefix when being applied "
|
||||
"to the specified resource type. Must "
|
||||
"include prefix separator (e.g. a "
|
||||
"colon :).",
|
||||
"maxLength": 80
|
||||
},
|
||||
"properties_target": {
|
||||
"type": "string",
|
||||
"description": "Some resource types allow more than "
|
||||
"one key / value pair per instance. "
|
||||
"For example, Cinder allows user and "
|
||||
"image metadata on volumes. Only the "
|
||||
"image properties metadata is "
|
||||
"evaluated by Nova (scheduling or "
|
||||
"drivers). This property allows a "
|
||||
"namespace target to remove the "
|
||||
"ambiguity.",
|
||||
"maxLength": 80
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Resource type names should be "
|
||||
"aligned with Heat resource types "
|
||||
"whenever possible: http://docs."
|
||||
"openstack.org/developer/heat/"
|
||||
"template_guide/openstack.html",
|
||||
"maxLength": 80
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": "Date and time of resource type "
|
||||
"association",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": "Date and time of the last resource "
|
||||
"type association modification ",
|
||||
"format": "date-time"
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestResoureTypeController(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestResoureTypeController, self).setUp()
|
||||
self.api = utils.FakeAPI(data_fixtures)
|
||||
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
|
||||
self.controller = base.BaseResourceTypeController(
|
||||
self.api, self.schema_api, metadefs.ResourceTypeController)
|
||||
|
||||
def test_list_resource_types(self):
|
||||
resource_types = self.controller.list()
|
||||
names = [rt.name for rt in resource_types]
|
||||
self.assertEqual([RESOURCE_TYPE1, RESOURCE_TYPE2], names)
|
||||
|
||||
def test_get_resource_types(self):
|
||||
resource_types = self.controller.get(NAMESPACE1)
|
||||
self.assertEqual([RESOURCE_TYPE3, RESOURCE_TYPE4], resource_types)
|
||||
|
||||
def test_associate_resource_types(self):
|
||||
resource_types = self.controller.associate(NAMESPACE1,
|
||||
name=RESOURCE_TYPENEW)
|
||||
|
||||
self.assertEqual(RESOURCE_TYPENEW, resource_types['name'])
|
||||
|
||||
def test_associate_resource_types_invalid_property(self):
|
||||
longer = '1234' * 50
|
||||
properties = {'name': RESOURCE_TYPENEW, 'prefix': longer}
|
||||
self.assertRaises(TypeError, self.controller.associate, NAMESPACE1,
|
||||
**properties)
|
||||
|
||||
def test_deassociate_resource_types(self):
|
||||
self.controller.deassociate(NAMESPACE1, RESOURCE_TYPE1)
|
||||
expect = [
|
||||
('DELETE',
|
||||
'/v2/metadefs/namespaces/%s/resource_types/%s' % (NAMESPACE1,
|
||||
RESOURCE_TYPE1),
|
||||
{},
|
||||
None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
@ -1,184 +0,0 @@
|
||||
# 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.
|
||||
|
||||
import testtools
|
||||
|
||||
from glanceclient.tests.unit.v2 import base
|
||||
from glanceclient.tests import utils
|
||||
from glanceclient.v2 import metadefs
|
||||
|
||||
NAMESPACE1 = 'Namespace1'
|
||||
TAG1 = 'Tag1'
|
||||
TAG2 = 'Tag2'
|
||||
TAGNEW1 = 'TagNew1'
|
||||
TAGNEW2 = 'TagNew2'
|
||||
TAGNEW3 = 'TagNew3'
|
||||
|
||||
|
||||
def _get_tag_fixture(tag_name, **kwargs):
|
||||
tag = {
|
||||
"name": tag_name
|
||||
}
|
||||
tag.update(kwargs)
|
||||
return tag
|
||||
|
||||
|
||||
data_fixtures = {
|
||||
"/v2/metadefs/namespaces/%s/tags" % NAMESPACE1: {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"tags": [
|
||||
_get_tag_fixture(TAG1),
|
||||
_get_tag_fixture(TAG2)
|
||||
]
|
||||
}
|
||||
),
|
||||
"POST": (
|
||||
{},
|
||||
{
|
||||
'tags': [
|
||||
_get_tag_fixture(TAGNEW2),
|
||||
_get_tag_fixture(TAGNEW3)
|
||||
]
|
||||
}
|
||||
),
|
||||
"DELETE": (
|
||||
{},
|
||||
{}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW1): {
|
||||
"POST": (
|
||||
{},
|
||||
_get_tag_fixture(TAGNEW1)
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAG1): {
|
||||
"GET": (
|
||||
{},
|
||||
_get_tag_fixture(TAG1)
|
||||
),
|
||||
"PUT": (
|
||||
{},
|
||||
_get_tag_fixture(TAG2)
|
||||
),
|
||||
"DELETE": (
|
||||
{},
|
||||
{}
|
||||
)
|
||||
},
|
||||
"/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAG2): {
|
||||
"GET": (
|
||||
{},
|
||||
_get_tag_fixture(TAG2)
|
||||
),
|
||||
},
|
||||
"/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW2): {
|
||||
"GET": (
|
||||
{},
|
||||
_get_tag_fixture(TAGNEW2)
|
||||
),
|
||||
},
|
||||
"/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW3): {
|
||||
"GET": (
|
||||
{},
|
||||
_get_tag_fixture(TAGNEW3)
|
||||
),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
schema_fixtures = {
|
||||
"metadefs/tag": {
|
||||
"GET": (
|
||||
{},
|
||||
{
|
||||
"additionalProperties": True,
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": ("Date and time of tag creation"),
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": ("Date and time of the last tag"
|
||||
" modification"),
|
||||
"format": "date-time"
|
||||
},
|
||||
'properties': {}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestTagController(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestTagController, self).setUp()
|
||||
self.api = utils.FakeAPI(data_fixtures)
|
||||
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
|
||||
self.controller = base.BaseController(self.api, self.schema_api,
|
||||
metadefs.TagController)
|
||||
|
||||
def test_list_tag(self):
|
||||
tags = self.controller.list(NAMESPACE1)
|
||||
actual = [tag.name for tag in tags]
|
||||
self.assertEqual([TAG1, TAG2], actual)
|
||||
|
||||
def test_get_tag(self):
|
||||
tag = self.controller.get(NAMESPACE1, TAG1)
|
||||
self.assertEqual(TAG1, tag.name)
|
||||
|
||||
def test_create_tag(self):
|
||||
tag = self.controller.create(NAMESPACE1, TAGNEW1)
|
||||
self.assertEqual(TAGNEW1, tag.name)
|
||||
|
||||
def test_create_multiple_tags(self):
|
||||
properties = {
|
||||
'tags': [TAGNEW2, TAGNEW3]
|
||||
}
|
||||
tags = self.controller.create_multiple(NAMESPACE1, **properties)
|
||||
self.assertEqual([TAGNEW2, TAGNEW3], tags)
|
||||
|
||||
def test_update_tag(self):
|
||||
properties = {
|
||||
'name': TAG2
|
||||
}
|
||||
tag = self.controller.update(NAMESPACE1, TAG1, **properties)
|
||||
self.assertEqual(TAG2, tag.name)
|
||||
|
||||
def test_delete_tag(self):
|
||||
self.controller.delete(NAMESPACE1, TAG1)
|
||||
expect = [
|
||||
('DELETE',
|
||||
'/v2/metadefs/namespaces/%s/tags/%s' % (NAMESPACE1, TAG1),
|
||||
{},
|
||||
None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_delete_all_tags(self):
|
||||
self.controller.delete_all(NAMESPACE1)
|
||||
expect = [
|
||||
('DELETE',
|
||||
'/v2/metadefs/namespaces/%s/tags' % NAMESPACE1,
|
||||
{},
|
||||
None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
@ -1,232 +0,0 @@
|
||||
# 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 jsonpatch
|
||||
import testtools
|
||||
import warlock
|
||||
|
||||
from glanceclient.tests import utils
|
||||
from glanceclient.v2 import schemas
|
||||
|
||||
|
||||
fixtures = {
|
||||
'/v2/schemas': {
|
||||
'GET': (
|
||||
{},
|
||||
{
|
||||
'image': '/v2/schemas/image',
|
||||
'access': '/v2/schemas/image/access',
|
||||
},
|
||||
),
|
||||
},
|
||||
'/v2/schemas/image': {
|
||||
'GET': (
|
||||
{},
|
||||
{
|
||||
'name': 'image',
|
||||
'properties': {
|
||||
'name': {'type': 'string',
|
||||
'description': 'Name of image'},
|
||||
'tags': {'type': 'array'}
|
||||
},
|
||||
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
_SCHEMA = schemas.Schema({
|
||||
'name': 'image',
|
||||
'properties': {
|
||||
'name': {'type': 'string'},
|
||||
'color': {'type': 'string'},
|
||||
'shape': {'type': 'string', 'is_base': False},
|
||||
'tags': {'type': 'array'}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
def compare_json_patches(a, b):
|
||||
"""Return 0 if a and b describe the same JSON patch."""
|
||||
return(jsonpatch.JsonPatch.from_string(a) ==
|
||||
jsonpatch.JsonPatch.from_string(b))
|
||||
|
||||
|
||||
class TestSchemaProperty(testtools.TestCase):
|
||||
def test_property_minimum(self):
|
||||
prop = schemas.SchemaProperty('size')
|
||||
self.assertEqual('size', prop.name)
|
||||
|
||||
def test_property_description(self):
|
||||
prop = schemas.SchemaProperty('size', description='some quantity')
|
||||
self.assertEqual('size', prop.name)
|
||||
self.assertEqual('some quantity', prop.description)
|
||||
|
||||
def test_property_is_base(self):
|
||||
prop1 = schemas.SchemaProperty('name')
|
||||
prop2 = schemas.SchemaProperty('foo', is_base=False)
|
||||
prop3 = schemas.SchemaProperty('foo', is_base=True)
|
||||
self.assertTrue(prop1.is_base)
|
||||
self.assertFalse(prop2.is_base)
|
||||
self.assertTrue(prop3.is_base)
|
||||
|
||||
|
||||
class TestSchema(testtools.TestCase):
|
||||
def test_schema_minimum(self):
|
||||
raw_schema = {'name': 'Country', 'properties': {}}
|
||||
schema = schemas.Schema(raw_schema)
|
||||
self.assertEqual('Country', schema.name)
|
||||
self.assertEqual([], schema.properties)
|
||||
|
||||
def test_schema_with_property(self):
|
||||
raw_schema = {'name': 'Country', 'properties': {'size': {}}}
|
||||
schema = schemas.Schema(raw_schema)
|
||||
self.assertEqual('Country', schema.name)
|
||||
self.assertEqual(['size'], [p.name for p in schema.properties])
|
||||
|
||||
def test_raw(self):
|
||||
raw_schema = {'name': 'Country', 'properties': {}}
|
||||
schema = schemas.Schema(raw_schema)
|
||||
self.assertEqual(raw_schema, schema.raw())
|
||||
|
||||
def test_property_is_base(self):
|
||||
raw_schema = {'name': 'Country',
|
||||
'properties': {
|
||||
'size': {},
|
||||
'population': {'is_base': False}}}
|
||||
schema = schemas.Schema(raw_schema)
|
||||
self.assertTrue(schema.is_base_property('size'))
|
||||
self.assertFalse(schema.is_base_property('population'))
|
||||
self.assertFalse(schema.is_base_property('foo'))
|
||||
|
||||
|
||||
class TestController(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestController, self).setUp()
|
||||
self.api = utils.FakeAPI(fixtures)
|
||||
self.controller = schemas.Controller(self.api)
|
||||
|
||||
def test_get_schema(self):
|
||||
schema = self.controller.get('image')
|
||||
self.assertEqual('image', schema.name)
|
||||
self.assertEqual(set(['name', 'tags']),
|
||||
set([p.name for p in schema.properties]))
|
||||
|
||||
|
||||
class TestSchemaBasedModel(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestSchemaBasedModel, self).setUp()
|
||||
self.model = warlock.model_factory(_SCHEMA.raw(),
|
||||
base_class=schemas.SchemaBasedModel)
|
||||
|
||||
def test_patch_should_replace_missing_core_properties(self):
|
||||
obj = {
|
||||
'name': 'fred'
|
||||
}
|
||||
|
||||
original = self.model(obj)
|
||||
original['color'] = 'red'
|
||||
|
||||
patch = original.patch
|
||||
expected = '[{"path": "/color", "value": "red", "op": "replace"}]'
|
||||
self.assertTrue(compare_json_patches(patch, expected))
|
||||
|
||||
def test_patch_should_add_extra_properties(self):
|
||||
obj = {
|
||||
'name': 'fred',
|
||||
}
|
||||
|
||||
original = self.model(obj)
|
||||
original['weight'] = '10'
|
||||
|
||||
patch = original.patch
|
||||
expected = '[{"path": "/weight", "value": "10", "op": "add"}]'
|
||||
self.assertTrue(compare_json_patches(patch, expected))
|
||||
|
||||
def test_patch_should_replace_extra_properties(self):
|
||||
obj = {
|
||||
'name': 'fred',
|
||||
'weight': '10'
|
||||
}
|
||||
|
||||
original = self.model(obj)
|
||||
original['weight'] = '22'
|
||||
|
||||
patch = original.patch
|
||||
expected = '[{"path": "/weight", "value": "22", "op": "replace"}]'
|
||||
self.assertTrue(compare_json_patches(patch, expected))
|
||||
|
||||
def test_patch_should_remove_extra_properties(self):
|
||||
obj = {
|
||||
'name': 'fred',
|
||||
'weight': '10'
|
||||
}
|
||||
|
||||
original = self.model(obj)
|
||||
del original['weight']
|
||||
|
||||
patch = original.patch
|
||||
expected = '[{"path": "/weight", "op": "remove"}]'
|
||||
self.assertTrue(compare_json_patches(patch, expected))
|
||||
|
||||
def test_patch_should_remove_core_properties(self):
|
||||
obj = {
|
||||
'name': 'fred',
|
||||
'color': 'red'
|
||||
}
|
||||
|
||||
original = self.model(obj)
|
||||
del original['color']
|
||||
|
||||
patch = original.patch
|
||||
expected = '[{"path": "/color", "op": "remove"}]'
|
||||
self.assertTrue(compare_json_patches(patch, expected))
|
||||
|
||||
def test_patch_should_add_missing_custom_properties(self):
|
||||
obj = {
|
||||
'name': 'fred'
|
||||
}
|
||||
|
||||
original = self.model(obj)
|
||||
original['shape'] = 'circle'
|
||||
|
||||
patch = original.patch
|
||||
expected = '[{"path": "/shape", "value": "circle", "op": "add"}]'
|
||||
self.assertTrue(compare_json_patches(patch, expected))
|
||||
|
||||
def test_patch_should_replace_custom_properties(self):
|
||||
obj = {
|
||||
'name': 'fred',
|
||||
'shape': 'circle'
|
||||
}
|
||||
|
||||
original = self.model(obj)
|
||||
original['shape'] = 'square'
|
||||
|
||||
patch = original.patch
|
||||
expected = '[{"path": "/shape", "value": "square", "op": "replace"}]'
|
||||
self.assertTrue(compare_json_patches(patch, expected))
|
||||
|
||||
def test_patch_should_replace_tags(self):
|
||||
obj = {'name': 'fred', }
|
||||
|
||||
original = self.model(obj)
|
||||
original['tags'] = ['tag1', 'tag2']
|
||||
|
||||
patch = original.patch
|
||||
expected = '[{"path": "/tags", "value": ["tag1", "tag2"], ' \
|
||||
'"op": "replace"}]'
|
||||
self.assertTrue(compare_json_patches(patch, expected))
|
File diff suppressed because it is too large
Load Diff
@ -1,83 +0,0 @@
|
||||
# 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 testtools
|
||||
|
||||
from glanceclient.tests.unit.v2 import base
|
||||
from glanceclient.tests import utils
|
||||
from glanceclient.v2 import image_tags
|
||||
|
||||
|
||||
IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1'
|
||||
TAG = 'tag01'
|
||||
|
||||
|
||||
data_fixtures = {
|
||||
'/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, tag_value=TAG): {
|
||||
'DELETE': (
|
||||
{},
|
||||
None,
|
||||
),
|
||||
'PUT': (
|
||||
{},
|
||||
{
|
||||
'image_id': IMAGE,
|
||||
'tag_value': TAG
|
||||
}
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
schema_fixtures = {
|
||||
'tag': {
|
||||
'GET': (
|
||||
{},
|
||||
{'name': 'image', 'properties': {'image_id': {}, 'tags': {}}}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestController(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestController, self).setUp()
|
||||
self.api = utils.FakeAPI(data_fixtures)
|
||||
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
|
||||
self.controller = base.BaseController(self.api, self.schema_api,
|
||||
image_tags.Controller)
|
||||
|
||||
def test_update_image_tag(self):
|
||||
image_id = IMAGE
|
||||
tag_value = TAG
|
||||
self.controller.update(image_id, tag_value)
|
||||
expect = [
|
||||
('PUT',
|
||||
'/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE,
|
||||
tag_value=TAG),
|
||||
{},
|
||||
None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
|
||||
def test_delete_image_tag(self):
|
||||
image_id = IMAGE
|
||||
tag_value = TAG
|
||||
self.controller.delete(image_id, tag_value)
|
||||
expect = [
|
||||
('DELETE',
|
||||
'/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE,
|
||||
tag_value=TAG),
|
||||
{},
|
||||
None)]
|
||||
self.assertEqual(expect, self.api.calls)
|
@ -1,367 +0,0 @@
|
||||
# Copyright 2013 OpenStack Foundation.
|
||||
# Copyright 2013 IBM 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.
|
||||
|
||||
import testtools
|
||||
|
||||
from glanceclient.tests.unit.v2 import base
|
||||
from glanceclient.tests import utils
|
||||
from glanceclient.v2 import tasks
|
||||
|
||||
|
||||
_OWNED_TASK_ID = 'a4963502-acc7-42ba-ad60-5aa0962b7faf'
|
||||
_OWNER_ID = '6bd473f0-79ae-40ad-a927-e07ec37b642f'
|
||||
_FAKE_OWNER_ID = '63e7f218-29de-4477-abdc-8db7c9533188'
|
||||
_PENDING_ID = '3a4560a1-e585-443e-9b39-553b46ec92d1'
|
||||
_PROCESSING_ID = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'
|
||||
|
||||
|
||||
fixtures = {
|
||||
'/v2/tasks?limit=%d' % tasks.DEFAULT_PAGE_SIZE: {
|
||||
'GET': (
|
||||
{},
|
||||
{'tasks': [
|
||||
{
|
||||
'id': _PENDING_ID,
|
||||
'type': 'import',
|
||||
'status': 'pending',
|
||||
},
|
||||
{
|
||||
'id': _PROCESSING_ID,
|
||||
'type': 'import',
|
||||
'status': 'processing',
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v2/tasks?limit=1': {
|
||||
'GET': (
|
||||
{},
|
||||
{
|
||||
'tasks': [
|
||||
{
|
||||
'id': _PENDING_ID,
|
||||
'type': 'import',
|
||||
'status': 'pending',
|
||||
},
|
||||
],
|
||||
'next': ('/v2/tasks?limit=1&'
|
||||
'marker=3a4560a1-e585-443e-9b39-553b46ec92d1'),
|
||||
},
|
||||
),
|
||||
},
|
||||
('/v2/tasks?limit=1&marker=3a4560a1-e585-443e-9b39-553b46ec92d1'): {
|
||||
'GET': (
|
||||
{},
|
||||
{'tasks': [
|
||||
{
|
||||
'id': _PROCESSING_ID,
|
||||
'type': 'import',
|
||||
'status': 'pending',
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v2/tasks/3a4560a1-e585-443e-9b39-553b46ec92d1': {
|
||||
'GET': (
|
||||
{},
|
||||
{
|
||||
'id': _PENDING_ID,
|
||||
'type': 'import',
|
||||
'status': 'pending',
|
||||
},
|
||||
),
|
||||
'PATCH': (
|
||||
{},
|
||||
'',
|
||||
),
|
||||
},
|
||||
'/v2/tasks/e7e59ff6-fa2e-4075-87d3-1a1398a07dc3': {
|
||||
'GET': (
|
||||
{},
|
||||
{
|
||||
'id': 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3',
|
||||
'type': 'import',
|
||||
'status': 'pending',
|
||||
},
|
||||
),
|
||||
'PATCH': (
|
||||
{},
|
||||
'',
|
||||
),
|
||||
},
|
||||
'/v2/tasks': {
|
||||
'POST': (
|
||||
{},
|
||||
{
|
||||
'id': _PENDING_ID,
|
||||
'type': 'import',
|
||||
'status': 'pending',
|
||||
'input': '{"import_from": "file:///", '
|
||||
'"import_from_format": "qcow2"}'
|
||||
},
|
||||
),
|
||||
},
|
||||
'/v2/tasks?limit=%d&owner=%s' % (tasks.DEFAULT_PAGE_SIZE, _OWNER_ID): {
|
||||
'GET': (
|
||||
{},
|
||||
{'tasks': [
|
||||
{
|
||||
'id': _OWNED_TASK_ID,
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v2/tasks?limit=%d&status=processing' % (tasks.DEFAULT_PAGE_SIZE): {
|
||||
'GET': (
|
||||
{},
|
||||
{'tasks': [
|
||||
{
|
||||
'id': _OWNED_TASK_ID,
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v2/tasks?limit=%d&type=import' % (tasks.DEFAULT_PAGE_SIZE): {
|
||||
'GET': (
|
||||
{},
|
||||
{'tasks': [
|
||||
{
|
||||
'id': _OWNED_TASK_ID,
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v2/tasks?limit=%d&type=fake' % (tasks.DEFAULT_PAGE_SIZE): {
|
||||
'GET': (
|
||||
{},
|
||||
{'tasks': [
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v2/tasks?limit=%d&status=fake' % (tasks.DEFAULT_PAGE_SIZE): {
|
||||
'GET': (
|
||||
{},
|
||||
{'tasks': [
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v2/tasks?limit=%d&type=import' % (tasks.DEFAULT_PAGE_SIZE): {
|
||||
'GET': (
|
||||
{},
|
||||
{'tasks': [
|
||||
{
|
||||
'id': _OWNED_TASK_ID,
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v2/tasks?limit=%d&owner=%s' % (tasks.DEFAULT_PAGE_SIZE, _FAKE_OWNER_ID):
|
||||
{
|
||||
'GET': ({},
|
||||
{'tasks': []},
|
||||
),
|
||||
},
|
||||
'/v2/tasks?limit=%d&sort_key=type' % tasks.DEFAULT_PAGE_SIZE: {
|
||||
'GET': (
|
||||
{},
|
||||
{'tasks': [
|
||||
{
|
||||
'id': _PENDING_ID,
|
||||
'type': 'import',
|
||||
'status': 'pending',
|
||||
},
|
||||
{
|
||||
'id': _PROCESSING_ID,
|
||||
'type': 'import',
|
||||
'status': 'processing',
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v2/tasks?limit=%d&sort_dir=asc&sort_key=id' % tasks.DEFAULT_PAGE_SIZE: {
|
||||
'GET': (
|
||||
{},
|
||||
{'tasks': [
|
||||
{
|
||||
'id': _PENDING_ID,
|
||||
'type': 'import',
|
||||
'status': 'pending',
|
||||
},
|
||||
{
|
||||
'id': _PROCESSING_ID,
|
||||
'type': 'import',
|
||||
'status': 'processing',
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
'/v2/tasks?limit=%d&sort_dir=desc&sort_key=id' % tasks.DEFAULT_PAGE_SIZE: {
|
||||
'GET': (
|
||||
{},
|
||||
{'tasks': [
|
||||
{
|
||||
'id': _PROCESSING_ID,
|
||||
'type': 'import',
|
||||
'status': 'processing',
|
||||
},
|
||||
{
|
||||
'id': _PENDING_ID,
|
||||
'type': 'import',
|
||||
'status': 'pending',
|
||||
},
|
||||
]},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
schema_fixtures = {
|
||||
'task': {
|
||||
'GET': (
|
||||
{},
|
||||
{
|
||||
'name': 'task',
|
||||
'properties': {
|
||||
'id': {},
|
||||
'type': {},
|
||||
'status': {},
|
||||
'input': {},
|
||||
'result': {},
|
||||
'message': {},
|
||||
},
|
||||
'additionalProperties': False,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestController(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestController, self).setUp()
|
||||
self.api = utils.FakeAPI(fixtures)
|
||||
self.schema_api = utils.FakeSchemaAPI(schema_fixtures)
|
||||
self.controller = base.BaseController(self.api, self.schema_api,
|
||||
tasks.Controller)
|
||||
|
||||
def test_list_tasks(self):
|
||||
tasks = self.controller.list()
|
||||
self.assertEqual(_PENDING_ID, tasks[0].id)
|
||||
self.assertEqual('import', tasks[0].type)
|
||||
self.assertEqual('pending', tasks[0].status)
|
||||
self.assertEqual(_PROCESSING_ID, tasks[1].id)
|
||||
self.assertEqual('import', tasks[1].type)
|
||||
self.assertEqual('processing', tasks[1].status)
|
||||
|
||||
def test_list_tasks_paginated(self):
|
||||
tasks = self.controller.list(page_size=1)
|
||||
self.assertEqual(_PENDING_ID, tasks[0].id)
|
||||
self.assertEqual('import', tasks[0].type)
|
||||
self.assertEqual(_PROCESSING_ID, tasks[1].id)
|
||||
self.assertEqual('import', tasks[1].type)
|
||||
|
||||
def test_list_tasks_with_status(self):
|
||||
filters = {'filters': {'status': 'processing'}}
|
||||
tasks = self.controller.list(**filters)
|
||||
self.assertEqual(_OWNED_TASK_ID, tasks[0].id)
|
||||
|
||||
def test_list_tasks_with_wrong_status(self):
|
||||
filters = {'filters': {'status': 'fake'}}
|
||||
tasks = self.controller.list(**filters)
|
||||
self.assertEqual(0, len(tasks))
|
||||
|
||||
def test_list_tasks_with_type(self):
|
||||
filters = {'filters': {'type': 'import'}}
|
||||
tasks = self.controller.list(**filters)
|
||||
self.assertEqual(_OWNED_TASK_ID, tasks[0].id)
|
||||
|
||||
def test_list_tasks_with_wrong_type(self):
|
||||
filters = {'filters': {'type': 'fake'}}
|
||||
tasks = self.controller.list(**filters)
|
||||
self.assertEqual(0, len(tasks))
|
||||
|
||||
def test_list_tasks_for_owner(self):
|
||||
filters = {'filters': {'owner': _OWNER_ID}}
|
||||
tasks = self.controller.list(**filters)
|
||||
self.assertEqual(_OWNED_TASK_ID, tasks[0].id)
|
||||
|
||||
def test_list_tasks_for_fake_owner(self):
|
||||
filters = {'filters': {'owner': _FAKE_OWNER_ID}}
|
||||
tasks = self.controller.list(**filters)
|
||||
self.assertEqual(tasks, [])
|
||||
|
||||
def test_list_tasks_filters_encoding(self):
|
||||
filters = {"owner": u"ni\xf1o"}
|
||||
try:
|
||||
self.controller.list(filters=filters)
|
||||
except KeyError:
|
||||
# NOTE(flaper87): It raises KeyError because there's
|
||||
# no fixture supporting this query:
|
||||
# /v2/tasks?owner=ni%C3%B1o&limit=20
|
||||
# We just want to make sure filters are correctly encoded.
|
||||
pass
|
||||
|
||||
self.assertEqual(b"ni\xc3\xb1o", filters["owner"])
|
||||
|
||||
def test_list_tasks_with_marker(self):
|
||||
tasks = self.controller.list(marker=_PENDING_ID, page_size=1)
|
||||
self.assertEqual(1, len(tasks))
|
||||
self.assertEqual(_PROCESSING_ID, tasks[0]['id'])
|
||||
|
||||
def test_list_tasks_with_single_sort_key(self):
|
||||
tasks = self.controller.list(sort_key='type')
|
||||
self.assertEqual(2, len(tasks))
|
||||
self.assertEqual(_PENDING_ID, tasks[0].id)
|
||||
|
||||
def test_list_tasks_with_invalid_sort_key(self):
|
||||
self.assertRaises(ValueError,
|
||||
self.controller.list, sort_key='invalid')
|
||||
|
||||
def test_list_tasks_with_desc_sort_dir(self):
|
||||
tasks = self.controller.list(sort_key='id', sort_dir='desc')
|
||||
self.assertEqual(2, len(tasks))
|
||||
self.assertEqual(_PENDING_ID, tasks[1].id)
|
||||
|
||||
def test_list_tasks_with_asc_sort_dir(self):
|
||||
tasks = self.controller.list(sort_key='id', sort_dir='asc')
|
||||
self.assertEqual(2, len(tasks))
|
||||
self.assertEqual(_PENDING_ID, tasks[0].id)
|
||||
|
||||
def test_list_tasks_with_invalid_sort_dir(self):
|
||||
self.assertRaises(ValueError,
|
||||
self.controller.list,
|
||||
sort_dir='invalid')
|
||||
|
||||
def test_get_task(self):
|
||||
task = self.controller.get(_PENDING_ID)
|
||||
self.assertEqual(_PENDING_ID, task.id)
|
||||
self.assertEqual('import', task.type)
|
||||
|
||||
def test_create_task(self):
|
||||
properties = {
|
||||
'type': 'import',
|
||||
'input': {'import_from_format': 'ovf', 'import_from':
|
||||
'swift://cloud.foo/myaccount/mycontainer/path'},
|
||||
}
|
||||
task = self.controller.create(**properties)
|
||||
self.assertEqual(_PENDING_ID, task.id)
|
||||
self.assertEqual('import', task.type)
|
||||
|
||||
def test_create_task_invalid_property(self):
|
||||
properties = {
|
||||
'type': 'import',
|
||||
'bad_prop': 'value',
|
||||
}
|
||||
self.assertRaises(TypeError, self.controller.create, **properties)
|
@ -1,75 +0,0 @@
|
||||
# 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.
|
||||
|
||||
import testtools
|
||||
|
||||
from glanceclient.tests import utils
|
||||
from glanceclient.v2 import versions
|
||||
|
||||
fixtures = {
|
||||
'/versions': {
|
||||
'GET': (
|
||||
{},
|
||||
{"versions": [
|
||||
{
|
||||
"status": "EXPERIMENTAL",
|
||||
"id": "v3.0",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://10.229.45.145:9292/v3/",
|
||||
"rel": "self"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"status": "CURRENT",
|
||||
"id": "v2.3",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://10.229.45.145:9292/v2/",
|
||||
"rel": "self"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"status": "SUPPORTED",
|
||||
"id": "v1.0",
|
||||
"links": [
|
||||
{
|
||||
"href": "http://10.229.45.145:9292/v1/",
|
||||
"rel": "self"
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestVersions(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestVersions, self).setUp()
|
||||
self.api = utils.FakeAPI(fixtures)
|
||||
self.controller = versions.VersionController(self.api)
|
||||
|
||||
def test_version_list(self):
|
||||
version = list(self.controller.list())
|
||||
self.assertEqual('v3.0', version[0]['id'])
|
||||
self.assertEqual('EXPERIMENTAL', version[0]['status'])
|
||||
self.assertEqual([{"href": "http://10.229.45.145:9292/v3/",
|
||||
"rel": "self"}], version[0]['links'])
|
@ -1,34 +0,0 @@
|
||||
-----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-----
|
@ -1,66 +0,0 @@
|
||||
# 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-----
|
||||
MIIGADCCA+igAwIBAgIBATANBgkqhkiG9w0BAQUFADCBuDEZMBcGA1UEChMQT3Bl
|
||||
bnN0YWNrIENBIE9yZzEaMBgGA1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAhBgkq
|
||||
hkiG9w0BCQEWFGFkbWluQGNhLmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0ZSBD
|
||||
QTELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3RhY2sg
|
||||
VGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTIxMTE2MTI1MDE5WhcNNDAw
|
||||
NDAzMTI1MDE5WjCBmjEbMBkGA1UEChMST3BlbnN0YWNrIFRlc3QgT3JnMRwwGgYD
|
||||
VQQLExNPcGVuc3RhY2sgVGVzdCBVbml0MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl
|
||||
eGFtcGxlLmNvbTEPMA0GA1UEBxMGU3RhdGUxMQswCQYDVQQIEwJDQTELMAkGA1UE
|
||||
BhMCVVMxEDAOBgNVBAMTBzAuMC4wLjAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
|
||||
ggIKAoICAQDUuzrEoAZUMSNdsHhavkVErqGJhhHYyqgzsE/z4UYehaMqnKTgwhQ0
|
||||
T5Hf3GmlIBt4I96/3cxj0qSLrdR81fM+5Km8lIlVHwVn1y6LKcMlaUC4K+sgDLcj
|
||||
hZfbf9+fMkcur3WlNzKpAEaIosWwsu6YvYc+W/nPBpKxMbOZ4fZiPMEo8Pxmw7sl
|
||||
/6hnlBOJj7dpZOZpHhVPZgzYNVoyfKCZiwgdxH4JEYa+EQos87+2Nwhs7bCgrTLL
|
||||
ppCUvpobwZV5w4O0D6INpUfBmsr4IAuXeFWZa61vZYqhaVbAbTTlUzOLGh7Z2uz9
|
||||
gt75iSR2J0e2xntVaUIYLIAUNOO2edk8NMAuIOGr2EIyC7i2O/BTti2YjGNO7SsE
|
||||
ClxiIFKjYahylHmNrS1Q/oMAcJppmhz+oOCmKOMmAZXYAH1A3gs/sWphJpgv/MWt
|
||||
6Ji24VpFaJ+o4bHILlqIpuvL4GLIOkmxVP639khaumgKtgNIUTKJ/V6t/J31WARf
|
||||
xKxlBQTTzV/Be+84YJiiddx8eunU8AorPyAJFzsDPTJpFUB4Q5BwAeDGCySgxJpU
|
||||
qM2MTETBycdiVToM4SWkRsOZgZxQ+AVfkkqDct2Bat2lg9epcIez8PrsohQjQbmi
|
||||
qUUL2c3de4kLYzIWF8EN3P2Me/7b06jbn4c7Fly/AN6tJOG23BzhHQIDAQABozEw
|
||||
LzAtBgNVHREEJjAkghBhbHQxLmV4YW1wbGUuY29tghBhbHQyLmV4YW1wbGUuY29t
|
||||
MA0GCSqGSIb3DQEBBQUAA4ICAQAs/FyHJL1K+kDSLjWkKvMcs2ew5IrNZ2tVUNTL
|
||||
3S0mpRVikKOQbNLh5B6Q7eQIvilCdkuit7o2HrpxQHsRor5b4+LyjSLoltyE7dgr
|
||||
ioP5nkKH+ujw6PtMxJCiKvvI+6cVHh6EV2ZkddvbJLVBVVZmB4H64xocS3rrQj19
|
||||
SXFYVrEjqdLzdGPNIBR+XVnTCeofXg1rkMaU7JuY8nRztee8PRVcKYX6scPfZJb8
|
||||
+Ea2dsTmtQP4H9mk+JiKGYhEeMLVmjiv3q7KIFownTKZ88K6QbpW2Nj66ItvphoT
|
||||
QqI3rs6E8N0BhftiCcxXtXg+o4utfcnp8jTXX5tVnv44FqtWx7Gzg8XTLPri+ZEB
|
||||
5IbgU4Q3qFicenBfjwZhH3+GNe52/wLVZLYjal5RPVSRdu9UEDeDAwTCMZSLF4lC
|
||||
rc9giQCMnJ4ISi6C7xH+lDZGFqcJd4oXg/ue9aOJJAFTwhd83fdCHhUu431iPrts
|
||||
NubfrHLMeUjluFgIWmhEZg+XTjB1SQeQzNaZiMODaAv4/40ZVKxvNpDFwIIsPUDf
|
||||
+uC+fv1Q8+alqVMl2ouVyr8ut43HWNV6CJHXODvFp5irjxzVSgLtYDVUInkDFJEs
|
||||
tFpTY21/zVAHIvsj2n4F1231nILR6vBp/WbwBY7r7j0oRtbaO3B1Q6tsbCZQRkKU
|
||||
tdc5rw==
|
||||
-----END CERTIFICATE-----
|
@ -1,35 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIGFTCCA/2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBuDEZMBcGA1UEChMQT3Bl
|
||||
bnN0YWNrIENBIE9yZzEaMBgGA1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAhBgkq
|
||||
hkiG9w0BCQEWFGFkbWluQGNhLmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0ZSBD
|
||||
QTELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3RhY2sg
|
||||
VGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTIxMTE1MTcwNjMzWhcNMTIx
|
||||
MTE2MTcwNjMzWjCBqDEbMBkGA1UEChMST3BlbnN0YWNrIFRlc3QgT3JnMRwwGgYD
|
||||
VQQLExNPcGVuc3RhY2sgVGVzdCBVbml0MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl
|
||||
eGFtcGxlLmNvbTEPMA0GA1UEBxMGU3RhdGUxMQswCQYDVQQIEwJDQTELMAkGA1UE
|
||||
BhMCVVMxHjAcBgNVBAMTFW9wZW5zdGFjay5leGFtcGxlLmNvbTCCAiIwDQYJKoZI
|
||||
hvcNAQEBBQADggIPADCCAgoCggIBANn9w82sGN+iALSlZ5/Odd5iJ3MAJ5BoalMG
|
||||
kfUECGMewd7lE5+6ok1+vqVbYjd+F56aSkIJFR/ck51EYG2diGM5E5zjdiLcyB9l
|
||||
dKB5PmaB2P9dHyomy+sMONqhw5uEsWKIfPbtjzGRhjJL0bIYwptGr4JPraZy8R3d
|
||||
HWbTO3SlnFkjHHtfoKuZtRJq5OD1hXM8J9IEsBC90zw7RWCTw1iKllLfKITPUi7O
|
||||
i8ITjUyTVKR2e56XRtmxGgGsGyZpcYrmhRuLo9jyL9m3VuNzsfwDvCqn7cnZIOQa
|
||||
VO4hNZdO+33PINCC+YVNOGYwqfBuKxYvHJSbMfOZ6JDK98v65pWLBN7PObYIjQFH
|
||||
uJyK5DuQMqvyRIcrtfLUalepD+PQaCn4ajgXjpqBz4t0pMte8jh0i4clLwvT0elT
|
||||
PtA+MMos3hIGjJgEHTvLdCff9qlkjHlW7lg45PYn7S0Z7dqtBWD7Ys2B+AWp/skt
|
||||
hRr7YZeegLfHVJVkMFL6Ojs98161W2FLmEA+5nejzjx7kWlJsg9aZPbBnN87m6iK
|
||||
RHI+VkqSpBHm10iMlp4Nn30RtOj0wQhxoZjtEouGeRobHN5ULwpAfNEpKMMZf5bt
|
||||
604JjOP9Pn+WzsvzGDeXjgxUP55PIR+EpHkvS5h1YQ+9RV5J669e2J9T4gnc0Abg
|
||||
t3jJvtp1AgMBAAGjODA2MDQGA1UdEQQtMCuCEGFsdDEuZXhhbXBsZS5jb22BDm9z
|
||||
QGV4YW1wbGUuY29tggcwLjAuMC4wMA0GCSqGSIb3DQEBBQUAA4ICAQBkKUA4lhsS
|
||||
zjcuh77wtAIP9SN5Se4CheTRDXKDeuwWB6VQDzdJdtqSnWNF6sVEA97vhNTSjaBD
|
||||
hfrtX9FZ+ImADlOf01t4Dakhsmje/DEPiQHaCy9P5fGtGIGRlWUyTmyQoV1LDLM5
|
||||
wgB1V5Oz2iDat2AdvUb0OFP0O1M887OgPpfUDQJEUTVAs5JS+6P/6RPyFh/dHWiX
|
||||
UGoM0nMvTwsLWT4CZ9NdIChecVwBFqXjNytPY53tKbCWp77d/oGUg5Pb6EBD3xSW
|
||||
AeMJ6PuafDRgm/He8nOtZnUd+53Ha59yzSGnSopu5WqrUa/xD+ZiK6dX7LsH/M8y
|
||||
Hz0rh7w22qNHUxNaC3hrhx1BxX4au6z4kpKXIlAWH7ViRzVZ8XkwqqrndqWPWOFk
|
||||
1emLLJ1dfT8FXdgpHenkUiktAf5qZhUWbF6nr9at+c4T7ZrLHSekux2r29kD9BJw
|
||||
O2gSSclxKlMPwirUC0P4J/2WP72kCbf6AEfKU2siT12E6/xOmgen9lVYKckBiLbb
|
||||
rJ97L1ieJI8GZTGExjtE9Lo+XVsv28D2XLU8vNCODs0xPZCr2TLNS/6YcnVy6594
|
||||
vpvU7fbNFAyxG4sjQC0wHoN6rn+kd1kzfprmBHKTx3W7y+hzjb+W7iS2EZn20k+N
|
||||
l3+dFHnWayuCdqcFwIl3m8i8FupFihz9+A==
|
||||
-----END CERTIFICATE-----
|
@ -1,51 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKQIBAAKCAgEA1Ls6xKAGVDEjXbB4Wr5FRK6hiYYR2MqoM7BP8+FGHoWjKpyk
|
||||
4MIUNE+R39xppSAbeCPev93MY9Kki63UfNXzPuSpvJSJVR8FZ9cuiynDJWlAuCvr
|
||||
IAy3I4WX23/fnzJHLq91pTcyqQBGiKLFsLLumL2HPlv5zwaSsTGzmeH2YjzBKPD8
|
||||
ZsO7Jf+oZ5QTiY+3aWTmaR4VT2YM2DVaMnygmYsIHcR+CRGGvhEKLPO/tjcIbO2w
|
||||
oK0yy6aQlL6aG8GVecODtA+iDaVHwZrK+CALl3hVmWutb2WKoWlWwG005VMzixoe
|
||||
2drs/YLe+YkkdidHtsZ7VWlCGCyAFDTjtnnZPDTALiDhq9hCMgu4tjvwU7YtmIxj
|
||||
Tu0rBApcYiBSo2GocpR5ja0tUP6DAHCaaZoc/qDgpijjJgGV2AB9QN4LP7FqYSaY
|
||||
L/zFreiYtuFaRWifqOGxyC5aiKbry+BiyDpJsVT+t/ZIWrpoCrYDSFEyif1erfyd
|
||||
9VgEX8SsZQUE081fwXvvOGCYonXcfHrp1PAKKz8gCRc7Az0yaRVAeEOQcAHgxgsk
|
||||
oMSaVKjNjExEwcnHYlU6DOElpEbDmYGcUPgFX5JKg3LdgWrdpYPXqXCHs/D67KIU
|
||||
I0G5oqlFC9nN3XuJC2MyFhfBDdz9jHv+29Oo25+HOxZcvwDerSThttwc4R0CAwEA
|
||||
AQKCAgEAqnwqSu4cZFjFCQ6mRcL67GIvn3FM2DsBtfr0+HRvp4JeE4ZaNK4VVx71
|
||||
vzx7hhRHL28/0vBEHzPvHun+wtUMDjlfNnyr2wXzZRb0fB7KAC9r6K15z8Og+dzU
|
||||
qNrAMmsu1OFVHUUxWnOYE2Svnj6oLMynmHhJqXqREWTNlOOce3pJKzCGdy0hzQAo
|
||||
zGnFhpcg3Fw6s7+iQHF+lb+cO53Zb3QW2xRgFZBwNd6eEwx9deCA5htPVFW5wbAJ
|
||||
asud4eSwkFb6M9Hbg6gT67rMMzIrWAbeQwgihIYSJe2v0qMyox6czjvuwZVMHJdH
|
||||
byBTkkVEmdxTd03V5F21f3wrik/4oWqytjmjvMIY1gGTMo7aBnvPoKpgc2fqJub9
|
||||
cdAfGiJnFqo4Ae55mL4sgJPUCP7UATaDNAOCgt0zStmHMH8ACwk0dh1pzjyjpSR3
|
||||
OQfFs8QCAl9cvzxwux1tzG/uYxOrr+Rj2JlZKW/ljbWOeE0Gnjca73F40uGkEIbZ
|
||||
5i6YEuiPE6XGH0TP62Sdu2t5OlaKnZT12Tf6E8xNDsdaLuvAIz5sXyhoxvOmVd9w
|
||||
V4+uN1bZ10c5k/4uGRsHiXjX6IyYZEj8rKz6ryNikCdi6OzxWE3pCXmfBlVaXtO6
|
||||
EIubzk6dgjWcsPoqOsIl5Ywz4RWu0YUk4ZxRts54jCn14bPQpoECggEBAPiLTN8Z
|
||||
I0GQXMQaq9sN8kVsM/6AG/vWbc+IukPDYEC6Prk79jzkxMpDP8qK9C71bh39U1ky
|
||||
Kz4gSsLi9v3rM1gZwNshkZJ/zdQJ1NiCkzJVJX48DGeyYqUBjVt8Si37V2vzblBN
|
||||
RvM7U3rDN0xGiannyWnBC/jed+ZFCo97E9yOxIAs2ekwsl+ED3j1cARv8pBTGWnw
|
||||
Zhh4AD/Osk5U038oYcWHaIzUuNhEpv46bFLjVT11mGHfUY51Db3jBn0HYRlOPEV/
|
||||
F0kE5F+6rRg2tt7n0PO3UbzSNFyDRwtknJ2Nh4EtZZe93domls8SMR/kEHXcPLiQ
|
||||
ytEFyIAzsxfUwrECggEBANsc54N/LPmX1XuC643ZsDobH5/ALKc8W7wE7e82oSTD
|
||||
7cKBgdgB71DupJ7m81LHaDgT2RIzjl+lR3VVYLR/ukMcW+47JWrHyrsinu6itOdt
|
||||
ruhw0UPksoJGsB4KxUdRioFVT7m45GpnseJL0tjYaTCW01swae4QL4skNjjphPrb
|
||||
b/heMz9n79TK2ePlw1BvJKH0fnOJRuh/v63pD9SymB8EPsazjloKZ5qTrqVi3Obs
|
||||
F8WTSdl8KB1JSgeppdvHRcZQY1J+UfdCAlGD/pP7/zCKkRYcetre7fGMKVyPIDzO
|
||||
GAWz0xA2jnrgg7UqIh74oRHe0lZVMdMQ7FoJbRa7KC0CggEAJreEbQh8bn0vhjjl
|
||||
ZoVApUHaw51vPobDql2RLncj6lFY7gACNrAoW52oNUP6D8qZscBBmJZxGAdtvfgf
|
||||
I6Tc5a91VG1hQOH5zTsO1f9ZMLEE2yo9gHXQWgXo4ER3RbxufNl56LZxA/jM40W/
|
||||
unkOftIllPzGgakeIlfE8l7o1CXFRHY4J9Q3JRvsURpirb5GmeboAZG6RbuDxmzL
|
||||
Z9pc6+T9fgi+55lHhiEDpnyxXSQepilIaI6iJL/lORxBaX6ZyJhgWS8YEH7bmHH6
|
||||
/tefGxAfg6ed6v0PvQ2SJpswrnZakmvg9IdWJOJ4AZ/C2UXsrn91Ugb0ISV2e0oS
|
||||
bvbssQKCAQBjstc04h0YxJmCxaNgu/iPt9+/1LV8st4awzNwcS8Jh40bv8nQ+7Bk
|
||||
5vFIzFVTCSDGw2E2Avd5Vb8aCGskNioOd0ztLURtPdNlKu+eLbKayzGW2h6eAeWn
|
||||
mXpxcP0q4lNfXe4U16g3Mk+iZFXgDThvv3EUQQcyJ3M6oJN7eeXkLwzXuiUfaK+b
|
||||
52EVbWpdovTMLG+NKp11FQummjF12n2VP11BFFplZe6WSzRgVIenGy4F3Grx5qhq
|
||||
CvsAWZT6V8XL4rAOzSOGmiZr6N9hfnwzHhm+Md9Ez8L88YWwc/97K1uK3LPg4LIb
|
||||
/yRuvmkgJolDlFuopMMzArRIk5lrimVRAoIBAQDZmXk/VMA7fsI1/2sgSME0xt1A
|
||||
jkJZMZSnVD0UDWFkbyK6E5jDnwVUyqBDYe+HJyT4UnPDNCj++BchCQcG0Jih04RM
|
||||
jwGqxkfTF9K7kfouINSSXPRw/BtHkqMhV/g324mWcifCFVkDQghuslfmey8BKumo
|
||||
2KPyGnF9Q8CvTSQ0VlK1ZAKRf/zish49PMm7vD1KGkjRPliS3tgAmXPEpwijPGse
|
||||
4dSUeTfw5wCKAoq9DHjyHdO5fnfkOvA5PMQ4JZAzOCzJak8ET+tw4wB/dBeYiLVi
|
||||
l00GHLYAr5Nv/WqVnl/VLMd9rOCnLck+pxBNSa6dTrp3FuY00son6hneIvkv
|
||||
-----END RSA PRIVATE KEY-----
|
@ -1,61 +0,0 @@
|
||||
#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-----
|
||||
MIIFyjCCA7ICCQC7QmA+WJ3t+jANBgkqhkiG9w0BAQUFADCBpTELMAkGA1UEBhMC
|
||||
VVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQHDAZTdGF0ZTExGzAZBgNVBAoMEk9wZW5z
|
||||
dGFjayBUZXN0IE9yZzEcMBoGA1UECwwTT3BlbnN0YWNrIFRlc3QgVW5pdDEbMBkG
|
||||
A1UEAwwSKi5wb25nLmV4YW1wbGUuY29tMSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl
|
||||
eGFtcGxlLmNvbTAgFw0xMzA4MjExNzI5MThaGA8yMTEzMDcyODE3MjkxOFowgaUx
|
||||
CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEPMA0GA1UEBwwGU3RhdGUxMRswGQYD
|
||||
VQQKDBJPcGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsME09wZW5zdGFjayBUZXN0
|
||||
IFVuaXQxGzAZBgNVBAMMEioucG9uZy5leGFtcGxlLmNvbTEgMB4GCSqGSIb3DQEJ
|
||||
ARYRYWRtaW5AZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
|
||||
AoICAQDUuzrEoAZUMSNdsHhavkVErqGJhhHYyqgzsE/z4UYehaMqnKTgwhQ0T5Hf
|
||||
3GmlIBt4I96/3cxj0qSLrdR81fM+5Km8lIlVHwVn1y6LKcMlaUC4K+sgDLcjhZfb
|
||||
f9+fMkcur3WlNzKpAEaIosWwsu6YvYc+W/nPBpKxMbOZ4fZiPMEo8Pxmw7sl/6hn
|
||||
lBOJj7dpZOZpHhVPZgzYNVoyfKCZiwgdxH4JEYa+EQos87+2Nwhs7bCgrTLLppCU
|
||||
vpobwZV5w4O0D6INpUfBmsr4IAuXeFWZa61vZYqhaVbAbTTlUzOLGh7Z2uz9gt75
|
||||
iSR2J0e2xntVaUIYLIAUNOO2edk8NMAuIOGr2EIyC7i2O/BTti2YjGNO7SsEClxi
|
||||
IFKjYahylHmNrS1Q/oMAcJppmhz+oOCmKOMmAZXYAH1A3gs/sWphJpgv/MWt6Ji2
|
||||
4VpFaJ+o4bHILlqIpuvL4GLIOkmxVP639khaumgKtgNIUTKJ/V6t/J31WARfxKxl
|
||||
BQTTzV/Be+84YJiiddx8eunU8AorPyAJFzsDPTJpFUB4Q5BwAeDGCySgxJpUqM2M
|
||||
TETBycdiVToM4SWkRsOZgZxQ+AVfkkqDct2Bat2lg9epcIez8PrsohQjQbmiqUUL
|
||||
2c3de4kLYzIWF8EN3P2Me/7b06jbn4c7Fly/AN6tJOG23BzhHQIDAQABMA0GCSqG
|
||||
SIb3DQEBBQUAA4ICAQCfzAhdGe5UMaNX1zyJicBpQd1G+HNo7Ea5+vXf9tlYNdhT
|
||||
lIi9NqYjngwNiWI1kUm2FPRDaTwC0kLxk5zBPzF7bcf0SwJCeDjmlUpY7YenS0DA
|
||||
XmIbg8FvgOlp69Ikrqz98Y4pB9H4O81WdjxNBBbHjrufAXxZYnh5rXrVsXeSJ8jN
|
||||
MYGWlSv4xwFGfRX53b8VwXFjGjAkH8SQGtRV2w9d0jF8OzFwBA4bKk4EplY0yBPR
|
||||
2d7Y3RVrDnOVfV13F8CZxJ5fu+6QamUwIaTjpyqflE1L52KTy+vWPYR47H2u2bhD
|
||||
IeZRufJ8adNIOtH32EcENkusQjLrb3cTXGW00TljhFXd22GqL5d740u+GEKHtWh+
|
||||
9OKPTMZK8yK7d5EyS2agTVWmXU6HfpAKz9+AEOnVYErpnggNZjkmJ9kD185rGlSZ
|
||||
Vvo429hXoUAHNbd+8zda3ufJnJf5q4ZEl8+hp8xsvraUy83XLroVZRsKceldmAM8
|
||||
swt6n6w5gRKg4xTH7KFrd+KNptaoY3SsVrnJuaSOPenrUXbZzaI2Q35CId93+8NP
|
||||
mXVIWdPO1msdZNiCYInRIGycK+oifUZPtAaJdErg8rt8NSpHzYKQ0jfjAGiVHBjK
|
||||
s0J2TjoKB3jtlrw2DAmFWKeMGNp//1Rm6kfQCCXWftn+TA7XEJhcjyDBVciugA==
|
||||
-----END CERTIFICATE-----
|
@ -1,54 +0,0 @@
|
||||
#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-----
|
@ -1,225 +0,0 @@
|
||||
# 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
|
||||
|
||||
from glanceclient.v2 import schemas
|
||||
|
||||
|
||||
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 FakeSchemaAPI(FakeAPI):
|
||||
def get(self, *args, **kwargs):
|
||||
_, raw_schema = self._request('GET', *args, **kwargs)
|
||||
return schemas.Schema(raw_schema)
|
||||
|
||||
|
||||
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.headers['x-openstack-request-id'] = 'req-1234'
|
||||
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):
|
||||
# NOTE(flwang): For image update, the data will be a 'list' which
|
||||
# contains operation dict, such as: [{"op": "remove", "path": "/a"}]
|
||||
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)
|
@ -1,16 +0,0 @@
|
||||
# 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 glanceclient.v1.client import Client # noqa
|
@ -1,529 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2012 Grid Dynamics
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# THIS MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-glanceclient-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
|
||||
# E1102: %s is not callable
|
||||
# pylint: disable=E1102
|
||||
|
||||
import abc
|
||||
import copy
|
||||
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from glanceclient._i18n import _
|
||||
from glanceclient.v1.apiclient import exceptions
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""Return id if argument is a Resource.
|
||||
|
||||
Abstracts the common pattern of allowing both an object or an object's ID
|
||||
(UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
try:
|
||||
if obj.uuid:
|
||||
return obj.uuid
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
|
||||
class HookableMixin(object):
|
||||
"""Mixin so classes can register and run hooks."""
|
||||
_hooks_map = {}
|
||||
|
||||
@classmethod
|
||||
def add_hook(cls, hook_type, hook_func):
|
||||
"""Add a new hook of specified type.
|
||||
|
||||
:param cls: class that registers hooks
|
||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||
:param hook_func: hook function
|
||||
"""
|
||||
if hook_type not in cls._hooks_map:
|
||||
cls._hooks_map[hook_type] = []
|
||||
|
||||
cls._hooks_map[hook_type].append(hook_func)
|
||||
|
||||
@classmethod
|
||||
def run_hooks(cls, hook_type, *args, **kwargs):
|
||||
"""Run all hooks of specified type.
|
||||
|
||||
:param cls: class that registers hooks
|
||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||
:param args: args to be passed to every hook function
|
||||
:param kwargs: kwargs to be passed to every hook function
|
||||
"""
|
||||
hook_funcs = cls._hooks_map.get(hook_type) or []
|
||||
for hook_func in hook_funcs:
|
||||
hook_func(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseManager(HookableMixin):
|
||||
"""Basic manager type providing common operations.
|
||||
|
||||
Managers interact with a particular type of API (servers, flavors, images,
|
||||
etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initializes BaseManager with `client`.
|
||||
|
||||
:param client: instance of BaseClient descendant for HTTP requests
|
||||
"""
|
||||
super(BaseManager, self).__init__()
|
||||
self.client = client
|
||||
|
||||
def _list(self, url, response_key=None, obj_class=None, json=None):
|
||||
"""List the collection.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'. If response_key is None - all response body
|
||||
will be used.
|
||||
:param obj_class: class for constructing the returned objects
|
||||
(self.resource_class will be used by default)
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
"""
|
||||
if json:
|
||||
body = self.client.post(url, json=json).json()
|
||||
else:
|
||||
body = self.client.get(url).json()
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
|
||||
data = body[response_key] if response_key is not None else body
|
||||
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
|
||||
# unlike other services which just return the list...
|
||||
try:
|
||||
data = data['values']
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||
|
||||
def _get(self, url, response_key=None):
|
||||
"""Get an object from collection.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'server'. If response_key is None - all response body
|
||||
will be used.
|
||||
"""
|
||||
body = self.client.get(url).json()
|
||||
data = body[response_key] if response_key is not None else body
|
||||
return self.resource_class(self, data, loaded=True)
|
||||
|
||||
def _head(self, url):
|
||||
"""Retrieve request headers for an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
"""
|
||||
resp = self.client.head(url)
|
||||
return resp.status_code == 204
|
||||
|
||||
def _post(self, url, json, response_key=None, return_raw=False):
|
||||
"""Create an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'server'. If response_key is None - all response body
|
||||
will be used.
|
||||
:param return_raw: flag to force returning raw JSON instead of
|
||||
Python object of self.resource_class
|
||||
"""
|
||||
body = self.client.post(url, json=json).json()
|
||||
data = body[response_key] if response_key is not None else body
|
||||
if return_raw:
|
||||
return data
|
||||
return self.resource_class(self, data)
|
||||
|
||||
def _put(self, url, json=None, response_key=None):
|
||||
"""Update an object with PUT method.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'. If response_key is None - all response body
|
||||
will be used.
|
||||
"""
|
||||
resp = self.client.put(url, json=json)
|
||||
# PUT requests may not return a body
|
||||
if resp.content:
|
||||
body = resp.json()
|
||||
if response_key is not None:
|
||||
return self.resource_class(self, body[response_key])
|
||||
else:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _patch(self, url, json=None, response_key=None):
|
||||
"""Update an object with PATCH method.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'. If response_key is None - all response body
|
||||
will be used.
|
||||
"""
|
||||
body = self.client.patch(url, json=json).json()
|
||||
if response_key is not None:
|
||||
return self.resource_class(self, body[response_key])
|
||||
else:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _delete(self, url):
|
||||
"""Delete an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers/my-server'
|
||||
"""
|
||||
return self.client.delete(url)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ManagerWithFind(BaseManager):
|
||||
"""Manager with additional `find()`/`findall()` methods."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
def find(self, **kwargs):
|
||||
"""Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
matches = self.findall(**kwargs)
|
||||
num_matches = len(matches)
|
||||
if num_matches == 0:
|
||||
msg = _("No %(name)s matching %(args)s.") % {
|
||||
'name': self.resource_class.__name__,
|
||||
'args': kwargs
|
||||
}
|
||||
raise exceptions.NotFound(msg)
|
||||
elif num_matches > 1:
|
||||
raise exceptions.NoUniqueMatch()
|
||||
else:
|
||||
return matches[0]
|
||||
|
||||
def findall(self, **kwargs):
|
||||
"""Find all items with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
found = []
|
||||
searches = kwargs.items()
|
||||
|
||||
for obj in self.list():
|
||||
try:
|
||||
if all(getattr(obj, attr) == value
|
||||
for (attr, value) in searches):
|
||||
found.append(obj)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
return found
|
||||
|
||||
|
||||
class CrudManager(BaseManager):
|
||||
"""Base manager class for manipulating entities.
|
||||
|
||||
Children of this class are expected to define a `collection_key` and `key`.
|
||||
|
||||
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
|
||||
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
|
||||
objects containing a list of member resources (e.g. `{'entities': [{},
|
||||
{}, {}]}`).
|
||||
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
|
||||
refer to an individual member of the collection.
|
||||
|
||||
"""
|
||||
collection_key = None
|
||||
key = None
|
||||
|
||||
def build_url(self, base_url=None, **kwargs):
|
||||
"""Builds a resource URL for the given kwargs.
|
||||
|
||||
Given an example collection where `collection_key = 'entities'` and
|
||||
`key = 'entity'`, the following URL's could be generated.
|
||||
|
||||
By default, the URL will represent a collection of entities, e.g.::
|
||||
|
||||
/entities
|
||||
|
||||
If kwargs contains an `entity_id`, then the URL will represent a
|
||||
specific member, e.g.::
|
||||
|
||||
/entities/{entity_id}
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
url = base_url if base_url is not None else ''
|
||||
|
||||
url += '/%s' % self.collection_key
|
||||
|
||||
# do we have a specific entity?
|
||||
entity_id = kwargs.get('%s_id' % self.key)
|
||||
if entity_id is not None:
|
||||
url += '/%s' % entity_id
|
||||
|
||||
return url
|
||||
|
||||
def _filter_kwargs(self, kwargs):
|
||||
"""Drop null values and handle ids."""
|
||||
for key, ref in kwargs.copy().items():
|
||||
if ref is None:
|
||||
kwargs.pop(key)
|
||||
else:
|
||||
if isinstance(ref, Resource):
|
||||
kwargs.pop(key)
|
||||
kwargs['%s_id' % key] = getid(ref)
|
||||
return kwargs
|
||||
|
||||
def create(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._post(
|
||||
self.build_url(**kwargs),
|
||||
{self.key: kwargs},
|
||||
self.key)
|
||||
|
||||
def get(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._get(
|
||||
self.build_url(**kwargs),
|
||||
self.key)
|
||||
|
||||
def head(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._head(self.build_url(**kwargs))
|
||||
|
||||
def list(self, base_url=None, **kwargs):
|
||||
"""List the collection.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._list(
|
||||
'%(base_url)s%(query)s' % {
|
||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
||||
},
|
||||
self.collection_key)
|
||||
|
||||
def put(self, base_url=None, **kwargs):
|
||||
"""Update an element.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._put(self.build_url(base_url=base_url, **kwargs))
|
||||
|
||||
def update(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
params = kwargs.copy()
|
||||
params.pop('%s_id' % self.key)
|
||||
|
||||
return self._patch(
|
||||
self.build_url(**kwargs),
|
||||
{self.key: params},
|
||||
self.key)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._delete(
|
||||
self.build_url(**kwargs))
|
||||
|
||||
def find(self, base_url=None, **kwargs):
|
||||
"""Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
rl = self._list(
|
||||
'%(base_url)s%(query)s' % {
|
||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
||||
},
|
||||
self.collection_key)
|
||||
num = len(rl)
|
||||
|
||||
if num == 0:
|
||||
msg = _("No %(name)s matching %(args)s.") % {
|
||||
'name': self.resource_class.__name__,
|
||||
'args': kwargs
|
||||
}
|
||||
raise exceptions.NotFound(msg)
|
||||
elif num > 1:
|
||||
raise exceptions.NoUniqueMatch
|
||||
else:
|
||||
return rl[0]
|
||||
|
||||
|
||||
class Extension(HookableMixin):
|
||||
"""Extension descriptor."""
|
||||
|
||||
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
|
||||
manager_class = None
|
||||
|
||||
def __init__(self, name, module):
|
||||
super(Extension, self).__init__()
|
||||
self.name = name
|
||||
self.module = module
|
||||
self._parse_extension_module()
|
||||
|
||||
def _parse_extension_module(self):
|
||||
self.manager_class = None
|
||||
for attr_name, attr_value in self.module.__dict__.items():
|
||||
if attr_name in self.SUPPORTED_HOOKS:
|
||||
self.add_hook(attr_name, attr_value)
|
||||
else:
|
||||
try:
|
||||
if issubclass(attr_value, BaseManager):
|
||||
self.manager_class = attr_value
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return "<Extension '%s'>" % self.name
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""Base class for OpenStack resources (tenant, user, etc.).
|
||||
|
||||
This is pretty much just a bag for attributes.
|
||||
"""
|
||||
|
||||
HUMAN_ID = False
|
||||
NAME_ATTR = 'name'
|
||||
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
"""Populate and bind to a manager.
|
||||
|
||||
:param manager: BaseManager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
self._loaded = loaded
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k
|
||||
for k in self.__dict__.keys()
|
||||
if k[0] != '_' and k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
@property
|
||||
def human_id(self):
|
||||
"""Human-readable ID which can be used for bash completion."""
|
||||
if self.HUMAN_ID:
|
||||
name = getattr(self, self.NAME_ATTR, None)
|
||||
if name is not None:
|
||||
return strutils.to_slug(name)
|
||||
return None
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.items():
|
||||
try:
|
||||
setattr(self, k, v)
|
||||
self._info[k] = v
|
||||
except AttributeError:
|
||||
# In this case we already defined the attribute on the class
|
||||
pass
|
||||
|
||||
def __getattr__(self, k):
|
||||
if k not in self.__dict__:
|
||||
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
||||
if not self.is_loaded():
|
||||
self.get()
|
||||
return self.__getattr__(k)
|
||||
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
def get(self):
|
||||
"""Support for lazy loading details.
|
||||
|
||||
Some clients, such as novaclient have the option to lazy load the
|
||||
details, details which can be loaded with this function.
|
||||
"""
|
||||
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||
self.set_loaded(True)
|
||||
if not hasattr(self.manager, 'get'):
|
||||
return
|
||||
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
self._add_details(
|
||||
{'x_request_id': self.manager.client.last_request_id})
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Resource):
|
||||
return NotImplemented
|
||||
# two resources of different types are not equal
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self._info == other._info
|
||||
|
||||
def is_loaded(self):
|
||||
return self._loaded
|
||||
|
||||
def set_loaded(self, val):
|
||||
self._loaded = val
|
||||
|
||||
def to_dict(self):
|
||||
return copy.deepcopy(self._info)
|
@ -1,477 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 Nebula, Inc.
|
||||
# Copyright 2013 Alessio Ababilov
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Exception definitions.
|
||||
"""
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# THIS MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-glanceclient-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
from glanceclient._i18n import _
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""The base exception class for all exceptions this library raises."""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(ClientException):
|
||||
"""Error in validation on API client side."""
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedVersion(ClientException):
|
||||
"""User is trying to use an unsupported version of the API."""
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(ClientException):
|
||||
"""Error in CLI tool."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationFailure(ClientException):
|
||||
"""Cannot authorize API client."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionError(ClientException):
|
||||
"""Cannot connect to API service."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionRefused(ConnectionError):
|
||||
"""Connection refused while trying to connect to API service."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthPluginOptionsMissing(AuthorizationFailure):
|
||||
"""Auth plugin misses some options."""
|
||||
def __init__(self, opt_names):
|
||||
super(AuthPluginOptionsMissing, self).__init__(
|
||||
_("Authentication failed. Missing options: %s") %
|
||||
", ".join(opt_names))
|
||||
self.opt_names = opt_names
|
||||
|
||||
|
||||
class AuthSystemNotFound(AuthorizationFailure):
|
||||
"""User has specified an AuthSystem that is not installed."""
|
||||
def __init__(self, auth_system):
|
||||
super(AuthSystemNotFound, self).__init__(
|
||||
_("AuthSystemNotFound: %r") % auth_system)
|
||||
self.auth_system = auth_system
|
||||
|
||||
|
||||
class NoUniqueMatch(ClientException):
|
||||
"""Multiple entities found instead of one."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointException(ClientException):
|
||||
"""Something is rotten in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(EndpointException):
|
||||
"""Could not find requested endpoint in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class AmbiguousEndpoints(EndpointException):
|
||||
"""Found more than one matching endpoint in Service Catalog."""
|
||||
def __init__(self, endpoints=None):
|
||||
super(AmbiguousEndpoints, self).__init__(
|
||||
_("AmbiguousEndpoints: %r") % endpoints)
|
||||
self.endpoints = endpoints
|
||||
|
||||
|
||||
class HttpError(ClientException):
|
||||
"""The base exception class for all HTTP exceptions."""
|
||||
http_status = 0
|
||||
message = _("HTTP Error")
|
||||
|
||||
def __init__(self, message=None, details=None,
|
||||
response=None, request_id=None,
|
||||
url=None, method=None, http_status=None):
|
||||
self.http_status = http_status or self.http_status
|
||||
self.message = message or self.message
|
||||
self.details = details
|
||||
self.request_id = request_id
|
||||
self.response = response
|
||||
self.url = url
|
||||
self.method = method
|
||||
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
|
||||
if request_id:
|
||||
formatted_string += " (Request-ID: %s)" % request_id
|
||||
super(HttpError, self).__init__(formatted_string)
|
||||
|
||||
|
||||
class HTTPRedirection(HttpError):
|
||||
"""HTTP Redirection."""
|
||||
message = _("HTTP Redirection")
|
||||
|
||||
|
||||
class HTTPClientError(HttpError):
|
||||
"""Client-side HTTP error.
|
||||
|
||||
Exception for cases in which the client seems to have erred.
|
||||
"""
|
||||
message = _("HTTP Client Error")
|
||||
|
||||
|
||||
class HttpServerError(HttpError):
|
||||
"""Server-side HTTP error.
|
||||
|
||||
Exception for cases in which the server is aware that it has
|
||||
erred or is incapable of performing the request.
|
||||
"""
|
||||
message = _("HTTP Server Error")
|
||||
|
||||
|
||||
class MultipleChoices(HTTPRedirection):
|
||||
"""HTTP 300 - Multiple Choices.
|
||||
|
||||
Indicates multiple options for the resource that the client may follow.
|
||||
"""
|
||||
|
||||
http_status = 300
|
||||
message = _("Multiple Choices")
|
||||
|
||||
|
||||
class BadRequest(HTTPClientError):
|
||||
"""HTTP 400 - Bad Request.
|
||||
|
||||
The request cannot be fulfilled due to bad syntax.
|
||||
"""
|
||||
http_status = 400
|
||||
message = _("Bad Request")
|
||||
|
||||
|
||||
class Unauthorized(HTTPClientError):
|
||||
"""HTTP 401 - Unauthorized.
|
||||
|
||||
Similar to 403 Forbidden, but specifically for use when authentication
|
||||
is required and has failed or has not yet been provided.
|
||||
"""
|
||||
http_status = 401
|
||||
message = _("Unauthorized")
|
||||
|
||||
|
||||
class PaymentRequired(HTTPClientError):
|
||||
"""HTTP 402 - Payment Required.
|
||||
|
||||
Reserved for future use.
|
||||
"""
|
||||
http_status = 402
|
||||
message = _("Payment Required")
|
||||
|
||||
|
||||
class Forbidden(HTTPClientError):
|
||||
"""HTTP 403 - Forbidden.
|
||||
|
||||
The request was a valid request, but the server is refusing to respond
|
||||
to it.
|
||||
"""
|
||||
http_status = 403
|
||||
message = _("Forbidden")
|
||||
|
||||
|
||||
class NotFound(HTTPClientError):
|
||||
"""HTTP 404 - Not Found.
|
||||
|
||||
The requested resource could not be found but may be available again
|
||||
in the future.
|
||||
"""
|
||||
http_status = 404
|
||||
message = _("Not Found")
|
||||
|
||||
|
||||
class MethodNotAllowed(HTTPClientError):
|
||||
"""HTTP 405 - Method Not Allowed.
|
||||
|
||||
A request was made of a resource using a request method not supported
|
||||
by that resource.
|
||||
"""
|
||||
http_status = 405
|
||||
message = _("Method Not Allowed")
|
||||
|
||||
|
||||
class NotAcceptable(HTTPClientError):
|
||||
"""HTTP 406 - Not Acceptable.
|
||||
|
||||
The requested resource is only capable of generating content not
|
||||
acceptable according to the Accept headers sent in the request.
|
||||
"""
|
||||
http_status = 406
|
||||
message = _("Not Acceptable")
|
||||
|
||||
|
||||
class ProxyAuthenticationRequired(HTTPClientError):
|
||||
"""HTTP 407 - Proxy Authentication Required.
|
||||
|
||||
The client must first authenticate itself with the proxy.
|
||||
"""
|
||||
http_status = 407
|
||||
message = _("Proxy Authentication Required")
|
||||
|
||||
|
||||
class RequestTimeout(HTTPClientError):
|
||||
"""HTTP 408 - Request Timeout.
|
||||
|
||||
The server timed out waiting for the request.
|
||||
"""
|
||||
http_status = 408
|
||||
message = _("Request Timeout")
|
||||
|
||||
|
||||
class Conflict(HTTPClientError):
|
||||
"""HTTP 409 - Conflict.
|
||||
|
||||
Indicates that the request could not be processed because of conflict
|
||||
in the request, such as an edit conflict.
|
||||
"""
|
||||
http_status = 409
|
||||
message = _("Conflict")
|
||||
|
||||
|
||||
class Gone(HTTPClientError):
|
||||
"""HTTP 410 - Gone.
|
||||
|
||||
Indicates that the resource requested is no longer available and will
|
||||
not be available again.
|
||||
"""
|
||||
http_status = 410
|
||||
message = _("Gone")
|
||||
|
||||
|
||||
class LengthRequired(HTTPClientError):
|
||||
"""HTTP 411 - Length Required.
|
||||
|
||||
The request did not specify the length of its content, which is
|
||||
required by the requested resource.
|
||||
"""
|
||||
http_status = 411
|
||||
message = _("Length Required")
|
||||
|
||||
|
||||
class PreconditionFailed(HTTPClientError):
|
||||
"""HTTP 412 - Precondition Failed.
|
||||
|
||||
The server does not meet one of the preconditions that the requester
|
||||
put on the request.
|
||||
"""
|
||||
http_status = 412
|
||||
message = _("Precondition Failed")
|
||||
|
||||
|
||||
class RequestEntityTooLarge(HTTPClientError):
|
||||
"""HTTP 413 - Request Entity Too Large.
|
||||
|
||||
The request is larger than the server is willing or able to process.
|
||||
"""
|
||||
http_status = 413
|
||||
message = _("Request Entity Too Large")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.retry_after = int(kwargs.pop('retry_after'))
|
||||
except (KeyError, ValueError):
|
||||
self.retry_after = 0
|
||||
|
||||
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class RequestUriTooLong(HTTPClientError):
|
||||
"""HTTP 414 - Request-URI Too Long.
|
||||
|
||||
The URI provided was too long for the server to process.
|
||||
"""
|
||||
http_status = 414
|
||||
message = _("Request-URI Too Long")
|
||||
|
||||
|
||||
class UnsupportedMediaType(HTTPClientError):
|
||||
"""HTTP 415 - Unsupported Media Type.
|
||||
|
||||
The request entity has a media type which the server or resource does
|
||||
not support.
|
||||
"""
|
||||
http_status = 415
|
||||
message = _("Unsupported Media Type")
|
||||
|
||||
|
||||
class RequestedRangeNotSatisfiable(HTTPClientError):
|
||||
"""HTTP 416 - Requested Range Not Satisfiable.
|
||||
|
||||
The client has asked for a portion of the file, but the server cannot
|
||||
supply that portion.
|
||||
"""
|
||||
http_status = 416
|
||||
message = _("Requested Range Not Satisfiable")
|
||||
|
||||
|
||||
class ExpectationFailed(HTTPClientError):
|
||||
"""HTTP 417 - Expectation Failed.
|
||||
|
||||
The server cannot meet the requirements of the Expect request-header field.
|
||||
"""
|
||||
http_status = 417
|
||||
message = _("Expectation Failed")
|
||||
|
||||
|
||||
class UnprocessableEntity(HTTPClientError):
|
||||
"""HTTP 422 - Unprocessable Entity.
|
||||
|
||||
The request was well-formed but was unable to be followed due to semantic
|
||||
errors.
|
||||
"""
|
||||
http_status = 422
|
||||
message = _("Unprocessable Entity")
|
||||
|
||||
|
||||
class InternalServerError(HttpServerError):
|
||||
"""HTTP 500 - Internal Server Error.
|
||||
|
||||
A generic error message, given when no more specific message is suitable.
|
||||
"""
|
||||
http_status = 500
|
||||
message = _("Internal Server Error")
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HttpNotImplemented(HttpServerError):
|
||||
"""HTTP 501 - Not Implemented.
|
||||
|
||||
The server either does not recognize the request method, or it lacks
|
||||
the ability to fulfill the request.
|
||||
"""
|
||||
http_status = 501
|
||||
message = _("Not Implemented")
|
||||
|
||||
|
||||
class BadGateway(HttpServerError):
|
||||
"""HTTP 502 - Bad Gateway.
|
||||
|
||||
The server was acting as a gateway or proxy and received an invalid
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 502
|
||||
message = _("Bad Gateway")
|
||||
|
||||
|
||||
class ServiceUnavailable(HttpServerError):
|
||||
"""HTTP 503 - Service Unavailable.
|
||||
|
||||
The server is currently unavailable.
|
||||
"""
|
||||
http_status = 503
|
||||
message = _("Service Unavailable")
|
||||
|
||||
|
||||
class GatewayTimeout(HttpServerError):
|
||||
"""HTTP 504 - Gateway Timeout.
|
||||
|
||||
The server was acting as a gateway or proxy and did not receive a timely
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 504
|
||||
message = _("Gateway Timeout")
|
||||
|
||||
|
||||
class HttpVersionNotSupported(HttpServerError):
|
||||
"""HTTP 505 - HttpVersion Not Supported.
|
||||
|
||||
The server does not support the HTTP protocol version used in the request.
|
||||
"""
|
||||
http_status = 505
|
||||
message = _("HTTP Version Not Supported")
|
||||
|
||||
|
||||
# _code_map contains all the classes that have http_status attribute.
|
||||
_code_map = dict(
|
||||
(getattr(obj, 'http_status', None), obj)
|
||||
for name, obj in vars(sys.modules[__name__]).items()
|
||||
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
|
||||
)
|
||||
|
||||
|
||||
def from_response(response, method, url):
|
||||
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
||||
|
||||
:param response: instance of `requests.Response` class
|
||||
:param method: HTTP method used for request
|
||||
:param url: URL used for request
|
||||
"""
|
||||
|
||||
req_id = response.headers.get("x-openstack-request-id")
|
||||
# NOTE(hdd) true for older versions of nova and cinder
|
||||
if not req_id:
|
||||
req_id = response.headers.get("x-compute-request-id")
|
||||
kwargs = {
|
||||
"http_status": response.status_code,
|
||||
"response": response,
|
||||
"method": method,
|
||||
"url": url,
|
||||
"request_id": req_id,
|
||||
}
|
||||
if "retry-after" in response.headers:
|
||||
kwargs["retry_after"] = response.headers["retry-after"]
|
||||
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
if content_type.startswith("application/json"):
|
||||
try:
|
||||
body = response.json()
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if isinstance(body, dict):
|
||||
error = body.get(list(body)[0])
|
||||
if isinstance(error, dict):
|
||||
kwargs["message"] = (error.get("message") or
|
||||
error.get("faultstring"))
|
||||
kwargs["details"] = (error.get("details") or
|
||||
six.text_type(body))
|
||||
elif content_type.startswith("text/"):
|
||||
kwargs["details"] = getattr(response, 'text', '')
|
||||
|
||||
try:
|
||||
cls = _code_map[response.status_code]
|
||||
except KeyError:
|
||||
if 500 <= response.status_code < 600:
|
||||
cls = HttpServerError
|
||||
elif 400 <= response.status_code < 500:
|
||||
cls = HTTPClientError
|
||||
else:
|
||||
cls = HttpError
|
||||
return cls(**kwargs)
|
@ -1,98 +0,0 @@
|
||||
#
|
||||
# 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 MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-glanceclient-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
|
||||
from glanceclient._i18n import _
|
||||
from glanceclient.v1.apiclient import exceptions
|
||||
|
||||
|
||||
def find_resource(manager, name_or_id, **find_args):
|
||||
"""Look for resource in a given manager.
|
||||
|
||||
Used as a helper for the _find_* methods.
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def _find_hypervisor(cs, hypervisor):
|
||||
#Get a hypervisor by name or ID.
|
||||
return cliutils.find_resource(cs.hypervisors, hypervisor)
|
||||
"""
|
||||
# first try to get entity as integer id
|
||||
try:
|
||||
return manager.get(int(name_or_id))
|
||||
except (TypeError, ValueError, exceptions.NotFound):
|
||||
pass
|
||||
|
||||
# now try to get entity as uuid
|
||||
try:
|
||||
if six.PY2:
|
||||
tmp_id = encodeutils.safe_encode(name_or_id)
|
||||
else:
|
||||
tmp_id = encodeutils.safe_decode(name_or_id)
|
||||
|
||||
if uuidutils.is_uuid_like(tmp_id):
|
||||
return manager.get(tmp_id)
|
||||
except (TypeError, ValueError, exceptions.NotFound):
|
||||
pass
|
||||
|
||||
# for str id which is not uuid
|
||||
if getattr(manager, 'is_alphanum_id_allowed', False):
|
||||
try:
|
||||
return manager.get(name_or_id)
|
||||
except exceptions.NotFound:
|
||||
pass
|
||||
|
||||
try:
|
||||
try:
|
||||
return manager.find(human_id=name_or_id, **find_args)
|
||||
except exceptions.NotFound:
|
||||
pass
|
||||
|
||||
# finally try to find entity by name
|
||||
try:
|
||||
resource = getattr(manager, 'resource_class', None)
|
||||
name_attr = resource.NAME_ATTR if resource else 'name'
|
||||
kwargs = {name_attr: name_or_id}
|
||||
kwargs.update(find_args)
|
||||
return manager.find(**kwargs)
|
||||
except exceptions.NotFound:
|
||||
msg = _("No %(name)s with a name or "
|
||||
"ID of '%(name_or_id)s' exists.") % {
|
||||
"name": manager.resource_class.__name__.lower(),
|
||||
"name_or_id": name_or_id
|
||||
}
|
||||
raise exceptions.CommandError(msg)
|
||||
except exceptions.NoUniqueMatch:
|
||||
msg = _("Multiple %(name)s matches found for "
|
||||
"'%(name_or_id)s', use an ID to be more specific.") % {
|
||||
"name": manager.resource_class.__name__.lower(),
|
||||
"name_or_id": name_or_id
|
||||
}
|
||||
raise exceptions.CommandError(msg)
|
@ -1,41 +0,0 @@
|
||||
# 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 glanceclient.common import http
|
||||
from glanceclient.common import utils
|
||||
from glanceclient.v1 import image_members
|
||||
from glanceclient.v1 import images
|
||||
from glanceclient.v1 import versions
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Client for the OpenStack Images v1 API.
|
||||
|
||||
:param string endpoint: A user-supplied endpoint URL for the glance
|
||||
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 glance.
|
||||
"""
|
||||
|
||||
def __init__(self, endpoint=None, **kwargs):
|
||||
"""Initialize a new client for the Images v1 API."""
|
||||
endpoint, self.version = utils.endpoint_version_from_url(endpoint, 1.0)
|
||||
self.http_client = http.get_http_client(endpoint=endpoint, **kwargs)
|
||||
self.images = images.ImageManager(self.http_client)
|
||||
self.image_members = image_members.ImageMemberManager(self.http_client)
|
||||
self.versions = versions.VersionManager(self.http_client)
|
@ -1,103 +0,0 @@
|
||||
# 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 glanceclient.v1.apiclient import base
|
||||
|
||||
|
||||
class ImageMember(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<ImageMember %s>" % self._info
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.member_id
|
||||
|
||||
def delete(self):
|
||||
self.manager.delete(self)
|
||||
|
||||
|
||||
class ImageMemberManager(base.ManagerWithFind):
|
||||
resource_class = ImageMember
|
||||
|
||||
def get(self, image, member_id):
|
||||
image_id = base.getid(image)
|
||||
url = '/v1/images/%s/members/%s' % (image_id, member_id)
|
||||
resp, body = self.client.get(url)
|
||||
member = body['member']
|
||||
member['image_id'] = image_id
|
||||
return ImageMember(self, member, loaded=True)
|
||||
|
||||
def list(self, image=None, member=None):
|
||||
out = []
|
||||
if image and member:
|
||||
try:
|
||||
out.append(self.get(image, member))
|
||||
# TODO(bcwaldon): narrow this down to 404
|
||||
except Exception:
|
||||
pass
|
||||
elif image:
|
||||
out.extend(self._list_by_image(image))
|
||||
elif member:
|
||||
out.extend(self._list_by_member(member))
|
||||
else:
|
||||
# TODO(bcwaldon): figure out what is appropriate to do here as we
|
||||
# are unable to provide the requested response
|
||||
pass
|
||||
return out
|
||||
|
||||
def _list_by_image(self, image):
|
||||
image_id = base.getid(image)
|
||||
url = '/v1/images/%s/members' % image_id
|
||||
resp, body = self.client.get(url)
|
||||
out = []
|
||||
for member in body['members']:
|
||||
member['image_id'] = image_id
|
||||
out.append(ImageMember(self, member, loaded=True))
|
||||
return out
|
||||
|
||||
def _list_by_member(self, member):
|
||||
member_id = base.getid(member)
|
||||
url = '/v1/shared-images/%s' % member_id
|
||||
resp, body = self.client.get(url)
|
||||
out = []
|
||||
for member in body['shared_images']:
|
||||
member['member_id'] = member_id
|
||||
out.append(ImageMember(self, member, loaded=True))
|
||||
return out
|
||||
|
||||
def delete(self, image_id, member_id):
|
||||
self._delete("/v1/images/%s/members/%s" % (image_id, member_id))
|
||||
|
||||
def create(self, image, member_id, can_share=False):
|
||||
"""Creates an image."""
|
||||
url = '/v1/images/%s/members/%s' % (base.getid(image), member_id)
|
||||
body = {'member': {'can_share': can_share}}
|
||||
self.client.put(url, data=body)
|
||||
|
||||
def replace(self, image, members):
|
||||
memberships = []
|
||||
for member in members:
|
||||
try:
|
||||
obj = {
|
||||
'member_id': member.member_id,
|
||||
'can_share': member.can_share,
|
||||
}
|
||||
except AttributeError:
|
||||
obj = {'member_id': member['member_id']}
|
||||
if 'can_share' in member:
|
||||
obj['can_share'] = member['can_share']
|
||||
memberships.append(obj)
|
||||
url = '/v1/images/%s/members' % base.getid(image)
|
||||
self.client.put(url, data={'memberships': memberships})
|
@ -1,370 +0,0 @@
|
||||
# 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
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from glanceclient.common import utils
|
||||
from glanceclient.v1.apiclient import base
|
||||
|
||||
UPDATE_PARAMS = ('name', 'disk_format', 'container_format', 'min_disk',
|
||||
'min_ram', 'owner', 'size', 'is_public', 'protected',
|
||||
'location', 'checksum', 'copy_from', 'properties',
|
||||
# NOTE(bcwaldon: an attempt to update 'deleted' will be
|
||||
# ignored, but we need to support it for backwards-
|
||||
# compatibility with the legacy client library
|
||||
'deleted')
|
||||
|
||||
CREATE_PARAMS = UPDATE_PARAMS + ('id', 'store')
|
||||
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
SORT_DIR_VALUES = ('asc', 'desc')
|
||||
SORT_KEY_VALUES = ('name', 'status', 'container_format', 'disk_format',
|
||||
'size', 'id', 'created_at', 'updated_at')
|
||||
|
||||
OS_REQ_ID_HDR = 'x-openstack-request-id'
|
||||
|
||||
|
||||
class Image(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<Image %s>" % self._info
|
||||
|
||||
def update(self, **fields):
|
||||
self.manager.update(self, **fields)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
return self.manager.delete(self)
|
||||
|
||||
def data(self, **kwargs):
|
||||
return self.manager.data(self, **kwargs)
|
||||
|
||||
|
||||
class ImageManager(base.ManagerWithFind):
|
||||
resource_class = Image
|
||||
|
||||
def _list(self, url, response_key, obj_class=None, body=None):
|
||||
resp, body = self.client.get(url)
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
|
||||
data = body[response_key]
|
||||
return ([obj_class(self, res, loaded=True) for res in data if res],
|
||||
resp)
|
||||
|
||||
def _image_meta_from_headers(self, headers):
|
||||
meta = {'properties': {}}
|
||||
safe_decode = encodeutils.safe_decode
|
||||
for key, value in headers.items():
|
||||
# NOTE(flaper87): this is a compatibility fix
|
||||
# for urllib3 >= 1.11. Please, refer to this
|
||||
# bug for more info:
|
||||
# https://bugs.launchpad.net/python-glanceclient/+bug/1487645
|
||||
key = key.lower()
|
||||
value = safe_decode(value, incoming='utf-8')
|
||||
if key.startswith('x-image-meta-property-'):
|
||||
_key = safe_decode(key[22:], incoming='utf-8')
|
||||
meta['properties'][_key] = value
|
||||
elif key.startswith('x-image-meta-'):
|
||||
_key = safe_decode(key[13:], incoming='utf-8')
|
||||
meta[_key] = value
|
||||
|
||||
for key in ['is_public', 'protected', 'deleted']:
|
||||
if key in meta:
|
||||
meta[key] = strutils.bool_from_string(meta[key])
|
||||
|
||||
return self._format_image_meta_for_user(meta)
|
||||
|
||||
def _image_meta_to_headers(self, fields):
|
||||
headers = {}
|
||||
fields_copy = copy.deepcopy(fields)
|
||||
|
||||
# NOTE(flaper87): Convert to str, headers
|
||||
# that are not instance of basestring. All
|
||||
# headers will be encoded later, before the
|
||||
# request is sent.
|
||||
def to_str(value):
|
||||
if not isinstance(value, six.string_types):
|
||||
return str(value)
|
||||
return value
|
||||
|
||||
for key, value in fields_copy.pop('properties', {}).items():
|
||||
headers['x-image-meta-property-%s' % key] = to_str(value)
|
||||
for key, value in fields_copy.items():
|
||||
headers['x-image-meta-%s' % key] = to_str(value)
|
||||
return headers
|
||||
|
||||
@staticmethod
|
||||
def _format_image_meta_for_user(meta):
|
||||
for key in ['size', 'min_ram', 'min_disk']:
|
||||
if key in meta:
|
||||
try:
|
||||
meta[key] = int(meta[key]) if meta[key] else 0
|
||||
except ValueError:
|
||||
pass
|
||||
return meta
|
||||
|
||||
def get(self, image, **kwargs):
|
||||
"""Get the metadata for a specific image.
|
||||
|
||||
:param image: image object or id to look up
|
||||
:rtype: :class:`Image`
|
||||
"""
|
||||
image_id = base.getid(image)
|
||||
resp, body = self.client.head('/v1/images/%s'
|
||||
% urlparse.quote(str(image_id)))
|
||||
meta = self._image_meta_from_headers(resp.headers)
|
||||
return_request_id = kwargs.get('return_req_id', None)
|
||||
if return_request_id is not None:
|
||||
return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
|
||||
return Image(self, meta)
|
||||
|
||||
def data(self, image, do_checksum=True, **kwargs):
|
||||
"""Get the raw data for a specific image.
|
||||
|
||||
:param image: image object or id to look up
|
||||
:param do_checksum: Enable/disable checksum validation
|
||||
:rtype: iterable containing image data
|
||||
"""
|
||||
image_id = base.getid(image)
|
||||
resp, body = self.client.get('/v1/images/%s'
|
||||
% urlparse.quote(str(image_id)))
|
||||
content_length = int(resp.headers.get('content-length', 0))
|
||||
checksum = resp.headers.get('x-image-meta-checksum', None)
|
||||
if do_checksum and checksum is not None:
|
||||
body = utils.integrity_iter(body, checksum)
|
||||
return_request_id = kwargs.get('return_req_id', None)
|
||||
if return_request_id is not None:
|
||||
return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
|
||||
|
||||
return utils.IterableWithLength(body, content_length)
|
||||
|
||||
def _build_params(self, parameters):
|
||||
params = {'limit': parameters.get('page_size', DEFAULT_PAGE_SIZE)}
|
||||
|
||||
if 'marker' in parameters:
|
||||
params['marker'] = parameters['marker']
|
||||
|
||||
sort_key = parameters.get('sort_key')
|
||||
if sort_key is not None:
|
||||
if sort_key in SORT_KEY_VALUES:
|
||||
params['sort_key'] = sort_key
|
||||
else:
|
||||
raise ValueError('sort_key must be one of the following: %s.'
|
||||
% ', '.join(SORT_KEY_VALUES))
|
||||
|
||||
sort_dir = parameters.get('sort_dir')
|
||||
if sort_dir is not None:
|
||||
if sort_dir in SORT_DIR_VALUES:
|
||||
params['sort_dir'] = sort_dir
|
||||
else:
|
||||
raise ValueError('sort_dir must be one of the following: %s.'
|
||||
% ', '.join(SORT_DIR_VALUES))
|
||||
|
||||
filters = parameters.get('filters', {})
|
||||
properties = filters.pop('properties', {})
|
||||
for key, value in properties.items():
|
||||
params['property-%s' % key] = value
|
||||
params.update(filters)
|
||||
if parameters.get('owner') is not None:
|
||||
params['is_public'] = None
|
||||
if 'is_public' in parameters:
|
||||
params['is_public'] = parameters['is_public']
|
||||
|
||||
return params
|
||||
|
||||
def list(self, **kwargs):
|
||||
"""Get a list of images.
|
||||
|
||||
:param page_size: number of items to request in each paginated request
|
||||
:param limit: maximum number of images to return
|
||||
:param marker: begin returning images that appear later in the image
|
||||
list than that represented by this image id
|
||||
:param filters: dict of direct comparison filters that mimics the
|
||||
structure of an image object
|
||||
:param owner: If provided, only images with this owner (tenant id)
|
||||
will be listed. An empty string ('') matches ownerless
|
||||
images.
|
||||
:param return_req_id: If an empty list is provided, populate this
|
||||
list with the request ID value from the header
|
||||
x-openstack-request-id
|
||||
:rtype: list of :class:`Image`
|
||||
"""
|
||||
absolute_limit = kwargs.get('limit')
|
||||
page_size = kwargs.get('page_size', DEFAULT_PAGE_SIZE)
|
||||
owner = kwargs.get('owner', None)
|
||||
|
||||
def filter_owner(owner, image):
|
||||
# If client side owner 'filter' is specified
|
||||
# only return images that match 'owner'.
|
||||
if owner is None:
|
||||
# Do not filter based on owner
|
||||
return False
|
||||
if (not hasattr(image, 'owner')) or image.owner is None:
|
||||
# ownerless image
|
||||
return not (owner == '')
|
||||
else:
|
||||
return not (image.owner == owner)
|
||||
|
||||
def paginate(qp, return_request_id=None):
|
||||
for param, value in qp.items():
|
||||
if isinstance(value, six.string_types):
|
||||
# Note(flaper87) Url encoding should
|
||||
# be moved inside http utils, at least
|
||||
# shouldn't be here.
|
||||
#
|
||||
# Making sure all params are str before
|
||||
# trying to encode them
|
||||
qp[param] = encodeutils.safe_decode(value)
|
||||
|
||||
url = '/v1/images/detail?%s' % urlparse.urlencode(qp)
|
||||
images, resp = self._list(url, "images")
|
||||
|
||||
if return_request_id is not None:
|
||||
return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
|
||||
|
||||
for image in images:
|
||||
yield image
|
||||
|
||||
return_request_id = kwargs.get('return_req_id', None)
|
||||
|
||||
params = self._build_params(kwargs)
|
||||
|
||||
seen = 0
|
||||
while True:
|
||||
seen_last_page = 0
|
||||
filtered = 0
|
||||
for image in paginate(params, return_request_id):
|
||||
last_image = image.id
|
||||
|
||||
if filter_owner(owner, image):
|
||||
# Note(kragniz): ignore this image
|
||||
filtered += 1
|
||||
continue
|
||||
|
||||
if (absolute_limit is not None and
|
||||
seen + seen_last_page >= absolute_limit):
|
||||
# Note(kragniz): we've seen enough images
|
||||
return
|
||||
else:
|
||||
seen_last_page += 1
|
||||
yield image
|
||||
|
||||
seen += seen_last_page
|
||||
|
||||
if seen_last_page + filtered == 0:
|
||||
# Note(kragniz): we didn't get any images in the last page
|
||||
return
|
||||
|
||||
if absolute_limit is not None and seen >= absolute_limit:
|
||||
# Note(kragniz): reached the limit of images to return
|
||||
return
|
||||
|
||||
if page_size and seen_last_page + filtered < page_size:
|
||||
# Note(kragniz): we've reached the last page of the images
|
||||
return
|
||||
|
||||
# Note(kragniz): there are more images to come
|
||||
params['marker'] = last_image
|
||||
seen_last_page = 0
|
||||
|
||||
def delete(self, image, **kwargs):
|
||||
"""Delete an image."""
|
||||
url = "/v1/images/%s" % base.getid(image)
|
||||
resp, body = self.client.delete(url)
|
||||
return_request_id = kwargs.get('return_req_id', None)
|
||||
if return_request_id is not None:
|
||||
return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
|
||||
|
||||
def create(self, **kwargs):
|
||||
"""Create an image.
|
||||
|
||||
TODO(bcwaldon): document accepted params
|
||||
"""
|
||||
image_data = kwargs.pop('data', None)
|
||||
if image_data is not None:
|
||||
image_size = utils.get_file_size(image_data)
|
||||
if image_size is not None:
|
||||
kwargs.setdefault('size', image_size)
|
||||
|
||||
fields = {}
|
||||
for field in kwargs:
|
||||
if field in CREATE_PARAMS:
|
||||
fields[field] = kwargs[field]
|
||||
elif field == 'return_req_id':
|
||||
continue
|
||||
else:
|
||||
msg = 'create() got an unexpected keyword argument \'%s\''
|
||||
raise TypeError(msg % field)
|
||||
|
||||
copy_from = fields.pop('copy_from', None)
|
||||
hdrs = self._image_meta_to_headers(fields)
|
||||
if copy_from is not None:
|
||||
hdrs['x-glance-api-copy-from'] = copy_from
|
||||
|
||||
resp, body = self.client.post('/v1/images',
|
||||
headers=hdrs,
|
||||
data=image_data)
|
||||
return_request_id = kwargs.get('return_req_id', None)
|
||||
if return_request_id is not None:
|
||||
return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
|
||||
|
||||
return Image(self, self._format_image_meta_for_user(body['image']))
|
||||
|
||||
def update(self, image, **kwargs):
|
||||
"""Update an image.
|
||||
|
||||
TODO(bcwaldon): document accepted params
|
||||
"""
|
||||
image_data = kwargs.pop('data', None)
|
||||
if image_data is not None:
|
||||
image_size = utils.get_file_size(image_data)
|
||||
if image_size is not None:
|
||||
kwargs.setdefault('size', image_size)
|
||||
|
||||
hdrs = {}
|
||||
purge_props = 'false'
|
||||
purge_props_bool = kwargs.pop('purge_props', None)
|
||||
if purge_props_bool:
|
||||
purge_props = 'true'
|
||||
|
||||
hdrs['x-glance-registry-purge-props'] = purge_props
|
||||
fields = {}
|
||||
for field in kwargs:
|
||||
if field in UPDATE_PARAMS:
|
||||
fields[field] = kwargs[field]
|
||||
elif field == 'return_req_id':
|
||||
continue
|
||||
else:
|
||||
msg = 'update() got an unexpected keyword argument \'%s\''
|
||||
raise TypeError(msg % field)
|
||||
|
||||
copy_from = fields.pop('copy_from', None)
|
||||
hdrs.update(self._image_meta_to_headers(fields))
|
||||
if copy_from is not None:
|
||||
hdrs['x-glance-api-copy-from'] = copy_from
|
||||
|
||||
url = '/v1/images/%s' % base.getid(image)
|
||||
resp, body = self.client.put(url, headers=hdrs, data=image_data)
|
||||
return_request_id = kwargs.get('return_req_id', None)
|
||||
if return_request_id is not None:
|
||||
return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None))
|
||||
|
||||
return Image(self, self._format_image_meta_for_user(body['image']))
|
@ -1,428 +0,0 @@
|
||||
# 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 copy
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import strutils
|
||||
|
||||
from glanceclient.common import progressbar
|
||||
from glanceclient.common import utils
|
||||
from glanceclient import exc
|
||||
import glanceclient.v1.images
|
||||
|
||||
CONTAINER_FORMATS = 'Acceptable formats: ami, ari, aki, bare, and ovf.'
|
||||
DISK_FORMATS = ('Acceptable formats: ami, ari, aki, vhd, vmdk, raw, '
|
||||
'qcow2, vdi, iso, and ploop.')
|
||||
DATA_FIELDS = ('location', 'copy_from', 'file')
|
||||
|
||||
_bool_strict = functools.partial(strutils.bool_from_string, strict=True)
|
||||
|
||||
|
||||
@utils.arg('--name', metavar='<NAME>',
|
||||
help='Filter images to those that have this name.')
|
||||
@utils.arg('--status', metavar='<STATUS>',
|
||||
help='Filter images to those that have this status.')
|
||||
@utils.arg('--changes-since', metavar='<CHANGES_SINCE>',
|
||||
help='Filter images to those that changed since the given time'
|
||||
', which will include the deleted images.')
|
||||
@utils.arg('--container-format', metavar='<CONTAINER_FORMAT>',
|
||||
help='Filter images to those that have this container format. '
|
||||
+ CONTAINER_FORMATS)
|
||||
@utils.arg('--disk-format', metavar='<DISK_FORMAT>',
|
||||
help='Filter images to those that have this disk format. '
|
||||
+ DISK_FORMATS)
|
||||
@utils.arg('--size-min', metavar='<SIZE>', type=int,
|
||||
help='Filter images to those with a size greater than this.')
|
||||
@utils.arg('--size-max', metavar='<SIZE>', type=int,
|
||||
help='Filter images to those with a size less than this.')
|
||||
@utils.arg('--property-filter', metavar='<KEY=VALUE>',
|
||||
help="Filter images by a user-defined image property.",
|
||||
action='append', dest='properties', default=[])
|
||||
@utils.arg('--page-size', metavar='<SIZE>', default=None, type=int,
|
||||
help='Number of images to request in each paginated request.')
|
||||
@utils.arg('--human-readable', action='store_true', default=False,
|
||||
help='Print image size in a human-friendly format.')
|
||||
@utils.arg('--sort-key', default='name',
|
||||
choices=glanceclient.v1.images.SORT_KEY_VALUES,
|
||||
help='Sort image list by specified field.')
|
||||
@utils.arg('--sort-dir', default='asc',
|
||||
choices=glanceclient.v1.images.SORT_DIR_VALUES,
|
||||
help='Sort image list in specified direction.')
|
||||
@utils.arg('--is-public',
|
||||
type=_bool_strict, metavar='{True,False}',
|
||||
help=('Allows the user to select a listing of public or non '
|
||||
'public images.'))
|
||||
@utils.arg('--owner', default=None, metavar='<TENANT_ID>',
|
||||
help='Display only images owned by this tenant id. Filtering '
|
||||
'occurs on the client side so may be inefficient. This option '
|
||||
'is mainly intended for admin use. Use an empty string (\'\') '
|
||||
'to list images with no owner. Note: This option overrides '
|
||||
'the --is-public argument if present. Note: the v2 API '
|
||||
'supports more efficient server-side owner based filtering.')
|
||||
@utils.arg('--all-tenants', action='store_true', default=False,
|
||||
help=('Allows the admin user to list all images '
|
||||
'irrespective of the image\'s owner or is_public value.'))
|
||||
def do_image_list(gc, args):
|
||||
"""List images you can access."""
|
||||
filter_keys = ['name', 'status', 'container_format', 'disk_format',
|
||||
'size_min', 'size_max', 'is_public', 'changes_since']
|
||||
filter_items = [(key, getattr(args, key)) for key in filter_keys]
|
||||
filters = dict([item for item in filter_items if item[1] is not None])
|
||||
|
||||
if 'changes_since' in filters:
|
||||
filters['changes-since'] = filters.pop('changes_since')
|
||||
|
||||
if args.properties:
|
||||
property_filter_items = [p.split('=', 1) for p in args.properties]
|
||||
if any(len(pair) != 2 for pair in property_filter_items):
|
||||
utils.exit('Argument --property-filter requires properties in the'
|
||||
' format KEY=VALUE')
|
||||
|
||||
filters['properties'] = dict(property_filter_items)
|
||||
|
||||
kwargs = {'filters': filters}
|
||||
if args.page_size is not None:
|
||||
kwargs['page_size'] = args.page_size
|
||||
|
||||
kwargs['sort_key'] = args.sort_key
|
||||
kwargs['sort_dir'] = args.sort_dir
|
||||
kwargs['owner'] = args.owner
|
||||
if args.all_tenants is True:
|
||||
kwargs['is_public'] = None
|
||||
|
||||
images = gc.images.list(**kwargs)
|
||||
|
||||
if args.human_readable:
|
||||
def convert_size(image):
|
||||
image.size = utils.make_size_human_readable(image.size)
|
||||
return image
|
||||
|
||||
images = (convert_size(image) for image in images)
|
||||
|
||||
columns = ['ID', 'Name', 'Disk Format', 'Container Format',
|
||||
'Size', 'Status']
|
||||
utils.print_list(images, columns)
|
||||
|
||||
|
||||
def _image_show(image, human_readable=False, max_column_width=80):
|
||||
# Flatten image properties dict for display
|
||||
info = copy.deepcopy(image._info)
|
||||
if human_readable:
|
||||
info['size'] = utils.make_size_human_readable(info['size'])
|
||||
for (k, v) in info.pop('properties').items():
|
||||
info['Property \'%s\'' % k] = v
|
||||
|
||||
utils.print_dict(info, max_column_width=max_column_width)
|
||||
|
||||
|
||||
def _set_data_field(fields, args):
|
||||
if 'location' not in fields and 'copy_from' not in fields:
|
||||
fields['data'] = utils.get_data_file(args)
|
||||
|
||||
|
||||
@utils.arg('image', metavar='<IMAGE>', help='Name or ID of image to describe.')
|
||||
@utils.arg('--human-readable', action='store_true', default=False,
|
||||
help='Print image size in a human-friendly format.')
|
||||
@utils.arg('--max-column-width', metavar='<integer>', default=80,
|
||||
help='The max column width of the printed table.')
|
||||
def do_image_show(gc, args):
|
||||
"""Describe a specific image."""
|
||||
image_id = utils.find_resource(gc.images, args.image).id
|
||||
image = gc.images.get(image_id)
|
||||
_image_show(image, args.human_readable,
|
||||
max_column_width=int(args.max_column_width))
|
||||
|
||||
|
||||
@utils.arg('--file', metavar='<FILE>',
|
||||
help='Local file to save downloaded image data to. '
|
||||
'If this is not specified and there is no redirection '
|
||||
'the image data will not be saved.')
|
||||
@utils.arg('image', metavar='<IMAGE>', help='Name or ID of image to download.')
|
||||
@utils.arg('--progress', action='store_true', default=False,
|
||||
help='Show download progress bar.')
|
||||
def do_image_download(gc, args):
|
||||
"""Download a specific image."""
|
||||
image = utils.find_resource(gc.images, args.image)
|
||||
body = image.data()
|
||||
if args.progress:
|
||||
body = progressbar.VerboseIteratorWrapper(body, len(body))
|
||||
if not (sys.stdout.isatty() and args.file is None):
|
||||
utils.save_image(body, args.file)
|
||||
else:
|
||||
print('No redirection or local file specified for downloaded image '
|
||||
'data. Please specify a local file with --file to save '
|
||||
'downloaded image or redirect output to another source.')
|
||||
|
||||
|
||||
@utils.arg('--id', metavar='<IMAGE_ID>',
|
||||
help='ID of image to reserve.')
|
||||
@utils.arg('--name', metavar='<NAME>',
|
||||
help='Name of image.')
|
||||
@utils.arg('--store', metavar='<STORE>',
|
||||
help='Store to upload image to.')
|
||||
@utils.arg('--disk-format', metavar='<DISK_FORMAT>',
|
||||
help='Disk format of image. ' + DISK_FORMATS)
|
||||
@utils.arg('--container-format', metavar='<CONTAINER_FORMAT>',
|
||||
help='Container format of image. ' + CONTAINER_FORMATS)
|
||||
@utils.arg('--owner', metavar='<TENANT_ID>',
|
||||
help='Tenant who should own image.')
|
||||
@utils.arg('--size', metavar='<SIZE>', type=int,
|
||||
help=('Size of image data (in bytes). Only used with'
|
||||
' \'--location\' and \'--copy_from\'.'))
|
||||
@utils.arg('--min-disk', metavar='<DISK_GB>', type=int,
|
||||
help='Minimum size of disk needed to boot image (in gigabytes).')
|
||||
@utils.arg('--min-ram', metavar='<DISK_RAM>', type=int,
|
||||
help='Minimum amount of ram needed to boot image (in megabytes).')
|
||||
@utils.arg('--location', metavar='<IMAGE_URL>',
|
||||
help=('URL where the data for this image already resides. For '
|
||||
'example, if the image data is stored in swift, you could '
|
||||
'specify \'swift+http://tenant%%3Aaccount:key@auth_url/'
|
||||
'v2.0/container/obj\'. '
|
||||
'(Note: \'%%3A\' is \':\' URL encoded.)'))
|
||||
@utils.arg('--file', metavar='<FILE>',
|
||||
help=('Local file that contains disk image to be uploaded during'
|
||||
' creation. Alternatively, images can be passed to the client'
|
||||
' via stdin.'))
|
||||
@utils.arg('--checksum', metavar='<CHECKSUM>',
|
||||
help=('Hash of image data used Glance can use for verification.'
|
||||
' Provide a md5 checksum here.'))
|
||||
@utils.arg('--copy-from', metavar='<IMAGE_URL>',
|
||||
help=('Similar to \'--location\' in usage, but this indicates that'
|
||||
' the Glance server should immediately copy the data and'
|
||||
' store it in its configured image store.'))
|
||||
@utils.arg('--is-public',
|
||||
type=_bool_strict, metavar='{True,False}',
|
||||
help='Make image accessible to the public.')
|
||||
@utils.arg('--is-protected',
|
||||
type=_bool_strict, metavar='{True,False}',
|
||||
help='Prevent image from being deleted.')
|
||||
@utils.arg('--property', metavar="<key=value>", action='append', default=[],
|
||||
help=("Arbitrary property to associate with image. "
|
||||
"May be used multiple times."))
|
||||
@utils.arg('--human-readable', action='store_true', default=False,
|
||||
help='Print image size in a human-friendly format.')
|
||||
@utils.arg('--progress', action='store_true', default=False,
|
||||
help='Show upload progress bar.')
|
||||
@utils.on_data_require_fields(DATA_FIELDS)
|
||||
def do_image_create(gc, args):
|
||||
"""Create a new image."""
|
||||
# Filter out None values
|
||||
fields = dict(filter(lambda x: x[1] is not None, vars(args).items()))
|
||||
|
||||
fields['is_public'] = fields.get('is_public')
|
||||
|
||||
if 'is_protected' in fields:
|
||||
fields['protected'] = fields.pop('is_protected')
|
||||
|
||||
raw_properties = fields.pop('property')
|
||||
fields['properties'] = {}
|
||||
for datum in raw_properties:
|
||||
key, value = datum.split('=', 1)
|
||||
fields['properties'][key] = value
|
||||
|
||||
# Filter out values we can't use
|
||||
CREATE_PARAMS = glanceclient.v1.images.CREATE_PARAMS
|
||||
fields = dict(filter(lambda x: x[0] in CREATE_PARAMS, fields.items()))
|
||||
|
||||
_set_data_field(fields, args)
|
||||
|
||||
# Only show progress bar for local image files
|
||||
if fields.get('data') and args.progress:
|
||||
filesize = utils.get_file_size(fields['data'])
|
||||
if filesize is not None:
|
||||
# NOTE(kragniz): do not show a progress bar if the size of the
|
||||
# input is unknown (most likely a piped input)
|
||||
fields['data'] = progressbar.VerboseFileWrapper(
|
||||
fields['data'], filesize
|
||||
)
|
||||
|
||||
image = gc.images.create(**fields)
|
||||
_image_show(image, args.human_readable)
|
||||
|
||||
|
||||
def _is_image_data_provided(args):
|
||||
"""Return True if some image data has probably been provided by the user"""
|
||||
# NOTE(kragniz): Check stdin works, then check is there is any data
|
||||
# on stdin or a filename has been provided with --file
|
||||
try:
|
||||
os.fstat(0)
|
||||
except OSError:
|
||||
return False
|
||||
return not sys.stdin.isatty() or args.file or args.copy_from
|
||||
|
||||
|
||||
@utils.arg('image', metavar='<IMAGE>', help='Name or ID of image to modify.')
|
||||
@utils.arg('--name', metavar='<NAME>',
|
||||
help='Name of image.')
|
||||
@utils.arg('--disk-format', metavar='<DISK_FORMAT>',
|
||||
help='Disk format of image. ' + DISK_FORMATS)
|
||||
@utils.arg('--container-format', metavar='<CONTAINER_FORMAT>',
|
||||
help='Container format of image. ' + CONTAINER_FORMATS)
|
||||
@utils.arg('--owner', metavar='<TENANT_ID>',
|
||||
help='Tenant who should own image.')
|
||||
@utils.arg('--size', metavar='<SIZE>', type=int,
|
||||
help='Size of image data (in bytes).')
|
||||
@utils.arg('--min-disk', metavar='<DISK_GB>', type=int,
|
||||
help='Minimum size of disk needed to boot image (in gigabytes).')
|
||||
@utils.arg('--min-ram', metavar='<DISK_RAM>', type=int,
|
||||
help='Minimum amount of ram needed to boot image (in megabytes).')
|
||||
@utils.arg('--location', metavar='<IMAGE_URL>',
|
||||
help=('URL where the data for this image already resides. For '
|
||||
'example, if the image data is stored in swift, you could '
|
||||
'specify \'swift+http://tenant%%3Aaccount:key@auth_url/'
|
||||
'v2.0/container/obj\'. '
|
||||
'(Note: \'%%3A\' is \':\' URL encoded.) '
|
||||
'This option only works for images in \'queued\' status.'))
|
||||
@utils.arg('--file', metavar='<FILE>',
|
||||
help=('Local file that contains disk image to be uploaded during'
|
||||
' update. Alternatively, images can be passed to the client'
|
||||
' via stdin.'))
|
||||
@utils.arg('--checksum', metavar='<CHECKSUM>',
|
||||
help='Hash of image data used Glance can use for verification.')
|
||||
@utils.arg('--copy-from', metavar='<IMAGE_URL>',
|
||||
help=('Similar to \'--location\' in usage, but this indicates that'
|
||||
' the Glance server should immediately copy the data and'
|
||||
' store it in its configured image store.'
|
||||
' This option only works for images in \'queued\' status.'))
|
||||
@utils.arg('--is-public',
|
||||
type=_bool_strict, metavar='{True,False}',
|
||||
help='Make image accessible to the public.')
|
||||
@utils.arg('--is-protected',
|
||||
type=_bool_strict, metavar='{True,False}',
|
||||
help='Prevent image from being deleted.')
|
||||
@utils.arg('--property', metavar="<key=value>", action='append', default=[],
|
||||
help=("Arbitrary property to associate with image. "
|
||||
"May be used multiple times."))
|
||||
@utils.arg('--purge-props', action='store_true', default=False,
|
||||
help=("If this flag is present, delete all image properties "
|
||||
"not explicitly set in the update request. Otherwise, "
|
||||
"those properties not referenced are preserved."))
|
||||
@utils.arg('--human-readable', action='store_true', default=False,
|
||||
help='Print image size in a human-friendly format.')
|
||||
@utils.arg('--progress', action='store_true', default=False,
|
||||
help='Show upload progress bar.')
|
||||
def do_image_update(gc, args):
|
||||
"""Update a specific image."""
|
||||
# Filter out None values
|
||||
fields = dict(filter(lambda x: x[1] is not None, vars(args).items()))
|
||||
|
||||
image_arg = fields.pop('image')
|
||||
image = utils.find_resource(gc.images, image_arg)
|
||||
|
||||
if 'is_protected' in fields:
|
||||
fields['protected'] = fields.pop('is_protected')
|
||||
|
||||
raw_properties = fields.pop('property')
|
||||
fields['properties'] = {}
|
||||
for datum in raw_properties:
|
||||
key, value = datum.split('=', 1)
|
||||
fields['properties'][key] = value
|
||||
|
||||
# Filter out values we can't use
|
||||
UPDATE_PARAMS = glanceclient.v1.images.UPDATE_PARAMS
|
||||
fields = dict(filter(lambda x: x[0] in UPDATE_PARAMS, fields.items()))
|
||||
|
||||
if image.status == 'queued':
|
||||
_set_data_field(fields, args)
|
||||
|
||||
if args.progress:
|
||||
filesize = utils.get_file_size(fields['data'])
|
||||
fields['data'] = progressbar.VerboseFileWrapper(
|
||||
fields['data'], filesize
|
||||
)
|
||||
|
||||
elif _is_image_data_provided(args):
|
||||
# NOTE(kragniz): Exit with an error if the status is not queued
|
||||
# and image data was provided
|
||||
utils.exit('Unable to upload image data to an image which '
|
||||
'is %s.' % image.status)
|
||||
|
||||
image = gc.images.update(image, purge_props=args.purge_props, **fields)
|
||||
_image_show(image, args.human_readable)
|
||||
|
||||
|
||||
@utils.arg('images', metavar='<IMAGE>', nargs='+',
|
||||
help='Name or ID of image(s) to delete.')
|
||||
def do_image_delete(gc, args):
|
||||
"""Delete specified image(s)."""
|
||||
for args_image in args.images:
|
||||
image = utils.find_resource(gc.images, args_image)
|
||||
if image and image.status == "deleted":
|
||||
msg = "No image with an ID of '%s' exists." % image.id
|
||||
raise exc.CommandError(msg)
|
||||
try:
|
||||
if args.verbose:
|
||||
print('Requesting image delete for %s ...' %
|
||||
encodeutils.safe_decode(args_image), end=' ')
|
||||
|
||||
gc.images.delete(image)
|
||||
|
||||
if args.verbose:
|
||||
print('[Done]')
|
||||
|
||||
except exc.HTTPException as e:
|
||||
if args.verbose:
|
||||
print('[Fail]')
|
||||
print('%s: Unable to delete image %s' % (e, args_image))
|
||||
|
||||
|
||||
@utils.arg('--image-id', metavar='<IMAGE_ID>',
|
||||
help='Filter results by an image ID.')
|
||||
@utils.arg('--tenant-id', metavar='<TENANT_ID>',
|
||||
help='Filter results by a tenant ID.')
|
||||
def do_member_list(gc, args):
|
||||
"""Describe sharing permissions by image or tenant."""
|
||||
if args.image_id and args.tenant_id:
|
||||
utils.exit('Unable to filter members by both --image-id and'
|
||||
' --tenant-id.')
|
||||
elif args.image_id:
|
||||
kwargs = {'image': args.image_id}
|
||||
elif args.tenant_id:
|
||||
kwargs = {'member': args.tenant_id}
|
||||
else:
|
||||
utils.exit('Unable to list all members. Specify --image-id or'
|
||||
' --tenant-id')
|
||||
|
||||
members = gc.image_members.list(**kwargs)
|
||||
columns = ['Image ID', 'Member ID', 'Can Share']
|
||||
utils.print_list(members, columns)
|
||||
|
||||
|
||||
@utils.arg('image', metavar='<IMAGE>',
|
||||
help='Image to add member to.')
|
||||
@utils.arg('tenant_id', metavar='<TENANT_ID>',
|
||||
help='Tenant to add as member.')
|
||||
@utils.arg('--can-share', action='store_true', default=False,
|
||||
help='Allow the specified tenant to share this image.')
|
||||
def do_member_create(gc, args):
|
||||
"""Share a specific image with a tenant."""
|
||||
image = utils.find_resource(gc.images, args.image)
|
||||
gc.image_members.create(image, args.tenant_id, args.can_share)
|
||||
|
||||
|
||||
@utils.arg('image', metavar='<IMAGE>',
|
||||
help='Image from which to remove member.')
|
||||
@utils.arg('tenant_id', metavar='<TENANT_ID>',
|
||||
help='Tenant to remove as member.')
|
||||
def do_member_delete(gc, args):
|
||||
"""Remove a shared image from a tenant."""
|
||||
image_id = utils.find_resource(gc.images, args.image).id
|
||||
gc.image_members.delete(image_id, args.tenant_id)
|
@ -1,26 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from glanceclient.v1.apiclient import base
|
||||
|
||||
|
||||
class VersionManager(base.ManagerWithFind):
|
||||
|
||||
def list(self):
|
||||
"""List all versions."""
|
||||
url = '/versions'
|
||||
resp, body = self.client.get(url)
|
||||
return body.get('versions', None)
|
@ -1,15 +0,0 @@
|
||||
# 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 glanceclient.v2.client import Client # noqa
|
@ -1,68 +0,0 @@
|
||||
# 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 glanceclient.common import http
|
||||
from glanceclient.common import utils
|
||||
from glanceclient.v2 import image_members
|
||||
from glanceclient.v2 import image_tags
|
||||
from glanceclient.v2 import images
|
||||
from glanceclient.v2 import metadefs
|
||||
from glanceclient.v2 import schemas
|
||||
from glanceclient.v2 import tasks
|
||||
from glanceclient.v2 import versions
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Client for the OpenStack Images v2 API.
|
||||
|
||||
:param string endpoint: A user-supplied endpoint URL for the glance
|
||||
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 glance.
|
||||
"""
|
||||
|
||||
def __init__(self, endpoint=None, **kwargs):
|
||||
endpoint, self.version = utils.endpoint_version_from_url(endpoint, 2.0)
|
||||
self.http_client = http.get_http_client(endpoint=endpoint, **kwargs)
|
||||
self.schemas = schemas.Controller(self.http_client)
|
||||
|
||||
self.images = images.Controller(self.http_client, self.schemas)
|
||||
self.image_tags = image_tags.Controller(self.http_client,
|
||||
self.schemas)
|
||||
self.image_members = image_members.Controller(self.http_client,
|
||||
self.schemas)
|
||||
|
||||
self.tasks = tasks.Controller(self.http_client, self.schemas)
|
||||
|
||||
self.metadefs_resource_type = (
|
||||
metadefs.ResourceTypeController(self.http_client, self.schemas))
|
||||
|
||||
self.metadefs_property = (
|
||||
metadefs.PropertyController(self.http_client, self.schemas))
|
||||
|
||||
self.metadefs_object = (
|
||||
metadefs.ObjectController(self.http_client, self.schemas))
|
||||
|
||||
self.metadefs_tag = (
|
||||
metadefs.TagController(self.http_client, self.schemas))
|
||||
|
||||
self.metadefs_namespace = (
|
||||
metadefs.NamespaceController(self.http_client, self.schemas))
|
||||
|
||||
self.versions = versions.VersionController(self.http_client)
|
@ -1,60 +0,0 @@
|
||||
# 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 warlock
|
||||
|
||||
from glanceclient.common import utils
|
||||
from glanceclient.v2 import schemas
|
||||
|
||||
|
||||
MEMBER_STATUS_VALUES = ('accepted', 'rejected', 'pending')
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self, http_client, schema_client):
|
||||
self.http_client = http_client
|
||||
self.schema_client = schema_client
|
||||
|
||||
@utils.memoized_property
|
||||
def model(self):
|
||||
schema = self.schema_client.get('member')
|
||||
return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel)
|
||||
|
||||
@utils.add_req_id_to_generator()
|
||||
def list(self, image_id):
|
||||
url = '/v2/images/%s/members' % image_id
|
||||
resp, body = self.http_client.get(url)
|
||||
for member in body['members']:
|
||||
yield self.model(member), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def delete(self, image_id, member_id):
|
||||
resp, body = self.http_client.delete('/v2/images/%s/members/%s' %
|
||||
(image_id, member_id))
|
||||
return (resp, body), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def update(self, image_id, member_id, member_status):
|
||||
url = '/v2/images/%s/members/%s' % (image_id, member_id)
|
||||
body = {'status': member_status}
|
||||
resp, updated_member = self.http_client.put(url, data=body)
|
||||
return self.model(updated_member), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def create(self, image_id, member_id):
|
||||
url = '/v2/images/%s/members' % image_id
|
||||
body = {'member': member_id}
|
||||
resp, created_member = self.http_client.post(url, data=body)
|
||||
return self.model(created_member), resp
|
@ -1,209 +0,0 @@
|
||||
# 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.
|
||||
|
||||
_doc_url = "http://docs.openstack.org/user-guide/common/cli-manage-images.html" # noqa
|
||||
# NOTE(flaper87): Keep a copy of the current default schema so that
|
||||
# we can react on cases where there's no connection to an OpenStack
|
||||
# deployment. See #1481729
|
||||
_BASE_SCHEMA = {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": "image",
|
||||
"links": [{
|
||||
"href": "{self}",
|
||||
"rel": "self"
|
||||
}, {
|
||||
"href": "{file}",
|
||||
"rel": "enclosure"
|
||||
}, {
|
||||
"href": "{schema}",
|
||||
"rel": "describedby"
|
||||
}],
|
||||
"properties": {
|
||||
"container_format": {
|
||||
"enum": [None, "ami", "ari", "aki", "bare", "ovf", "ova",
|
||||
"docker"],
|
||||
"type": ["null", "string"],
|
||||
"description": "Format of the container"
|
||||
},
|
||||
"min_ram": {
|
||||
"type": "integer",
|
||||
"description": "Amount of ram (in MB) required to boot image."
|
||||
},
|
||||
"ramdisk_id": {
|
||||
"pattern": ("^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}"
|
||||
"-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}"
|
||||
"-([0-9a-fA-F]){12}$"),
|
||||
"type": ["null", "string"],
|
||||
"description": ("ID of image stored in Glance that should be "
|
||||
"used as the ramdisk when booting an AMI-style "
|
||||
"image."),
|
||||
"is_base": False
|
||||
},
|
||||
"locations": {
|
||||
"items": {
|
||||
"required": ["url", "metadata"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "array",
|
||||
"description": ("A set of URLs to access the image "
|
||||
"file kept in external store")
|
||||
},
|
||||
"file": {
|
||||
"readOnly": True,
|
||||
"type": "string",
|
||||
"description": "An image file url"
|
||||
},
|
||||
"owner": {
|
||||
"type": ["null", "string"],
|
||||
"description": "Owner of the image",
|
||||
"maxLength": 255
|
||||
},
|
||||
"id": {
|
||||
"pattern": ("^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}"
|
||||
"-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}"
|
||||
"-([0-9a-fA-F]){12}$"),
|
||||
"type": "string",
|
||||
"description": "An identifier for the image"
|
||||
},
|
||||
"size": {
|
||||
"readOnly": True,
|
||||
"type": ["null", "integer"],
|
||||
"description": "Size of image file in bytes"
|
||||
},
|
||||
"os_distro": {
|
||||
"type": "string",
|
||||
"description": ("Common name of operating system distribution "
|
||||
"as specified in %s" % _doc_url),
|
||||
"is_base": False
|
||||
},
|
||||
"self": {
|
||||
"readOnly": True,
|
||||
"type": "string",
|
||||
"description": "An image self url"
|
||||
},
|
||||
"disk_format": {
|
||||
"enum": [None, "ami", "ari", "aki", "vhd", "vhdx", "vmdk", "raw",
|
||||
"qcow2", "vdi", "iso", "ploop"],
|
||||
"type": ["null", "string"],
|
||||
"description": "Format of the disk"
|
||||
},
|
||||
"os_version": {
|
||||
"type": "string",
|
||||
"description": "Operating system version as specified by the"
|
||||
" distributor",
|
||||
"is_base": False
|
||||
},
|
||||
"direct_url": {
|
||||
"readOnly": True,
|
||||
"type": "string",
|
||||
"description": "URL to access the image file kept in external"
|
||||
" store"
|
||||
},
|
||||
"schema": {
|
||||
"readOnly": True,
|
||||
"type": "string",
|
||||
"description": "An image schema url"
|
||||
},
|
||||
"status": {
|
||||
"readOnly": True,
|
||||
"enum": ["queued", "saving", "active", "killed", "deleted",
|
||||
"pending_delete", "deactivated"],
|
||||
"type": "string",
|
||||
"description": "Status of the image"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"type": "string",
|
||||
"maxLength": 255
|
||||
},
|
||||
"type": "array",
|
||||
"description": "List of strings related to the image"
|
||||
},
|
||||
"kernel_id": {
|
||||
"pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])"
|
||||
"{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$",
|
||||
"type": ["null", "string"],
|
||||
"description": "ID of image stored in Glance that should be "
|
||||
"used as the kernel when booting an "
|
||||
"AMI-style image.",
|
||||
"is_base": False
|
||||
},
|
||||
"visibility": {
|
||||
"enum": ["public", "private", "community", "shared"],
|
||||
"type": "string",
|
||||
"description": "Scope of image accessibility"
|
||||
},
|
||||
"updated_at": {
|
||||
"readOnly": True,
|
||||
"type": "string",
|
||||
"description": "Date and time of the last image modification"
|
||||
},
|
||||
"min_disk": {
|
||||
"type": "integer",
|
||||
"description": "Amount of disk space (in GB) required to boot "
|
||||
"image."
|
||||
},
|
||||
"virtual_size": {
|
||||
"readOnly": True,
|
||||
"type": ["null", "integer"],
|
||||
"description": "Virtual size of image in bytes"
|
||||
},
|
||||
"instance_uuid": {
|
||||
"type": "string",
|
||||
"description": "Metadata which can be used to record which "
|
||||
"instance this image is associated with. "
|
||||
"(Informational only, does not create an "
|
||||
"instance snapshot.)",
|
||||
"is_base": False
|
||||
},
|
||||
"name": {
|
||||
"type": ["null", "string"],
|
||||
"description": "Descriptive name for the image",
|
||||
"maxLength": 255
|
||||
},
|
||||
"checksum": {
|
||||
"readOnly": True,
|
||||
"type": ["null", "string"],
|
||||
"description": "md5 hash of image contents.",
|
||||
"maxLength": 32
|
||||
},
|
||||
"created_at": {
|
||||
"readOnly": True,
|
||||
"type": "string",
|
||||
"description": "Date and time of image registration"
|
||||
},
|
||||
"protected": {
|
||||
"type": "boolean",
|
||||
"description": "If true, image will not be deletable."
|
||||
},
|
||||
"architecture": {
|
||||
"type": "string",
|
||||
"description": ("Operating system architecture as specified "
|
||||
"in %s" % _doc_url),
|
||||
"is_base": False
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
# 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 warlock
|
||||
|
||||
from glanceclient.common import utils
|
||||
from glanceclient.v2 import schemas
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self, http_client, schema_client):
|
||||
self.http_client = http_client
|
||||
self.schema_client = schema_client
|
||||
|
||||
@utils.memoized_property
|
||||
def model(self):
|
||||
schema = self.schema_client.get('image')
|
||||
return warlock.model_factory(schema.raw(),
|
||||
base_class=schemas.SchemaBasedModel)
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def update(self, image_id, tag_value):
|
||||
"""Update an image with the given tag.
|
||||
|
||||
:param image_id: image to be updated with the given tag.
|
||||
:param tag_value: value of the tag.
|
||||
"""
|
||||
url = '/v2/images/%s/tags/%s' % (image_id, tag_value)
|
||||
resp, body = self.http_client.put(url)
|
||||
return (resp, body), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def delete(self, image_id, tag_value):
|
||||
"""Delete the tag associated with the given image.
|
||||
|
||||
:param image_id: Image whose tag to be deleted.
|
||||
:param tag_value: tag value to be deleted.
|
||||
"""
|
||||
url = '/v2/images/%s/tags/%s' % (image_id, tag_value)
|
||||
resp, body = self.http_client.delete(url)
|
||||
return (resp, body), resp
|
@ -1,399 +0,0 @@
|
||||
# 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 json
|
||||
from oslo_utils import encodeutils
|
||||
from requests import codes
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
import warlock
|
||||
|
||||
from glanceclient.common import utils
|
||||
from glanceclient import exc
|
||||
from glanceclient.v2 import schemas
|
||||
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
SORT_DIR_VALUES = ('asc', 'desc')
|
||||
SORT_KEY_VALUES = ('name', 'status', 'container_format', 'disk_format',
|
||||
'size', 'id', 'created_at', 'updated_at')
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self, http_client, schema_client):
|
||||
self.http_client = http_client
|
||||
self.schema_client = schema_client
|
||||
|
||||
@utils.memoized_property
|
||||
def model(self):
|
||||
schema = self.schema_client.get('image')
|
||||
warlock_model = warlock.model_factory(
|
||||
schema.raw(), base_class=schemas.SchemaBasedModel)
|
||||
return warlock_model
|
||||
|
||||
@utils.memoized_property
|
||||
def unvalidated_model(self):
|
||||
"""A model which does not validate the image against the v2 schema."""
|
||||
schema = self.schema_client.get('image')
|
||||
warlock_model = warlock.model_factory(
|
||||
schema.raw(), base_class=schemas.SchemaBasedModel)
|
||||
warlock_model.validate = lambda *args, **kwargs: None
|
||||
return warlock_model
|
||||
|
||||
@staticmethod
|
||||
def _wrap(value):
|
||||
if isinstance(value, six.string_types):
|
||||
return [value]
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _validate_sort_param(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 SORT_DIR_VALUES:
|
||||
msg = ('Invalid sort direction: %(sort_dir)s.'
|
||||
' It must be one of the following: %(available)s.'
|
||||
) % {'sort_dir': dir,
|
||||
'available': ', '.join(SORT_DIR_VALUES)}
|
||||
raise exc.HTTPBadRequest(msg)
|
||||
if key not in SORT_KEY_VALUES:
|
||||
msg = ('Invalid sort key: %(sort_key)s.'
|
||||
' It must be one of the following: %(available)s.'
|
||||
) % {'sort_key': key,
|
||||
'available': ', '.join(SORT_KEY_VALUES)}
|
||||
raise exc.HTTPBadRequest(msg)
|
||||
return sort
|
||||
|
||||
@utils.add_req_id_to_generator()
|
||||
def list(self, **kwargs):
|
||||
"""Retrieve a listing of Image objects.
|
||||
|
||||
:param page_size: Number of images to request in each
|
||||
paginated request.
|
||||
:returns: generator over list of Images.
|
||||
"""
|
||||
|
||||
limit = kwargs.get('limit')
|
||||
# NOTE(flaper87): Don't use `get('page_size', DEFAULT_SIZE)` otherwise,
|
||||
# it could be possible to send invalid data to the server by passing
|
||||
# page_size=None.
|
||||
page_size = kwargs.get('page_size') or DEFAULT_PAGE_SIZE
|
||||
|
||||
def paginate(url, page_size, limit=None):
|
||||
next_url = url
|
||||
req_id_hdr = {}
|
||||
|
||||
while True:
|
||||
if limit and page_size > limit:
|
||||
# NOTE(flaper87): Avoid requesting 2000 images when limit
|
||||
# is 1
|
||||
next_url = next_url.replace("limit=%s" % page_size,
|
||||
"limit=%s" % limit)
|
||||
|
||||
resp, body = self.http_client.get(next_url, headers=req_id_hdr)
|
||||
# NOTE(rsjethani): Store curent request id so that it can be
|
||||
# used in subsequent requests. Refer bug #1525259
|
||||
req_id_hdr['x-openstack-request-id'] = \
|
||||
utils._extract_request_id(resp)
|
||||
|
||||
for image in body['images']:
|
||||
# NOTE(bcwaldon): remove 'self' for now until we have
|
||||
# an elegant way to pass it into the model constructor
|
||||
# without conflict.
|
||||
image.pop('self', None)
|
||||
# We do not validate the model when listing.
|
||||
# This prevents side-effects of injecting invalid
|
||||
# schema values via v1.
|
||||
yield self.unvalidated_model(**image), resp
|
||||
if limit:
|
||||
limit -= 1
|
||||
if limit <= 0:
|
||||
raise StopIteration
|
||||
|
||||
try:
|
||||
next_url = body['next']
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
filters = kwargs.get('filters', {})
|
||||
# NOTE(flaper87): We paginate in the client, hence we use
|
||||
# the page_size as Glance's limit.
|
||||
filters['limit'] = page_size
|
||||
|
||||
tags = filters.pop('tag', [])
|
||||
tags_url_params = []
|
||||
|
||||
for tag in tags:
|
||||
if not isinstance(tag, six.string_types):
|
||||
raise exc.HTTPBadRequest("Invalid tag value %s" % tag)
|
||||
|
||||
tags_url_params.append({'tag': encodeutils.safe_encode(tag)})
|
||||
|
||||
for param, value in filters.items():
|
||||
if isinstance(value, six.string_types):
|
||||
filters[param] = encodeutils.safe_encode(value)
|
||||
|
||||
url = '/v2/images?%s' % parse.urlencode(filters)
|
||||
|
||||
for param in tags_url_params:
|
||||
url = '%s&%s' % (url, parse.urlencode(param))
|
||||
|
||||
if 'sort' in kwargs:
|
||||
if 'sort_key' in kwargs or 'sort_dir' in kwargs:
|
||||
raise exc.HTTPBadRequest("The 'sort' argument is not supported"
|
||||
" with 'sort_key' or 'sort_dir'.")
|
||||
url = '%s&sort=%s' % (url,
|
||||
self._validate_sort_param(
|
||||
kwargs['sort']))
|
||||
else:
|
||||
sort_dir = self._wrap(kwargs.get('sort_dir', []))
|
||||
sort_key = self._wrap(kwargs.get('sort_key', []))
|
||||
|
||||
if len(sort_key) != len(sort_dir) and len(sort_dir) > 1:
|
||||
raise exc.HTTPBadRequest(
|
||||
"Unexpected number of sort directions: "
|
||||
"either provide a single sort direction or an equal "
|
||||
"number of sort keys and sort directions.")
|
||||
for key in sort_key:
|
||||
url = '%s&sort_key=%s' % (url, key)
|
||||
|
||||
for dir in sort_dir:
|
||||
url = '%s&sort_dir=%s' % (url, dir)
|
||||
|
||||
if isinstance(kwargs.get('marker'), six.string_types):
|
||||
url = '%s&marker=%s' % (url, kwargs['marker'])
|
||||
|
||||
for image, resp in paginate(url, page_size, limit):
|
||||
yield image, resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def _get(self, image_id, header=None):
|
||||
url = '/v2/images/%s' % image_id
|
||||
header = header or {}
|
||||
resp, body = self.http_client.get(url, headers=header)
|
||||
# NOTE(bcwaldon): remove 'self' for now until we have an elegant
|
||||
# way to pass it into the model constructor without conflict
|
||||
body.pop('self', None)
|
||||
return self.unvalidated_model(**body), resp
|
||||
|
||||
def get(self, image_id):
|
||||
return self._get(image_id)
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def data(self, image_id, do_checksum=True):
|
||||
"""Retrieve data of an image.
|
||||
|
||||
:param image_id: ID of the image to download.
|
||||
:param do_checksum: Enable/disable checksum validation.
|
||||
:returns: An iterable body or None
|
||||
"""
|
||||
url = '/v2/images/%s/file' % image_id
|
||||
resp, body = self.http_client.get(url)
|
||||
if resp.status_code == codes.no_content:
|
||||
return None, resp
|
||||
|
||||
checksum = resp.headers.get('content-md5', None)
|
||||
content_length = int(resp.headers.get('content-length', 0))
|
||||
|
||||
if do_checksum and checksum is not None:
|
||||
body = utils.integrity_iter(body, checksum)
|
||||
|
||||
return utils.IterableWithLength(body, content_length), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def upload(self, image_id, image_data, image_size=None):
|
||||
"""Upload the data for an image.
|
||||
|
||||
:param image_id: ID of the image to upload data for.
|
||||
:param image_data: File-like object supplying the data to upload.
|
||||
:param image_size: Unused - present for backwards compatibility
|
||||
"""
|
||||
url = '/v2/images/%s/file' % image_id
|
||||
hdrs = {'Content-Type': 'application/octet-stream'}
|
||||
body = image_data
|
||||
resp, body = self.http_client.put(url, headers=hdrs, data=body)
|
||||
return (resp, body), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def delete(self, image_id):
|
||||
"""Delete an image."""
|
||||
url = '/v2/images/%s' % image_id
|
||||
resp, body = self.http_client.delete(url)
|
||||
return (resp, body), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def create(self, **kwargs):
|
||||
"""Create an image."""
|
||||
url = '/v2/images'
|
||||
|
||||
image = self.model()
|
||||
for (key, value) in kwargs.items():
|
||||
try:
|
||||
setattr(image, key, value)
|
||||
except warlock.InvalidOperation as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
|
||||
resp, body = self.http_client.post(url, data=image)
|
||||
# NOTE(esheffield): remove 'self' for now until we have an elegant
|
||||
# way to pass it into the model constructor without conflict
|
||||
body.pop('self', None)
|
||||
return self.model(**body), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def deactivate(self, image_id):
|
||||
"""Deactivate an image."""
|
||||
url = '/v2/images/%s/actions/deactivate' % image_id
|
||||
resp, body = self.http_client.post(url)
|
||||
return (resp, body), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def reactivate(self, image_id):
|
||||
"""Reactivate an image."""
|
||||
url = '/v2/images/%s/actions/reactivate' % image_id
|
||||
resp, body = self.http_client.post(url)
|
||||
return (resp, body), resp
|
||||
|
||||
def update(self, image_id, remove_props=None, **kwargs):
|
||||
"""Update attributes of an image.
|
||||
|
||||
:param image_id: ID of the image to modify.
|
||||
:param remove_props: List of property names to remove
|
||||
:param kwargs: Image attribute names and their new values.
|
||||
"""
|
||||
unvalidated_image = self.get(image_id)
|
||||
image = self.model(**unvalidated_image)
|
||||
for (key, value) in kwargs.items():
|
||||
try:
|
||||
setattr(image, key, value)
|
||||
except warlock.InvalidOperation as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
|
||||
if remove_props:
|
||||
cur_props = image.keys()
|
||||
new_props = kwargs.keys()
|
||||
# NOTE(esheffield): Only remove props that currently exist on the
|
||||
# image and are NOT in the properties being updated / added
|
||||
props_to_remove = set(cur_props).intersection(
|
||||
set(remove_props).difference(new_props))
|
||||
|
||||
for key in props_to_remove:
|
||||
delattr(image, key)
|
||||
|
||||
url = '/v2/images/%s' % image_id
|
||||
hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'}
|
||||
resp, _ = self.http_client.patch(url, headers=hdrs, data=image.patch)
|
||||
# Get request id from `patch` request so it can be passed to the
|
||||
# following `get` call
|
||||
req_id_hdr = {
|
||||
'x-openstack-request-id': utils._extract_request_id(resp)}
|
||||
|
||||
# NOTE(bcwaldon): calling image.patch doesn't clear the changes, so
|
||||
# we need to fetch the image again to get a clean history. This is
|
||||
# an obvious optimization for warlock
|
||||
return self._get(image_id, req_id_hdr)
|
||||
|
||||
def _get_image_with_locations_or_fail(self, image_id):
|
||||
image = self.get(image_id)
|
||||
if getattr(image, 'locations', None) is None:
|
||||
raise exc.HTTPBadRequest('The administrator has disabled '
|
||||
'API access to image locations')
|
||||
return image
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def _send_image_update_request(self, image_id, patch_body):
|
||||
url = '/v2/images/%s' % image_id
|
||||
hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'}
|
||||
resp, body = self.http_client.patch(url, headers=hdrs,
|
||||
data=json.dumps(patch_body))
|
||||
return (resp, body), resp
|
||||
|
||||
def add_location(self, image_id, url, metadata):
|
||||
"""Add a new location entry to an image's list of locations.
|
||||
|
||||
It is an error to add a URL that is already present in the list of
|
||||
locations.
|
||||
|
||||
:param image_id: ID of image to which the location is to be added.
|
||||
:param url: URL of the location to add.
|
||||
:param metadata: Metadata associated with the location.
|
||||
:returns: The updated image
|
||||
"""
|
||||
add_patch = [{'op': 'add', 'path': '/locations/-',
|
||||
'value': {'url': url, 'metadata': metadata}}]
|
||||
response = self._send_image_update_request(image_id, add_patch)
|
||||
# Get request id from the above update request and pass the same to
|
||||
# following get request
|
||||
req_id_hdr = {'x-openstack-request-id': response.request_ids[0]}
|
||||
return self._get(image_id, req_id_hdr)
|
||||
|
||||
def delete_locations(self, image_id, url_set):
|
||||
"""Remove one or more location entries of an image.
|
||||
|
||||
:param image_id: ID of image from which locations are to be removed.
|
||||
:param url_set: set of URLs of location entries to remove.
|
||||
:returns: None
|
||||
"""
|
||||
image = self._get_image_with_locations_or_fail(image_id)
|
||||
current_urls = [l['url'] for l in image.locations]
|
||||
|
||||
missing_locs = url_set.difference(set(current_urls))
|
||||
if missing_locs:
|
||||
raise exc.HTTPNotFound('Unknown URL(s): %s' % list(missing_locs))
|
||||
|
||||
# NOTE: warlock doesn't generate the most efficient patch for remove
|
||||
# operations (it shifts everything up and deletes the tail elements) so
|
||||
# we do it ourselves.
|
||||
url_indices = [current_urls.index(url) for url in url_set]
|
||||
url_indices.sort(reverse=True)
|
||||
patches = [{'op': 'remove', 'path': '/locations/%s' % url_idx}
|
||||
for url_idx in url_indices]
|
||||
return self._send_image_update_request(image_id, patches)
|
||||
|
||||
def update_location(self, image_id, url, metadata):
|
||||
"""Update an existing location entry in an image's list of locations.
|
||||
|
||||
The URL specified must be already present in the image's list of
|
||||
locations.
|
||||
|
||||
:param image_id: ID of image whose location is to be updated.
|
||||
:param url: URL of the location to update.
|
||||
:param metadata: Metadata associated with the location.
|
||||
:returns: The updated image
|
||||
"""
|
||||
image = self._get_image_with_locations_or_fail(image_id)
|
||||
url_map = dict([(l['url'], l) for l in image.locations])
|
||||
if url not in url_map:
|
||||
raise exc.HTTPNotFound('Unknown URL: %s, the URL must be one of'
|
||||
' existing locations of current image' %
|
||||
url)
|
||||
|
||||
if url_map[url]['metadata'] == metadata:
|
||||
return image
|
||||
|
||||
url_map[url]['metadata'] = metadata
|
||||
patches = [{'op': 'replace',
|
||||
'path': '/locations',
|
||||
'value': list(url_map.values())}]
|
||||
response = self._send_image_update_request(image_id, patches)
|
||||
# Get request id from the above update request and pass the same to
|
||||
# following get request
|
||||
req_id_hdr = {'x-openstack-request-id': response.request_ids[0]}
|
||||
|
||||
return self._get(image_id, req_id_hdr)
|
@ -1,585 +0,0 @@
|
||||
# Copyright 2014 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 oslo_utils import encodeutils
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
import warlock
|
||||
|
||||
from glanceclient.common import utils
|
||||
from glanceclient.v2 import schemas
|
||||
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
SORT_DIR_VALUES = ('asc', 'desc')
|
||||
SORT_KEY_VALUES = ('created_at', 'namespace')
|
||||
|
||||
|
||||
class NamespaceController(object):
|
||||
def __init__(self, http_client, schema_client):
|
||||
self.http_client = http_client
|
||||
self.schema_client = schema_client
|
||||
|
||||
@utils.memoized_property
|
||||
def model(self):
|
||||
schema = self.schema_client.get('metadefs/namespace')
|
||||
return warlock.model_factory(schema.raw(),
|
||||
base_class=schemas.SchemaBasedModel)
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def create(self, **kwargs):
|
||||
"""Create a namespace.
|
||||
|
||||
:param kwargs: Unpacked namespace object.
|
||||
"""
|
||||
url = '/v2/metadefs/namespaces'
|
||||
try:
|
||||
namespace = self.model(kwargs)
|
||||
except (warlock.InvalidOperation, ValueError) as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
|
||||
resp, body = self.http_client.post(url, data=namespace)
|
||||
body.pop('self', None)
|
||||
return self.model(**body), resp
|
||||
|
||||
def update(self, namespace_name, **kwargs):
|
||||
"""Update a namespace.
|
||||
|
||||
:param namespace_name: Name of a namespace (old one).
|
||||
:param kwargs: Unpacked namespace object.
|
||||
"""
|
||||
namespace = self.get(namespace_name)
|
||||
for (key, value) in kwargs.items():
|
||||
try:
|
||||
setattr(namespace, key, value)
|
||||
except warlock.InvalidOperation as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
|
||||
# Remove read-only parameters.
|
||||
read_only = ['schema', 'updated_at', 'created_at']
|
||||
for elem in read_only:
|
||||
if elem in namespace:
|
||||
del namespace[elem]
|
||||
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s' % {
|
||||
'namespace': namespace_name}
|
||||
# Pass the original wrapped value to http client.
|
||||
resp, _ = self.http_client.put(url, data=namespace.wrapped)
|
||||
# Get request id from `put` request so it can be passed to the
|
||||
# following `get` call
|
||||
req_id_hdr = {
|
||||
'x-openstack-request-id': utils._extract_request_id(resp)
|
||||
}
|
||||
return self._get(namespace.namespace, header=req_id_hdr)
|
||||
|
||||
def get(self, namespace, **kwargs):
|
||||
return self._get(namespace, **kwargs)
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def _get(self, namespace, header=None, **kwargs):
|
||||
"""Get one namespace."""
|
||||
query_params = parse.urlencode(kwargs)
|
||||
if kwargs:
|
||||
query_params = '?%s' % query_params
|
||||
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s%(query_params)s' % {
|
||||
'namespace': namespace, 'query_params': query_params}
|
||||
header = header or {}
|
||||
resp, body = self.http_client.get(url, headers=header)
|
||||
# NOTE(bcwaldon): remove 'self' for now until we have an elegant
|
||||
# way to pass it into the model constructor without conflict
|
||||
body.pop('self', None)
|
||||
return self.model(**body), resp
|
||||
|
||||
@utils.add_req_id_to_generator()
|
||||
def list(self, **kwargs):
|
||||
"""Retrieve a listing of Namespace objects.
|
||||
|
||||
:param page_size: Number of items to request in each paginated request
|
||||
:param limit: Use to request a specific page size. Expect a response
|
||||
to a limited request to return between zero and limit
|
||||
items.
|
||||
:param marker: Specifies the namespace of the last-seen namespace.
|
||||
The typical pattern of limit and marker is to make an
|
||||
initial limited request and then to use the last
|
||||
namespace from the response as the marker parameter
|
||||
in a subsequent limited request.
|
||||
:param sort_key: The field to sort on (for example, 'created_at')
|
||||
:param sort_dir: The direction to sort ('asc' or 'desc')
|
||||
:returns: generator over list of Namespaces
|
||||
|
||||
"""
|
||||
|
||||
ori_validate_fun = self.model.validate
|
||||
empty_fun = lambda *args, **kwargs: None
|
||||
|
||||
def paginate(url):
|
||||
resp, body = self.http_client.get(url)
|
||||
for namespace in body['namespaces']:
|
||||
# NOTE(bcwaldon): remove 'self' for now until we have
|
||||
# an elegant way to pass it into the model constructor
|
||||
# without conflict.
|
||||
namespace.pop('self', None)
|
||||
yield self.model(**namespace), resp
|
||||
# NOTE(zhiyan): In order to resolve the performance issue
|
||||
# of JSON schema validation for image listing case, we
|
||||
# don't validate each image entry but do it only on first
|
||||
# image entry for each page.
|
||||
self.model.validate = empty_fun
|
||||
|
||||
# NOTE(zhiyan); Reset validation function.
|
||||
self.model.validate = ori_validate_fun
|
||||
|
||||
try:
|
||||
next_url = body['next']
|
||||
except KeyError:
|
||||
return
|
||||
else:
|
||||
for namespace, resp in paginate(next_url):
|
||||
yield namespace, resp
|
||||
|
||||
filters = kwargs.get('filters', {})
|
||||
filters = {} if filters is None else filters
|
||||
|
||||
if not kwargs.get('page_size'):
|
||||
filters['limit'] = DEFAULT_PAGE_SIZE
|
||||
else:
|
||||
filters['limit'] = kwargs['page_size']
|
||||
|
||||
if 'marker' in kwargs:
|
||||
filters['marker'] = kwargs['marker']
|
||||
|
||||
sort_key = kwargs.get('sort_key')
|
||||
if sort_key is not None:
|
||||
if sort_key in SORT_KEY_VALUES:
|
||||
filters['sort_key'] = sort_key
|
||||
else:
|
||||
raise ValueError('sort_key must be one of the following: %s.'
|
||||
% ', '.join(SORT_KEY_VALUES))
|
||||
|
||||
sort_dir = kwargs.get('sort_dir')
|
||||
if sort_dir is not None:
|
||||
if sort_dir in SORT_DIR_VALUES:
|
||||
filters['sort_dir'] = sort_dir
|
||||
else:
|
||||
raise ValueError('sort_dir must be one of the following: %s.'
|
||||
% ', '.join(SORT_DIR_VALUES))
|
||||
|
||||
for param, value in filters.items():
|
||||
if isinstance(value, list):
|
||||
filters[param] = encodeutils.safe_encode(','.join(value))
|
||||
elif isinstance(value, six.string_types):
|
||||
filters[param] = encodeutils.safe_encode(value)
|
||||
|
||||
url = '/v2/metadefs/namespaces?%s' % parse.urlencode(filters)
|
||||
|
||||
for namespace, resp in paginate(url):
|
||||
yield namespace, resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def delete(self, namespace):
|
||||
"""Delete a namespace."""
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s' % {
|
||||
'namespace': namespace}
|
||||
resp, body = self.http_client.delete(url)
|
||||
return (resp, body), resp
|
||||
|
||||
|
||||
class ResourceTypeController(object):
|
||||
def __init__(self, http_client, schema_client):
|
||||
self.http_client = http_client
|
||||
self.schema_client = schema_client
|
||||
|
||||
@utils.memoized_property
|
||||
def model(self):
|
||||
schema = self.schema_client.get('metadefs/resource_type')
|
||||
return warlock.model_factory(schema.raw(),
|
||||
base_class=schemas.SchemaBasedModel)
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def associate(self, namespace, **kwargs):
|
||||
"""Associate a resource type with a namespace."""
|
||||
try:
|
||||
res_type = self.model(kwargs)
|
||||
except (warlock.InvalidOperation, ValueError) as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/resource_types' % {
|
||||
'namespace': namespace}
|
||||
resp, body = self.http_client.post(url, data=res_type)
|
||||
body.pop('self', None)
|
||||
return self.model(**body), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def deassociate(self, namespace, resource):
|
||||
"""Deassociate a resource type with a namespace."""
|
||||
url = ('/v2/metadefs/namespaces/%(namespace)s/'
|
||||
'resource_types/%(resource)s') % {
|
||||
'namespace': namespace, 'resource': resource}
|
||||
resp, body = self.http_client.delete(url)
|
||||
return (resp, body), resp
|
||||
|
||||
@utils.add_req_id_to_generator()
|
||||
def list(self):
|
||||
"""Retrieve a listing of available resource types.
|
||||
|
||||
:returns: generator over list of resource_types
|
||||
"""
|
||||
|
||||
url = '/v2/metadefs/resource_types'
|
||||
resp, body = self.http_client.get(url)
|
||||
for resource_type in body['resource_types']:
|
||||
yield self.model(**resource_type), resp
|
||||
|
||||
@utils.add_req_id_to_generator()
|
||||
def get(self, namespace):
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/resource_types' % {
|
||||
'namespace': namespace}
|
||||
resp, body = self.http_client.get(url)
|
||||
body.pop('self', None)
|
||||
for resource_type in body['resource_type_associations']:
|
||||
yield self.model(**resource_type), resp
|
||||
|
||||
|
||||
class PropertyController(object):
|
||||
def __init__(self, http_client, schema_client):
|
||||
self.http_client = http_client
|
||||
self.schema_client = schema_client
|
||||
|
||||
@utils.memoized_property
|
||||
def model(self):
|
||||
schema = self.schema_client.get('metadefs/property')
|
||||
return warlock.model_factory(schema.raw(),
|
||||
base_class=schemas.SchemaBasedModel)
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def create(self, namespace, **kwargs):
|
||||
"""Create a property.
|
||||
|
||||
:param namespace: Name of a namespace the property will belong.
|
||||
:param kwargs: Unpacked property object.
|
||||
"""
|
||||
try:
|
||||
prop = self.model(kwargs)
|
||||
except (warlock.InvalidOperation, ValueError) as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/properties' % {
|
||||
'namespace': namespace}
|
||||
resp, body = self.http_client.post(url, data=prop)
|
||||
body.pop('self', None)
|
||||
return self.model(**body), resp
|
||||
|
||||
def update(self, namespace, prop_name, **kwargs):
|
||||
"""Update a property.
|
||||
|
||||
:param namespace: Name of a namespace the property belongs.
|
||||
:param prop_name: Name of a property (old one).
|
||||
:param kwargs: Unpacked property object.
|
||||
"""
|
||||
prop = self.get(namespace, prop_name)
|
||||
for (key, value) in kwargs.items():
|
||||
try:
|
||||
setattr(prop, key, value)
|
||||
except warlock.InvalidOperation as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
|
||||
url = ('/v2/metadefs/namespaces/%(namespace)s/'
|
||||
'properties/%(prop_name)s') % {
|
||||
'namespace': namespace, 'prop_name': prop_name}
|
||||
# Pass the original wrapped value to http client.
|
||||
resp, _ = self.http_client.put(url, data=prop.wrapped)
|
||||
# Get request id from `put` request so it can be passed to the
|
||||
# following `get` call
|
||||
req_id_hdr = {
|
||||
'x-openstack-request-id': utils._extract_request_id(resp)}
|
||||
|
||||
return self._get(namespace, prop.name, req_id_hdr)
|
||||
|
||||
def get(self, namespace, prop_name):
|
||||
return self._get(namespace, prop_name)
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def _get(self, namespace, prop_name, header=None):
|
||||
url = ('/v2/metadefs/namespaces/%(namespace)s/'
|
||||
'properties/%(prop_name)s') % {
|
||||
'namespace': namespace, 'prop_name': prop_name}
|
||||
header = header or {}
|
||||
resp, body = self.http_client.get(url, headers=header)
|
||||
body.pop('self', None)
|
||||
body['name'] = prop_name
|
||||
return self.model(**body), resp
|
||||
|
||||
@utils.add_req_id_to_generator()
|
||||
def list(self, namespace, **kwargs):
|
||||
"""Retrieve a listing of metadata properties.
|
||||
|
||||
:returns: generator over list of objects
|
||||
"""
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/properties' % {
|
||||
'namespace': namespace}
|
||||
|
||||
resp, body = self.http_client.get(url)
|
||||
|
||||
for key, value in body['properties'].items():
|
||||
value['name'] = key
|
||||
yield self.model(value), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def delete(self, namespace, prop_name):
|
||||
"""Delete a property."""
|
||||
url = ('/v2/metadefs/namespaces/%(namespace)s/'
|
||||
'properties/%(prop_name)s') % {
|
||||
'namespace': namespace, 'prop_name': prop_name}
|
||||
resp, body = self.http_client.delete(url)
|
||||
return (resp, body), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def delete_all(self, namespace):
|
||||
"""Delete all properties in a namespace."""
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/properties' % {
|
||||
'namespace': namespace}
|
||||
resp, body = self.http_client.delete(url)
|
||||
return (resp, body), resp
|
||||
|
||||
|
||||
class ObjectController(object):
|
||||
def __init__(self, http_client, schema_client):
|
||||
self.http_client = http_client
|
||||
self.schema_client = schema_client
|
||||
|
||||
@utils.memoized_property
|
||||
def model(self):
|
||||
schema = self.schema_client.get('metadefs/object')
|
||||
return warlock.model_factory(schema.raw(),
|
||||
base_class=schemas.SchemaBasedModel)
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def create(self, namespace, **kwargs):
|
||||
"""Create an object.
|
||||
|
||||
:param namespace: Name of a namespace the object belongs.
|
||||
:param kwargs: Unpacked object.
|
||||
"""
|
||||
try:
|
||||
obj = self.model(kwargs)
|
||||
except (warlock.InvalidOperation, ValueError) as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/objects' % {
|
||||
'namespace': namespace}
|
||||
|
||||
resp, body = self.http_client.post(url, data=obj)
|
||||
body.pop('self', None)
|
||||
return self.model(**body), resp
|
||||
|
||||
def update(self, namespace, object_name, **kwargs):
|
||||
"""Update an object.
|
||||
|
||||
:param namespace: Name of a namespace the object belongs.
|
||||
:param object_name: Name of an object (old one).
|
||||
:param kwargs: Unpacked object.
|
||||
"""
|
||||
obj = self.get(namespace, object_name)
|
||||
for (key, value) in kwargs.items():
|
||||
try:
|
||||
setattr(obj, key, value)
|
||||
except warlock.InvalidOperation as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
|
||||
# Remove read-only parameters.
|
||||
read_only = ['schema', 'updated_at', 'created_at']
|
||||
for elem in read_only:
|
||||
if elem in obj:
|
||||
del obj[elem]
|
||||
|
||||
url = ('/v2/metadefs/namespaces/%(namespace)s/'
|
||||
'objects/%(object_name)s') % {
|
||||
'namespace': namespace, 'object_name': object_name}
|
||||
# Pass the original wrapped value to http client.
|
||||
resp, _ = self.http_client.put(url, data=obj.wrapped)
|
||||
# Get request id from `put` request so it can be passed to the
|
||||
# following `get` call
|
||||
req_id_hdr = {
|
||||
'x-openstack-request-id': utils._extract_request_id(resp)}
|
||||
|
||||
return self._get(namespace, obj.name, req_id_hdr)
|
||||
|
||||
def get(self, namespace, object_name):
|
||||
return self._get(namespace, object_name)
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def _get(self, namespace, object_name, header=None):
|
||||
url = ('/v2/metadefs/namespaces/%(namespace)s/'
|
||||
'objects/%(object_name)s') % {
|
||||
'namespace': namespace, 'object_name': object_name}
|
||||
header = header or {}
|
||||
resp, body = self.http_client.get(url, headers=header)
|
||||
body.pop('self', None)
|
||||
return self.model(**body), resp
|
||||
|
||||
@utils.add_req_id_to_generator()
|
||||
def list(self, namespace, **kwargs):
|
||||
"""Retrieve a listing of metadata objects.
|
||||
|
||||
:returns: generator over list of objects
|
||||
"""
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/objects' % {
|
||||
'namespace': namespace}
|
||||
resp, body = self.http_client.get(url)
|
||||
|
||||
for obj in body['objects']:
|
||||
yield self.model(obj), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def delete(self, namespace, object_name):
|
||||
"""Delete an object."""
|
||||
url = ('/v2/metadefs/namespaces/%(namespace)s/'
|
||||
'objects/%(object_name)s') % {
|
||||
'namespace': namespace, 'object_name': object_name}
|
||||
resp, body = self.http_client.delete(url)
|
||||
return (resp, body), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def delete_all(self, namespace):
|
||||
"""Delete all objects in a namespace."""
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/objects' % {
|
||||
'namespace': namespace}
|
||||
resp, body = self.http_client.delete(url)
|
||||
return (resp, body), resp
|
||||
|
||||
|
||||
class TagController(object):
|
||||
def __init__(self, http_client, schema_client):
|
||||
self.http_client = http_client
|
||||
self.schema_client = schema_client
|
||||
|
||||
@utils.memoized_property
|
||||
def model(self):
|
||||
schema = self.schema_client.get('metadefs/tag')
|
||||
return warlock.model_factory(schema.raw(),
|
||||
base_class=schemas.SchemaBasedModel)
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def create(self, namespace, tag_name):
|
||||
"""Create a tag.
|
||||
|
||||
:param namespace: Name of a namespace the Tag belongs.
|
||||
:param tag_name: The name of the new tag to create.
|
||||
"""
|
||||
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % {
|
||||
'namespace': namespace, 'tag_name': tag_name}
|
||||
|
||||
resp, body = self.http_client.post(url)
|
||||
body.pop('self', None)
|
||||
return self.model(**body), resp
|
||||
|
||||
@utils.add_req_id_to_generator()
|
||||
def create_multiple(self, namespace, **kwargs):
|
||||
"""Create the list of tags.
|
||||
|
||||
:param namespace: Name of a namespace to which the Tags belong.
|
||||
:param kwargs: list of tags.
|
||||
"""
|
||||
|
||||
tag_names = kwargs.pop('tags', [])
|
||||
md_tag_list = []
|
||||
|
||||
for tag_name in tag_names:
|
||||
try:
|
||||
md_tag_list.append(self.model(name=tag_name))
|
||||
except (warlock.InvalidOperation) as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
tags = {'tags': md_tag_list}
|
||||
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/tags' % {
|
||||
'namespace': namespace}
|
||||
|
||||
resp, body = self.http_client.post(url, data=tags)
|
||||
body.pop('self', None)
|
||||
for tag in body['tags']:
|
||||
yield self.model(tag), resp
|
||||
|
||||
def update(self, namespace, tag_name, **kwargs):
|
||||
"""Update a tag.
|
||||
|
||||
:param namespace: Name of a namespace the Tag belongs.
|
||||
:param tag_name: Name of the Tag (old one).
|
||||
:param kwargs: Unpacked tag.
|
||||
"""
|
||||
tag = self.get(namespace, tag_name)
|
||||
for (key, value) in kwargs.items():
|
||||
try:
|
||||
setattr(tag, key, value)
|
||||
except warlock.InvalidOperation as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
|
||||
# Remove read-only parameters.
|
||||
read_only = ['updated_at', 'created_at']
|
||||
for elem in read_only:
|
||||
if elem in tag:
|
||||
del tag[elem]
|
||||
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % {
|
||||
'namespace': namespace, 'tag_name': tag_name}
|
||||
# Pass the original wrapped value to http client.
|
||||
resp, _ = self.http_client.put(url, data=tag.wrapped)
|
||||
# Get request id from `put` request so it can be passed to the
|
||||
# following `get` call
|
||||
req_id_hdr = {
|
||||
'x-openstack-request-id': utils._extract_request_id(resp)}
|
||||
|
||||
return self._get(namespace, tag.name, req_id_hdr)
|
||||
|
||||
def get(self, namespace, tag_name):
|
||||
return self._get(namespace, tag_name)
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def _get(self, namespace, tag_name, header=None):
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % {
|
||||
'namespace': namespace, 'tag_name': tag_name}
|
||||
header = header or {}
|
||||
resp, body = self.http_client.get(url, headers=header)
|
||||
body.pop('self', None)
|
||||
return self.model(**body), resp
|
||||
|
||||
@utils.add_req_id_to_generator()
|
||||
def list(self, namespace, **kwargs):
|
||||
"""Retrieve a listing of metadata tags.
|
||||
|
||||
:returns: generator over list of tags.
|
||||
"""
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/tags' % {
|
||||
'namespace': namespace}
|
||||
resp, body = self.http_client.get(url)
|
||||
|
||||
for tag in body['tags']:
|
||||
yield self.model(tag), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def delete(self, namespace, tag_name):
|
||||
"""Delete a tag."""
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % {
|
||||
'namespace': namespace, 'tag_name': tag_name}
|
||||
resp, body = self.http_client.delete(url)
|
||||
return (resp, body), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def delete_all(self, namespace):
|
||||
"""Delete all tags in a namespace."""
|
||||
url = '/v2/metadefs/namespaces/%(namespace)s/tags' % {
|
||||
'namespace': namespace}
|
||||
resp, body = self.http_client.delete(url)
|
||||
return (resp, body), resp
|
@ -1,243 +0,0 @@
|
||||
# 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.
|
||||
|
||||
|
||||
# NOTE(flaper87): Keep a copy of the current default schema so that
|
||||
# we can react on cases where there's no connection to an OpenStack
|
||||
# deployment. See #1481729
|
||||
BASE_SCHEMA = {
|
||||
"additionalProperties": False,
|
||||
"definitions": {
|
||||
"positiveInteger": {
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"positiveIntegerDefault0": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/positiveInteger"},
|
||||
{"default": 0}
|
||||
]
|
||||
},
|
||||
"stringArray": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": True
|
||||
},
|
||||
"property": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": ["title", "type"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"operators": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"array",
|
||||
"boolean",
|
||||
"integer",
|
||||
"number",
|
||||
"object",
|
||||
"string",
|
||||
None
|
||||
]
|
||||
},
|
||||
"required": {
|
||||
"$ref": "#/definitions/stringArray"
|
||||
},
|
||||
"minimum": {
|
||||
"type": "number"
|
||||
},
|
||||
"maximum": {
|
||||
"type": "number"
|
||||
},
|
||||
"maxLength": {
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
},
|
||||
"minLength": {
|
||||
"$ref": "#/definitions/positiveIntegerDefault0"
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"format": "regex"
|
||||
},
|
||||
"enum": {
|
||||
"type": "array"
|
||||
},
|
||||
"readonly": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"default": {},
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"array",
|
||||
"boolean",
|
||||
"integer",
|
||||
"number",
|
||||
"object",
|
||||
"string",
|
||||
None
|
||||
]
|
||||
},
|
||||
"enum": {
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"maxItems": {
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
},
|
||||
"minItems": {
|
||||
"$ref": "#/definitions/positiveIntegerDefault0"
|
||||
},
|
||||
"uniqueItems": {
|
||||
"type": "boolean",
|
||||
"default": False
|
||||
},
|
||||
"additionalItems": {
|
||||
"type": "boolean"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["namespace"],
|
||||
"name": "namespace",
|
||||
"properties": {
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"description": "The unique namespace text.",
|
||||
"maxLength": 80
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string",
|
||||
"description": "The user friendly name for the namespace. Used by "
|
||||
"UI if available.",
|
||||
"maxLength": 80
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Provides a user friendly description of the "
|
||||
"namespace.",
|
||||
"maxLength": 500
|
||||
},
|
||||
"visibility": {
|
||||
"enum": [
|
||||
"public",
|
||||
"private"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Scope of namespace accessibility."
|
||||
},
|
||||
"protected": {
|
||||
"type": "boolean",
|
||||
"description": "If true, namespace will not be deletable."
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Owner of the namespace.",
|
||||
"maxLength": 255
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": "Date and time of namespace creation.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": "Date and time of the last namespace modification.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"schema": {
|
||||
"readOnly": True,
|
||||
"type": "string"
|
||||
},
|
||||
"self": {
|
||||
"readOnly": True,
|
||||
"type": "string"
|
||||
},
|
||||
"resource_type_associations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"properties_target": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/property"
|
||||
},
|
||||
"objects": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"required": {
|
||||
"$ref": "#/definitions/stringArray"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/property"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
# 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.
|
||||
|
||||
|
||||
# NOTE(flaper87): Keep a copy of the current default schema so that
|
||||
# we can react on cases where there's no connection to an OpenStack
|
||||
# deployment. See #1481729
|
||||
BASE_SCHEMA = {
|
||||
"additionalProperties": False,
|
||||
"required": ["name"],
|
||||
"name": "resource_type_association",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Resource type names should be aligned with Heat "
|
||||
"resource types whenever possible: http://docs."
|
||||
"openstack.org/developer/heat/template_guide/"
|
||||
"openstack.html",
|
||||
"maxLength": 80
|
||||
|
||||
},
|
||||
"prefix": {
|
||||
"type": "string",
|
||||
"description": "Specifies the prefix to use for the given resource"
|
||||
" type. Any properties in the namespace should be"
|
||||
" prefixed with this prefix when being applied to"
|
||||
" the specified resource type. Must include prefix"
|
||||
" separator (e.g. a colon :).",
|
||||
"maxLength": 80
|
||||
},
|
||||
"properties_target": {
|
||||
"type": "string",
|
||||
"description": "Some resource types allow more than one key / "
|
||||
"value pair per instance. For example, Cinder "
|
||||
"allows user and image metadata on volumes. Only "
|
||||
"the image properties metadata is evaluated by Nova"
|
||||
" (scheduling or drivers). This property allows a "
|
||||
"namespace target to remove the ambiguity.",
|
||||
"maxLength": 80
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": "Date and time of resource type association.",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"readOnly": True,
|
||||
"description": "Date and time of the last resource type "
|
||||
"association modification.",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
# 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 jsonpatch
|
||||
import warlock.model as warlock
|
||||
|
||||
|
||||
class SchemaBasedModel(warlock.Model):
|
||||
"""Glance specific subclass of the warlock Model.
|
||||
|
||||
This implementation alters the function of the patch property
|
||||
to take into account the schema's core properties. With this version
|
||||
undefined properties which are core will generated 'replace'
|
||||
operations rather than 'add' since this is what the Glance API
|
||||
expects.
|
||||
"""
|
||||
|
||||
def _make_custom_patch(self, new, original):
|
||||
if not self.get('tags'):
|
||||
tags_patch = []
|
||||
else:
|
||||
tags_patch = [{"path": "/tags",
|
||||
"value": self.get('tags'),
|
||||
"op": "replace"}]
|
||||
|
||||
patch_string = jsonpatch.make_patch(original, new).to_string()
|
||||
patch = json.loads(patch_string)
|
||||
if not patch:
|
||||
return json.dumps(tags_patch)
|
||||
else:
|
||||
return json.dumps(patch + tags_patch)
|
||||
|
||||
@warlock.Model.patch.getter
|
||||
def patch(self):
|
||||
"""Return a jsonpatch object representing the delta."""
|
||||
original = copy.deepcopy(self.__dict__['__original__'])
|
||||
new = dict(self)
|
||||
if self.schema:
|
||||
for (name, prop) in self.schema['properties'].items():
|
||||
if (name not in original and name in new and
|
||||
prop.get('is_base', True)):
|
||||
original[name] = None
|
||||
|
||||
original['tags'] = None
|
||||
new['tags'] = None
|
||||
return self._make_custom_patch(new, original)
|
||||
|
||||
|
||||
class SchemaProperty(object):
|
||||
def __init__(self, name, **kwargs):
|
||||
self.name = name
|
||||
self.description = kwargs.get('description')
|
||||
self.is_base = kwargs.get('is_base', True)
|
||||
|
||||
|
||||
def translate_schema_properties(schema_properties):
|
||||
"""Parse the properties dictionary of a schema document.
|
||||
|
||||
:returns: list of SchemaProperty objects
|
||||
"""
|
||||
properties = []
|
||||
for (name, prop) in schema_properties.items():
|
||||
properties.append(SchemaProperty(name, **prop))
|
||||
return properties
|
||||
|
||||
|
||||
class Schema(object):
|
||||
def __init__(self, raw_schema):
|
||||
self._raw_schema = raw_schema
|
||||
self.name = raw_schema['name']
|
||||
raw_properties = raw_schema['properties']
|
||||
self.properties = translate_schema_properties(raw_properties)
|
||||
|
||||
def is_core_property(self, property_name):
|
||||
"""Check if a property with a given name is known to the schema.
|
||||
|
||||
Determines if it is either a base property or a custom one
|
||||
registered in schema-image.json file
|
||||
|
||||
:param property_name: name of the property
|
||||
:returns: True if the property is known, False otherwise
|
||||
"""
|
||||
return self._check_property(property_name, True)
|
||||
|
||||
def is_base_property(self, property_name):
|
||||
"""Checks if a property with a given name is a base property.
|
||||
|
||||
:param property_name: name of the property
|
||||
:returns: True if the property is base, False otherwise
|
||||
"""
|
||||
return self._check_property(property_name, False)
|
||||
|
||||
def _check_property(self, property_name, allow_non_base):
|
||||
for prop in self.properties:
|
||||
if property_name == prop.name:
|
||||
return prop.is_base or allow_non_base
|
||||
return False
|
||||
|
||||
def raw(self):
|
||||
return copy.deepcopy(self._raw_schema)
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self, http_client):
|
||||
self.http_client = http_client
|
||||
|
||||
def get(self, schema_name):
|
||||
uri = '/v2/schemas/%s' % schema_name
|
||||
_, raw_schema = self.http_client.get(uri)
|
||||
return Schema(raw_schema)
|
File diff suppressed because it is too large
Load Diff
@ -1,124 +0,0 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2013 IBM 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.
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
import six
|
||||
import warlock
|
||||
|
||||
from glanceclient.common import utils
|
||||
from glanceclient.v2 import schemas
|
||||
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
SORT_DIR_VALUES = ('asc', 'desc')
|
||||
SORT_KEY_VALUES = ('id', 'type', 'status')
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self, http_client, schema_client):
|
||||
self.http_client = http_client
|
||||
self.schema_client = schema_client
|
||||
|
||||
@utils.memoized_property
|
||||
def model(self):
|
||||
schema = self.schema_client.get('task')
|
||||
return warlock.model_factory(schema.raw(),
|
||||
base_class=schemas.SchemaBasedModel)
|
||||
|
||||
@utils.add_req_id_to_generator()
|
||||
def list(self, **kwargs):
|
||||
"""Retrieve a listing of Task objects.
|
||||
|
||||
:param page_size: Number of tasks to request in each paginated request
|
||||
:returns: generator over list of Tasks
|
||||
|
||||
"""
|
||||
def paginate(url):
|
||||
resp, body = self.http_client.get(url)
|
||||
for task in body['tasks']:
|
||||
yield task, resp
|
||||
try:
|
||||
next_url = body['next']
|
||||
except KeyError:
|
||||
return
|
||||
else:
|
||||
for task, resp in paginate(next_url):
|
||||
yield task, resp
|
||||
|
||||
filters = kwargs.get('filters', {})
|
||||
|
||||
if not kwargs.get('page_size'):
|
||||
filters['limit'] = DEFAULT_PAGE_SIZE
|
||||
else:
|
||||
filters['limit'] = kwargs['page_size']
|
||||
|
||||
if 'marker' in kwargs:
|
||||
filters['marker'] = kwargs['marker']
|
||||
|
||||
sort_key = kwargs.get('sort_key')
|
||||
if sort_key is not None:
|
||||
if sort_key in SORT_KEY_VALUES:
|
||||
filters['sort_key'] = sort_key
|
||||
else:
|
||||
raise ValueError('sort_key must be one of the following: %s.'
|
||||
% ', '.join(SORT_KEY_VALUES))
|
||||
|
||||
sort_dir = kwargs.get('sort_dir')
|
||||
if sort_dir is not None:
|
||||
if sort_dir in SORT_DIR_VALUES:
|
||||
filters['sort_dir'] = sort_dir
|
||||
else:
|
||||
raise ValueError('sort_dir must be one of the following: %s.'
|
||||
% ', '.join(SORT_DIR_VALUES))
|
||||
|
||||
for param, value in filters.items():
|
||||
if isinstance(value, six.string_types):
|
||||
filters[param] = encodeutils.safe_encode(value)
|
||||
|
||||
url = '/v2/tasks?%s' % six.moves.urllib.parse.urlencode(filters)
|
||||
for task, resp in paginate(url):
|
||||
# NOTE(flwang): remove 'self' for now until we have an elegant
|
||||
# way to pass it into the model constructor without conflict
|
||||
task.pop('self', None)
|
||||
yield self.model(**task), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def get(self, task_id):
|
||||
"""Get a task based on given task id."""
|
||||
url = '/v2/tasks/%s' % task_id
|
||||
resp, body = self.http_client.get(url)
|
||||
# NOTE(flwang): remove 'self' for now until we have an elegant
|
||||
# way to pass it into the model constructor without conflict
|
||||
body.pop('self', None)
|
||||
return self.model(**body), resp
|
||||
|
||||
@utils.add_req_id_to_object()
|
||||
def create(self, **kwargs):
|
||||
"""Create a new task."""
|
||||
url = '/v2/tasks'
|
||||
task = self.model()
|
||||
|
||||
for (key, value) in kwargs.items():
|
||||
try:
|
||||
setattr(task, key, value)
|
||||
except warlock.InvalidOperation as e:
|
||||
raise TypeError(encodeutils.exception_to_unicode(e))
|
||||
|
||||
resp, body = self.http_client.post(url, data=task)
|
||||
# NOTE(flwang): remove 'self' for now until we have an elegant
|
||||
# way to pass it into the model constructor without conflict
|
||||
body.pop('self', None)
|
||||
return self.model(**body), resp
|
@ -1,26 +0,0 @@
|
||||
# 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)
|
@ -1,11 +0,0 @@
|
||||
---
|
||||
prelude: >
|
||||
Switch to using keystoneauth for session and auth plugins.
|
||||
other:
|
||||
- >
|
||||
[`bp use-keystoneauth <https://blueprints.launchpad.net/python-glanceclient/+spec/use-keystoneauth>`_]
|
||||
As of keystoneclient 2.2.0, the session and auth plugins code has
|
||||
been deprecated. These modules have been moved to the keystoneauth
|
||||
library. Consumers of the session and plugin modules are encouraged
|
||||
to move to keystoneauth. Note that there should be no change to
|
||||
end users of glanceclient.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user