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:
Tony Breeds 2017-09-12 15:59:15 -06:00
parent 28c003dc11
commit f923549734
120 changed files with 14 additions and 21654 deletions

View File

@ -1,6 +0,0 @@
[run]
branch = True
source = glanceclient
[report]
ignore_errors = True

27
.gitignore vendored
View File

@ -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/*

View File

@ -1,4 +0,0 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/python-glanceclient.git

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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
View 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.

View File

@ -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

View File

@ -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/.

View File

@ -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

View File

@ -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``

View File

@ -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),
]

View File

@ -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

View File

@ -1,8 +0,0 @@
======================
Python API Reference
======================
.. toctree::
:maxdepth: 2
autoindex

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -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")

View File

@ -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

View File

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

View File

@ -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

View File

@ -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"
}
]
}

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -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})

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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

View File

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

View File

@ -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

View File

@ -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"
},
}
}

View File

@ -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"
}
}
}

View File

@ -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

View File

@ -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

View File

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

View File

@ -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