summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.stestr.conf3
-rw-r--r--.testr.conf4
-rw-r--r--.zuul.yaml112
-rw-r--r--README.rst10
-rw-r--r--doc/requirements.txt7
-rw-r--r--doc/source/cli/property-keys.rst356
-rw-r--r--doc/source/conf.py10
-rw-r--r--doc/source/index.rst2
-rw-r--r--doc/source/reference/api/index.rst8
-rw-r--r--doc/source/reference/index.rst2
-rw-r--r--glanceclient/common/http.py23
-rw-r--r--glanceclient/common/utils.py35
-rw-r--r--glanceclient/tests/functional/base.py3
-rw-r--r--glanceclient/tests/functional/v1/__init__.py0
-rw-r--r--glanceclient/tests/functional/v1/test_readonly_glance.py73
-rw-r--r--glanceclient/tests/functional/v2/__init__.py0
-rw-r--r--glanceclient/tests/functional/v2/test_http_headers.py (renamed from glanceclient/tests/functional/test_http_headers.py)0
-rw-r--r--glanceclient/tests/functional/v2/test_readonly_glance.py (renamed from glanceclient/tests/functional/test_readonly_glance.py)15
-rw-r--r--glanceclient/tests/unit/test_http.py14
-rw-r--r--glanceclient/tests/unit/test_shell.py21
-rw-r--r--glanceclient/tests/unit/test_utils.py74
-rw-r--r--glanceclient/tests/unit/v2/base.py4
-rw-r--r--glanceclient/tests/unit/v2/fixtures.py5
-rw-r--r--glanceclient/tests/unit/v2/test_images.py13
-rw-r--r--glanceclient/tests/unit/v2/test_shell_v2.py734
-rw-r--r--glanceclient/v2/image_schema.py5
-rw-r--r--glanceclient/v2/images.py10
-rw-r--r--glanceclient/v2/shell.py142
-rw-r--r--lower-constraints.txt81
-rw-r--r--releasenotes/notes/http-headers-per-rfc-8187-aafa3199f863be81.yaml14
-rw-r--r--releasenotes/notes/rocky-2.11.0-ba936fd5e969198d.yaml12
-rw-r--r--requirements.txt4
-rw-r--r--setup.cfg16
-rw-r--r--test-requirements.txt8
-rwxr-xr-xtools/fix_ca_bundle.sh4
-rw-r--r--tox.ini50
37 files changed, 1408 insertions, 467 deletions
diff --git a/.gitignore b/.gitignore
index a407c52..a313258 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@ python_glanceclient.egg-info
ChangeLog
run_tests.err.log
.testrepository
+.stestr/
.tox
doc/source/api
doc/build
diff --git a/.stestr.conf b/.stestr.conf
new file mode 100644
index 0000000..44d7432
--- /dev/null
+++ b/.stestr.conf
@@ -0,0 +1,3 @@
+[DEFAULT]
+test_path=./glanceclient/tests/unit
+top_path=./
diff --git a/.testr.conf b/.testr.conf
deleted file mode 100644
index a2f6f4d..0000000
--- a/.testr.conf
+++ /dev/null
@@ -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
diff --git a/.zuul.yaml b/.zuul.yaml
index 2a784b9..a8f7330 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,16 +1,54 @@
- job:
+ name: glanceclient-dsvm-functional-v1
+ parent: devstack-tox-functional
+ description: |
+ Devstack-based functional tests for glanceclient
+ against the Image API v1.
+
+ The Image API v1 is removed from glance in Rocky, but
+ is still supported by glanceclient until the S cycle,
+ so we test it against glance stable/queens.
+
+ THIS JOB SHOULD BE REMOVED AT THE BEGINNING OF THE S
+ CYCLE.
+ override-checkout: stable/queens
+ required-projects:
+ - name: openstack/python-glanceclient
+ override-checkout: master
+ timeout: 4200
+ vars:
+ tox_envlist: functional-v1
+ devstack_localrc:
+ GLANCE_V1_ENABLED: true
+ devstack_services:
+ # turn off ceilometer
+ ceilometer-acentral: false
+ ceilometer-acompute: false
+ ceilometer-alarm-evaluator: false
+ ceilometer-alarm-notifier: false
+ ceilometer-anotification: false
+ ceilometer-api: false
+ ceilometer-collector: false
+ # turn on swift
+ s-account: true
+ s-container: true
+ s-object: true
+ s-proxy: true
+ # Hardcode glanceclient path so the job can be run on glance patches
+ zuul_work_dir: src/git.openstack.org/openstack/python-glanceclient
+
+- job:
name: glanceclient-dsvm-functional
parent: devstack-tox-functional
description: |
- devstack-based functional tests for glanceclient
+ Devstack-based functional tests for glanceclient.
+
+ These test glanceclient against Image API v2 only.
required-projects:
- openstack/python-glanceclient
timeout: 4200
vars:
devstack_localrc:
- # TODO(rosmaita): remove when glanceclient tests no longer
- # use the Images v1 API
- GLANCE_V1_ENABLED: true
LIBS_FROM_GIT: python-glanceclient
devstack_services:
# turn off ceilometer
@@ -30,18 +68,76 @@
zuul_work_dir: src/git.openstack.org/openstack/python-glanceclient
- job:
- name: glanceclient-dsvm-functional-identity-v3-only
+ name: glanceclient-tox-keystone-tips-base
+ parent: tox
+ description: Abstract job for glanceclient vs. keystone
+ required-projects:
+ - name: openstack/keystoneauth
+
+- job:
+ name: glanceclient-tox-py27-keystone-tips
+ parent: glanceclient-tox-keystone-tips-base
+ description: |
+ glanceclient py27 unit tests vs. keystone masters
+ vars:
+ tox_envlist: py27
+
+- job:
+ name: glanceclient-tox-py35-keystone-tips
+ parent: glanceclient-tox-keystone-tips-base
+ description: |
+ glanceclient py35 unit tests vs. keystone masters
+ vars:
+ tox_envlist: py35
+
+- job:
+ name: glanceclient-tox-oslo-tips-base
+ parent: tox
+ description: Abstract job for glanceclient vs. oslo
+ required-projects:
+ - name: openstack/oslo.i18n
+ - name: openstack/oslo.utils
+
+- job:
+ name: glanceclient-tox-py27-oslo-tips
+ parent: glanceclient-tox-oslo-tips-base
+ description: |
+ glanceclient py27 unit tests vs. oslo masters
+ vars:
+ tox_envlist: py27
+
+- job:
+ name: glanceclient-tox-py35-oslo-tips
+ parent: glanceclient-tox-oslo-tips-base
+ description: |
+ glanceclient py35 unit tests vs. oslo masters
+ vars:
+ tox_envlist: py35
+
+- job:
+ name: glanceclient-dsvm-functional-py3
parent: glanceclient-dsvm-functional
vars:
devstack_localrc:
- ENABLE_IDENTITY_V2: false
+ USE_PYTHON3: true
- project:
check:
jobs:
+ - glanceclient-dsvm-functional-v1
- glanceclient-dsvm-functional
- - glanceclient-dsvm-functional-identity-v3-only:
- voting: false
+ - openstack-tox-lower-constraints
gate:
jobs:
+ - glanceclient-dsvm-functional-v1
- glanceclient-dsvm-functional
+ - openstack-tox-lower-constraints
+ periodic:
+ jobs:
+ - glanceclient-tox-py27-keystone-tips
+ - glanceclient-tox-py35-keystone-tips
+ - glanceclient-tox-py27-oslo-tips
+ - glanceclient-tox-py35-oslo-tips
+ experimental:
+ jobs:
+ - glanceclient-dsvm-functional-py3
diff --git a/README.rst b/README.rst
index a438d6d..c290659 100644
--- a/README.rst
+++ b/README.rst
@@ -22,13 +22,9 @@ 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/
+ :target: https://pypi.org/project/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>`_.
@@ -45,7 +41,7 @@ See release notes and more at `<http://docs.openstack.org/python-glanceclient/>`
* `Specs`_
* `How to Contribute`_
-.. _PyPi: https://pypi.python.org/pypi/python-glanceclient
+.. _PyPi: https://pypi.org/project/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
@@ -53,4 +49,4 @@ See release notes and more at `<http://docs.openstack.org/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/
-
+.. _Release notes: https://docs.openstack.org/releasenotes/python-glanceclient
diff --git a/doc/requirements.txt b/doc/requirements.txt
new file mode 100644
index 0000000..4faabc1
--- /dev/null
+++ b/doc/requirements.txt
@@ -0,0 +1,7 @@
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+openstackdocstheme>=1.18.1 # Apache-2.0
+reno>=2.5.0 # Apache-2.0
+sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
+sphinxcontrib-apidoc>=0.2.0 # BSD
diff --git a/doc/source/cli/property-keys.rst b/doc/source/cli/property-keys.rst
index a169cdc..9e59124 100644
--- a/doc/source/cli/property-keys.rst
+++ b/doc/source/cli/property-keys.rst
@@ -2,354 +2,28 @@
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.
+You can use the glanceclient command line interface to set image properties
+that can be consumed by other services to affect the behavior of those other
+services.
+
+Properties can be set on an image at the time of image creation or they
+can be set on an existing image. Use the :command:`openstack image create`
+and :command:`openstack image set` commands respectively.
+
For example:
.. code-block:: console
$ openstack image set IMG-UUID --property architecture=x86_64
+For a list of image properties that can be used to affect the behavior
+of other services, refer to `Useful image properties
+<https://docs.openstack.org/glance/latest/admin/useful-image-properties.html>`_
+in the Glance Administration Guide.
+
.. note::
Behavior set using image properties overrides behavior set using flavors.
- For more information, refer to the `Manage images
+ For more information, refer to `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_pointer_model``
- - Input devices that allow interaction with a graphical framebuffer,
- for example to provide a graphic tablet for absolute cursor movement.
- Currently only supported by the KVM/QEMU hypervisor configuration
- and VNC or SPICE consoles must be enabled.
- - ``usbtablet``
- * - 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``
- * - libvirt API driver
- - ``img_hide_hypervisor_id``
- - Some hypervisors add a signature to their guests. While the presence
- of the signature can enable some paravirtualization features on the
- guest, it can also have the effect of preventing some drivers from
- loading. Hiding the signature by setting this property to ``true``
- may allow such drivers to load and work.
- - ``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``
+ in the Glance Administration Guide.
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 98ab8c9..7d18119 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -26,10 +26,18 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),
# 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',
+ 'sphinxcontrib.apidoc',
]
+# sphinxcontrib.apidoc options
+apidoc_module_dir = '../../glanceclient'
+apidoc_output_dir = 'reference/api'
+apidoc_excluded_paths = [
+ 'tests/*',
+ 'tests']
+apidoc_separate_modules = True
+
# openstackdocstheme options
repository_name = 'openstack/python-glanceclient'
bug_project = 'python-glanceclient'
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 43ee57f..31b921c 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -3,7 +3,7 @@
==============================================
This is a client for the OpenStack Images API. There's :doc:`a Python
-API <reference/api/index>` (the :mod:`glanceclient` module) and a
+API <reference/api/modules>` (the :mod:`glanceclient` module) and a
:doc:`command-line script <cli/glance>` (installed as
:program:`glance`).
diff --git a/doc/source/reference/api/index.rst b/doc/source/reference/api/index.rst
deleted file mode 100644
index df916b6..0000000
--- a/doc/source/reference/api/index.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-======================
- Python API Reference
-======================
-
-.. toctree::
- :maxdepth: 2
-
- autoindex
diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst
index 33ce8b2..7710a1f 100644
--- a/doc/source/reference/index.rst
+++ b/doc/source/reference/index.rst
@@ -23,5 +23,5 @@ done so, you can use the API like so::
.. toctree::
:maxdepth: 2
- api/index
+ Python API Reference <api/modules>
apiv2
diff --git a/glanceclient/common/http.py b/glanceclient/common/http.py
index fc635ff..84cdc69 100644
--- a/glanceclient/common/http.py
+++ b/glanceclient/common/http.py
@@ -24,6 +24,7 @@ from oslo_utils import importutils
from oslo_utils import netutils
import requests
import six
+import six.moves.urllib.parse as urlparse
try:
import json
@@ -53,8 +54,26 @@ def encode_headers(headers):
: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)
+ # NOTE(rosmaita): This function's rejection of any header name without a
+ # corresponding value is arguably justified by RFC 7230. In any case, that
+ # behavior was already here and there is an existing unit test for it.
+
+ # Bug #1766235: According to RFC 8187, headers must be encoded as ASCII.
+ # So we first %-encode them to get them into range < 128 and then turn
+ # them into ASCII.
+ if six.PY2:
+ # incoming items may be unicode, so get them into something
+ # the py2 version of urllib can handle before percent encoding
+ encoded_dict = dict((urlparse.quote(encodeutils.safe_encode(h)),
+ urlparse.quote(encodeutils.safe_encode(v)))
+ for h, v in headers.items() if v is not None)
+ else:
+ encoded_dict = dict((urlparse.quote(h), urlparse.quote(v))
+ for h, v in headers.items() if v is not None)
+
+ return dict((encodeutils.safe_encode(h, encoding='ascii'),
+ encodeutils.safe_encode(v, encoding='ascii'))
+ for h, v in encoded_dict.items())
class _BaseHTTPClient(object):
diff --git a/glanceclient/common/utils.py b/glanceclient/common/utils.py
index d194e65..dee9978 100644
--- a/glanceclient/common/utils.py
+++ b/glanceclient/common/utils.py
@@ -177,6 +177,12 @@ def pretty_choice_list(l):
def print_list(objs, fields, formatters=None, field_settings=None):
+ '''Prints a list of objects.
+
+ @param objs: Objects to print
+ @param fields: Fields on each object to be printed
+ @param formatters: Custom field formatters
+ '''
formatters = formatters or {}
field_settings = field_settings or {}
pt = prettytable.PrettyTable([f for f in fields], caching=False)
@@ -196,11 +202,36 @@ def print_list(objs, fields, formatters=None, field_settings=None):
field_name = field.lower().replace(' ', '_')
data = getattr(o, field_name, None) or ''
row.append(data)
+ count = 0
+ # Converts unicode values in list to string
+ for part in row:
+ count = count + 1
+ if isinstance(part, list):
+ part = unicode_key_value_to_string(part)
+ row[count - 1] = part
pt.add_row(row)
print(encodeutils.safe_decode(pt.get_string()))
+def _encode(src):
+ """remove extra 'u' in PY2."""
+ if six.PY2 and isinstance(src, unicode):
+ return src.encode('utf-8')
+ return src
+
+
+def unicode_key_value_to_string(src):
+ """Recursively converts dictionary keys to strings."""
+ if isinstance(src, dict):
+ return dict((_encode(k),
+ _encode(unicode_key_value_to_string(v)))
+ for k, v in src.items())
+ if isinstance(src, list):
+ return [unicode_key_value_to_string(l) for l in src]
+ return _encode(src)
+
+
def print_dict(d, max_column_width=80):
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
pt.align = 'l'
@@ -380,7 +411,7 @@ def strip_version(endpoint):
(scheme, netloc, path, __, __, __) = url_parts
path = path.lstrip('/')
# regex to match 'v1' or 'v2.0' etc
- if re.match('v\d+\.?\d*', path):
+ if re.match(r'v\d+\.?\d*', path):
version = float(path.lstrip('v'))
endpoint = scheme + '://' + netloc
return endpoint, version
@@ -390,6 +421,8 @@ 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 'virtual_size' in image:
+ image['virtual_size'] = image.get('virtual_size') or 'Not available'
if human_readable:
image['size'] = make_size_human_readable(image['size'])
if str(max_col_width).isdigit():
diff --git a/glanceclient/tests/functional/base.py b/glanceclient/tests/functional/base.py
index 0efc079..578dc39 100644
--- a/glanceclient/tests/functional/base.py
+++ b/glanceclient/tests/functional/base.py
@@ -48,9 +48,10 @@ class ClientTestBase(base.ClientTestBase):
def _get_clients(self):
self.creds = credentials().get_auth_args()
+ venv_name = os.environ.get('OS_TESTENV_NAME', 'functional')
cli_dir = os.environ.get(
'OS_GLANCECLIENT_EXEC_DIR',
- os.path.join(os.path.abspath('.'), '.tox/functional/bin'))
+ os.path.join(os.path.abspath('.'), '.tox/%s/bin' % venv_name))
return base.CLIClient(
username=self.creds['username'],
diff --git a/glanceclient/tests/functional/v1/__init__.py b/glanceclient/tests/functional/v1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glanceclient/tests/functional/v1/__init__.py
diff --git a/glanceclient/tests/functional/v1/test_readonly_glance.py b/glanceclient/tests/functional/v1/test_readonly_glance.py
new file mode 100644
index 0000000..122c61b
--- /dev/null
+++ b/glanceclient/tests/functional/v1/test_readonly_glance.py
@@ -0,0 +1,73 @@
+# 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_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_help(self):
+ help_text = self.glance('--os-image-api-version 1 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-delete',
+ 'image-download', 'image-list',
+ 'image-show', 'image-update',
+ 'member-create', 'member-delete',
+ 'member-list'}
+ self.assertEqual(commands, wanted_commands)
+
+ def test_version(self):
+ self.glance('', flags='--version')
+
+ def test_debug_list(self):
+ self.glance('--os-image-api-version 1 image-list', flags='--debug')
diff --git a/glanceclient/tests/functional/v2/__init__.py b/glanceclient/tests/functional/v2/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/glanceclient/tests/functional/v2/__init__.py
diff --git a/glanceclient/tests/functional/test_http_headers.py b/glanceclient/tests/functional/v2/test_http_headers.py
index 1596444..1596444 100644
--- a/glanceclient/tests/functional/test_http_headers.py
+++ b/glanceclient/tests/functional/v2/test_http_headers.py
diff --git a/glanceclient/tests/functional/test_readonly_glance.py b/glanceclient/tests/functional/v2/test_readonly_glance.py
index ccd49d6..c024303 100644
--- a/glanceclient/tests/functional/test_readonly_glance.py
+++ b/glanceclient/tests/functional/v2/test_readonly_glance.py
@@ -24,13 +24,6 @@ class SimpleReadOnlyGlanceClientTest(base.ClientTestBase):
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)
@@ -41,14 +34,6 @@ class SimpleReadOnlyGlanceClientTest(base.ClientTestBase):
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
diff --git a/glanceclient/tests/unit/test_http.py b/glanceclient/tests/unit/test_http.py
index cec94e0..efd15bf 100644
--- a/glanceclient/tests/unit/test_http.py
+++ b/glanceclient/tests/unit/test_http.py
@@ -216,10 +216,15 @@ class TestClient(testtools.TestCase):
def test_headers_encoding(self):
value = u'ni\xf1o'
- headers = {"test": value, "none-val": None}
+ headers = {"test": value, "none-val": None, "Name": "value"}
encoded = http.encode_headers(headers)
- self.assertEqual(b"ni\xc3\xb1o", encoded[b"test"])
+ # Bug #1766235: According to RFC 8187, headers must be
+ # encoded as 7-bit ASCII, so expect to see only displayable
+ # chars in percent-encoding
+ self.assertEqual(b"ni%C3%B1o", encoded[b"test"])
self.assertNotIn("none-val", encoded)
+ self.assertNotIn(b"none-val", encoded)
+ self.assertEqual(b"value", encoded[b"Name"])
@mock.patch('keystoneauth1.adapter.Adapter.request')
def test_http_duplicate_content_type_headers(self, mock_ksarq):
@@ -466,4 +471,7 @@ class TestClient(testtools.TestCase):
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'])
+ # Bug #1766235: According to RFC 8187, headers must be
+ # encoded as 7-bit ASCII, so expect to see only displayable
+ # chars in percent-encoding
+ self.assertEqual(b'ni%C3%B1o', headers['X-Auth-Token'])
diff --git a/glanceclient/tests/unit/test_shell.py b/glanceclient/tests/unit/test_shell.py
index 898a9eb..0f15007 100644
--- a/glanceclient/tests/unit/test_shell.py
+++ b/glanceclient/tests/unit/test_shell.py
@@ -15,10 +15,7 @@
# under the License.
import argparse
-try:
- from collections import OrderedDict
-except ImportError:
- from ordereddict import OrderedDict
+from collections import OrderedDict
import hashlib
import logging
import os
@@ -802,10 +799,10 @@ class ShellCacheSchemaTest(testutils.TestCase):
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])
+ actual = json.loads(open.mock_calls[2][1][0])
+ self.assertEqual(schema_odict, actual)
+ actual = json.loads(open.mock_calls[6][1][0])
+ self.assertEqual(schema_odict, actual)
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
@mock.patch('os.path.exists', side_effect=[True, False, False, False])
@@ -825,10 +822,10 @@ class ShellCacheSchemaTest(testutils.TestCase):
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])
+ actual = json.loads(open.mock_calls[2][1][0])
+ self.assertEqual(schema_odict, actual)
+ actual = json.loads(open.mock_calls[6][1][0])
+ self.assertEqual(schema_odict, actual)
@mock.patch('six.moves.builtins.open', new=mock.mock_open(), create=True)
@mock.patch('os.path.exists', return_value=True)
diff --git a/glanceclient/tests/unit/test_utils.py b/glanceclient/tests/unit/test_utils.py
index cd87e21..3ef585a 100644
--- a/glanceclient/tests/unit/test_utils.py
+++ b/glanceclient/tests/unit/test_utils.py
@@ -115,6 +115,80 @@ class TestUtils(testtools.TestCase):
''',
output_dict.getvalue())
+ def test_print_list_with_list_no_unicode(self):
+ class Struct(object):
+ def __init__(self, **entries):
+ self.__dict__.update(entries)
+
+ # test for removing 'u' from lists in print_list output
+ columns = ['ID', 'Tags']
+ images = [Struct(**{'id': 'b8e1c57e-907a-4239-aed8-0df8e54b8d2d',
+ 'tags': [u'Name1', u'Tag_123', u'veeeery long']})]
+ saved_stdout = sys.stdout
+ try:
+ sys.stdout = output_list = six.StringIO()
+ utils.print_list(images, columns)
+
+ finally:
+ sys.stdout = saved_stdout
+
+ self.assertEqual('''\
++--------------------------------------+--------------------------------------+
+| ID | Tags |
++--------------------------------------+--------------------------------------+
+| b8e1c57e-907a-4239-aed8-0df8e54b8d2d | ['Name1', 'Tag_123', 'veeeery long'] |
++--------------------------------------+--------------------------------------+
+''',
+ output_list.getvalue())
+
+ def test_print_image_virtual_size_available(self):
+ image = {'id': '42', 'virtual_size': 1337}
+ saved_stdout = sys.stdout
+ try:
+ sys.stdout = output_list = six.StringIO()
+ utils.print_image(image)
+ finally:
+ sys.stdout = saved_stdout
+
+ self.assertEqual('''\
++--------------+-------+
+| Property | Value |
++--------------+-------+
+| id | 42 |
+| virtual_size | 1337 |
++--------------+-------+
+''',
+ output_list.getvalue())
+
+ def test_print_image_virtual_size_not_available(self):
+ image = {'id': '42', 'virtual_size': None}
+ saved_stdout = sys.stdout
+ try:
+ sys.stdout = output_list = six.StringIO()
+ utils.print_image(image)
+ finally:
+ sys.stdout = saved_stdout
+
+ self.assertEqual('''\
++--------------+---------------+
+| Property | Value |
++--------------+---------------+
+| id | 42 |
+| virtual_size | Not available |
++--------------+---------------+
+''',
+ output_list.getvalue())
+
+ def test_unicode_key_value_to_string(self):
+ src = {u'key': u'\u70fd\u7231\u5a77'}
+ expected = {'key': '\xe7\x83\xbd\xe7\x88\xb1\xe5\xa9\xb7'}
+ if six.PY2:
+ self.assertEqual(expected, utils.unicode_key_value_to_string(src))
+ else:
+ # u'xxxx' in PY3 is str, we will not get extra 'u' from cli
+ # output in PY3
+ self.assertEqual(src, utils.unicode_key_value_to_string(src))
+
def test_schema_args_with_list_types(self):
# NOTE(flaper87): Regression for bug
# https://bugs.launchpad.net/python-glanceclient/+bug/1401032
diff --git a/glanceclient/tests/unit/v2/base.py b/glanceclient/tests/unit/v2/base.py
index 7391595..d6f5cc5 100644
--- a/glanceclient/tests/unit/v2/base.py
+++ b/glanceclient/tests/unit/v2/base.py
@@ -106,6 +106,10 @@ class BaseController(testtools.TestCase):
resp = self.controller.deassociate(*args)
self._assertRequestId(resp)
+ def image_import(self, *args):
+ resp = self.controller.image_import(*args)
+ self._assertRequestId(resp)
+
class BaseResourceTypeController(BaseController):
def __init__(self, api, schema_api, controller_class):
diff --git a/glanceclient/tests/unit/v2/fixtures.py b/glanceclient/tests/unit/v2/fixtures.py
index 7f0e99c..5a603c0 100644
--- a/glanceclient/tests/unit/v2/fixtures.py
+++ b/glanceclient/tests/unit/v2/fixtures.py
@@ -303,12 +303,15 @@ schema_fixture = {
"readOnly": True,
"description": "Status of the image",
"enum": [
+ "deactivated",
"queued",
"saving",
"active",
"killed",
"deleted",
- "pending_delete"
+ "pending_delete",
+ "uploading",
+ "importing"
],
"type": "string"
},
diff --git a/glanceclient/tests/unit/v2/test_images.py b/glanceclient/tests/unit/v2/test_images.py
index 579392b..23cbb43 100644
--- a/glanceclient/tests/unit/v2/test_images.py
+++ b/glanceclient/tests/unit/v2/test_images.py
@@ -215,6 +215,9 @@ data_fixtures = {
'/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a/actions/deactivate': {
'POST': ({}, None)
},
+ '/v2/images/606b0e88-7c5a-4d54-b5bb-046105d4de6f/import': {
+ 'POST': ({}, None)
+ },
'/v2/images?limit=%d&visibility=public' % images.DEFAULT_PAGE_SIZE: {
'GET': (
{},
@@ -867,6 +870,16 @@ class TestController(testtools.TestCase):
body = ''.join([b for b in body])
self.assertEqual('CCC', body)
+ def test_image_import(self):
+ uri = 'http://example.com/image.qcow'
+ data = [('method', {'name': 'web-download',
+ 'uri': uri})]
+ image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f'
+ self.controller.image_import(image_id, 'web-download', uri)
+ expect = [('POST', '/v2/images/%s/import' % image_id, {},
+ data)]
+ self.assertEqual(expect, self.api.calls)
+
def test_download_no_data(self):
resp = utils.FakeResponse(headers={}, status_code=204)
self.controller.controller.http_client.get = mock.Mock(
diff --git a/glanceclient/tests/unit/v2/test_shell_v2.py b/glanceclient/tests/unit/v2/test_shell_v2.py
index d75613f..f2f30e0 100644
--- a/glanceclient/tests/unit/v2/test_shell_v2.py
+++ b/glanceclient/tests/unit/v2/test_shell_v2.py
@@ -14,10 +14,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import argparse
+from copy import deepcopy
import json
import mock
import os
import six
+import sys
import tempfile
import testtools
@@ -157,6 +159,46 @@ class ShellV2Test(testtools.TestCase):
self.assertEqual('error: Must provide --disk-format when using stdin.',
e.message)
+ @mock.patch('sys.stderr')
+ def test_create_via_import_glance_direct_missing_disk_format(self, __):
+ e = self.assertRaises(exc.CommandError, self._run_command,
+ '--os-image-api-version 2 '
+ 'image-create-via-import '
+ '--file fake_src --container-format bare')
+ self.assertEqual('error: Must provide --disk-format when using '
+ '--file.', e.message)
+
+ @mock.patch('sys.stderr')
+ def test_create_via_import_glance_direct_missing_container_format(
+ self, __):
+ e = self.assertRaises(exc.CommandError, self._run_command,
+ '--os-image-api-version 2 '
+ 'image-create-via-import '
+ '--file fake_src --disk-format qcow2')
+ self.assertEqual('error: Must provide --container-format when '
+ 'using --file.', e.message)
+
+ @mock.patch('sys.stderr')
+ def test_create_via_import_web_download_missing_disk_format(self, __):
+ e = self.assertRaises(exc.CommandError, self._run_command,
+ '--os-image-api-version 2 '
+ 'image-create-via-import ' +
+ '--import-method web-download ' +
+ '--uri fake_uri --container-format bare')
+ self.assertEqual('error: Must provide --disk-format when using '
+ '--uri.', e.message)
+
+ @mock.patch('sys.stderr')
+ def test_create_via_import_web_download_missing_container_format(
+ self, __):
+ e = self.assertRaises(exc.CommandError, self._run_command,
+ '--os-image-api-version 2 '
+ 'image-create-via-import '
+ '--import-method web-download '
+ '--uri fake_uri --disk-format qcow2')
+ self.assertEqual('error: Must provide --container-format when '
+ 'using --uri.', e.message)
+
def test_do_image_list(self):
input = {
'limit': None,
@@ -447,6 +489,491 @@ class ShellV2Test(testtools.TestCase):
utils.print_dict.assert_called_once_with({
'id': 'pass', 'name': 'IMG-01', 'myprop': 'myval'})
+ # NOTE(rosmaita): have to explicitly set to None the declared but unused
+ # arguments (the configparser does that for us normally)
+ base_args = {'name': 'Mortimer',
+ 'disk_format': 'raw',
+ 'container_format': 'bare',
+ 'progress': False,
+ 'file': None,
+ 'uri': None,
+ 'import_method': None}
+
+ import_info_response = {'import-methods': {
+ 'type': 'array',
+ 'description': 'Import methods available.',
+ 'value': ['glance-direct', 'web-download']}}
+
+ def _mock_utils_exit(self, msg):
+ sys.exit(msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('os.access')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_no_method_with_file_and_stdin(
+ self, mock_stdin, mock_access, mock_utils_exit):
+ expected_msg = ('You cannot use both --file and stdin with the '
+ 'glance-direct import method.')
+ my_args = self.base_args.copy()
+ my_args['file'] = 'some.file'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: False
+ mock_access.return_value = True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_no_method_passing_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You cannot use --uri without specifying an import '
+ 'method.')
+ my_args = self.base_args.copy()
+ my_args['uri'] = 'http://example.com/whatever'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_glance_direct_no_data(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You must specify a --file or provide data via stdin '
+ 'for the glance-direct import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'glance-direct'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_glance_direct_with_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You cannot specify a --uri with the glance-direct '
+ 'import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'glance-direct'
+ my_args['uri'] = 'https://example.com/some/stuff'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('os.access')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_glance_direct_with_file_and_uri(
+ self, mock_stdin, mock_access, mock_utils_exit):
+ expected_msg = ('You cannot specify a --uri with the glance-direct '
+ 'import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'glance-direct'
+ my_args['uri'] = 'https://example.com/some/stuff'
+ my_args['file'] = 'my.browncow'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_access.return_value = True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_glance_direct_with_data_and_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You cannot specify a --uri with the glance-direct '
+ 'import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'glance-direct'
+ my_args['uri'] = 'https://example.com/some/stuff'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: False
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_web_download_no_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('URI is required for web-download import method. '
+ 'Please use \'--uri <uri>\'.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'web-download'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_web_download_no_uri_with_file(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('URI is required for web-download import method. '
+ 'Please use \'--uri <uri>\'.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'web-download'
+ my_args['file'] = 'my.browncow'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_web_download_no_uri_with_data(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('URI is required for web-download import method. '
+ 'Please use \'--uri <uri>\'.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'web-download'
+ my_args['file'] = 'my.browncow'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: False
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_web_download_with_data_and_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You cannot pass data via stdin with the web-download '
+ 'import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'web-download'
+ my_args['uri'] = 'https://example.com/some/stuff'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: False
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_web_download_with_file_and_uri(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('You cannot specify a --file with the web-download '
+ 'import method.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'web-download'
+ my_args['uri'] = 'https://example.com/some/stuff'
+ my_args['file'] = 'my.browncow'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_bad_method(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('Import method \'swift-party-time\' is not valid '
+ 'for this cloud. Valid values can be retrieved with '
+ 'import-info command.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'swift-party-time'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_no_method_with_data_and_method_NA(
+ self, mock_stdin, mock_utils_exit):
+ expected_msg = ('Import method \'glance-direct\' is not valid '
+ 'for this cloud. Valid values can be retrieved with '
+ 'import-info command.')
+ args = self._make_args(self.base_args)
+ # need to fake some data, or this is "just like" a
+ # create-image-record-only call
+ mock_stdin.isatty = lambda: False
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ my_import_info_response = deepcopy(self.import_info_response)
+ my_import_info_response['import-methods']['value'] = ['web-download']
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = my_import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_neg_image_create_via_import_good_method_not_available(
+ self, mock_stdin, mock_utils_exit):
+ """Make sure the good method names aren't hard coded somewhere"""
+ expected_msg = ('Import method \'glance-direct\' is not valid for '
+ 'this cloud. Valid values can be retrieved with '
+ 'import-info command.')
+ my_args = self.base_args.copy()
+ my_args['import_method'] = 'glance-direct'
+ args = self._make_args(my_args)
+ mock_stdin.isatty = lambda: True
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ my_import_info_response = deepcopy(self.import_info_response)
+ my_import_info_response['import-methods']['value'] = ['bad-bad-method']
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = my_import_info_response
+ try:
+ test_shell.do_image_create_via_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.v2.shell.do_image_import')
+ @mock.patch('glanceclient.v2.shell.do_image_stage')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_image_create_via_import_no_method_with_stdin(
+ self, mock_stdin, mock_do_stage, mock_do_import):
+ """Backward compat -> handle this like a glance-direct"""
+ mock_stdin.isatty = lambda: False
+ self.mock_get_data_file.return_value = six.StringIO()
+ args = self._make_args(self.base_args)
+ with mock.patch.object(self.gc.images, 'create') as mocked_create:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+
+ ignore_fields = ['self', 'access', 'schema']
+ expect_image = dict([(field, field) for field in
+ ignore_fields])
+ expect_image['id'] = 'via-stdin'
+ expect_image['name'] = 'Mortimer'
+ expect_image['disk_format'] = 'raw'
+ expect_image['container_format'] = 'bare'
+ mocked_create.return_value = expect_image
+ mocked_get.return_value = expect_image
+ mocked_info.return_value = self.import_info_response
+
+ test_shell.do_image_create_via_import(self.gc, args)
+ mocked_create.assert_called_once()
+ mock_do_stage.assert_called_once()
+ mock_do_import.assert_called_once()
+ mocked_get.assert_called_with('via-stdin')
+ utils.print_dict.assert_called_with({
+ 'id': 'via-stdin', 'name': 'Mortimer',
+ 'disk_format': 'raw', 'container_format': 'bare'})
+
+ @mock.patch('glanceclient.v2.shell.do_image_import')
+ @mock.patch('glanceclient.v2.shell.do_image_stage')
+ @mock.patch('os.access')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_image_create_via_import_no_method_passing_file(
+ self, mock_stdin, mock_access, mock_do_stage, mock_do_import):
+ """Backward compat -> handle this like a glance-direct"""
+ mock_stdin.isatty = lambda: True
+ self.mock_get_data_file.return_value = six.StringIO()
+ mock_access.return_value = True
+ my_args = self.base_args.copy()
+ my_args['file'] = 'fake-image-file.browncow'
+ args = self._make_args(my_args)
+ with mock.patch.object(self.gc.images, 'create') as mocked_create:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+
+ ignore_fields = ['self', 'access', 'schema']
+ expect_image = dict([(field, field) for field in
+ ignore_fields])
+ expect_image['id'] = 'via-file'
+ expect_image['name'] = 'Mortimer'
+ expect_image['disk_format'] = 'raw'
+ expect_image['container_format'] = 'bare'
+ mocked_create.return_value = expect_image
+ mocked_get.return_value = expect_image
+ mocked_info.return_value = self.import_info_response
+
+ test_shell.do_image_create_via_import(self.gc, args)
+ mocked_create.assert_called_once()
+ mock_do_stage.assert_called_once()
+ mock_do_import.assert_called_once()
+ mocked_get.assert_called_with('via-file')
+ utils.print_dict.assert_called_with({
+ 'id': 'via-file', 'name': 'Mortimer',
+ 'disk_format': 'raw', 'container_format': 'bare'})
+
+ @mock.patch('glanceclient.v2.shell.do_image_import')
+ @mock.patch('glanceclient.v2.shell.do_image_stage')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_do_image_create_via_import_with_no_method_no_data(
+ self, mock_stdin, mock_do_image_stage, mock_do_image_import):
+ """Create an image record without calling do_stage or do_import"""
+ img_create_args = {'name': 'IMG-11',
+ 'os_architecture': 'powerpc',
+ 'id': 'watch-out-for-ossn-0075',
+ 'progress': False}
+ client_args = {'import_method': None,
+ 'file': None,
+ 'uri': None}
+ temp_args = img_create_args.copy()
+ temp_args.update(client_args)
+ args = self._make_args(temp_args)
+ with mock.patch.object(self.gc.images, 'create') as mocked_create:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+
+ ignore_fields = ['self', 'access', 'schema']
+ expect_image = dict([(field, field) for field in
+ ignore_fields])
+ expect_image['name'] = 'IMG-11'
+ expect_image['id'] = 'watch-out-for-ossn-0075'
+ expect_image['os_architecture'] = 'powerpc'
+ mocked_create.return_value = expect_image
+ mocked_get.return_value = expect_image
+ mocked_info.return_value = self.import_info_response
+ mock_stdin.isatty = lambda: True
+
+ test_shell.do_image_create_via_import(self.gc, args)
+ mocked_create.assert_called_once_with(**img_create_args)
+ mocked_get.assert_called_with('watch-out-for-ossn-0075')
+ mock_do_image_stage.assert_not_called()
+ mock_do_image_import.assert_not_called()
+ utils.print_dict.assert_called_with({
+ 'name': 'IMG-11', 'os_architecture': 'powerpc',
+ 'id': 'watch-out-for-ossn-0075'})
+
+ @mock.patch('glanceclient.v2.shell.do_image_import')
+ @mock.patch('glanceclient.v2.shell.do_image_stage')
+ @mock.patch('sys.stdin', autospec=True)
+ def test_do_image_create_via_import_with_web_download(
+ self, mock_stdin, mock_do_image_stage, mock_do_image_import):
+ temp_args = {'name': 'IMG-01',
+ 'disk_format': 'vhd',
+ 'container_format': 'bare',
+ 'uri': 'http://example.com/image.qcow',
+ 'import_method': 'web-download',
+ 'progress': False}
+ args = self._make_args(temp_args)
+ with mock.patch.object(self.gc.images, 'create') as mocked_create:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+
+ ignore_fields = ['self', 'access', 'schema']
+ expect_image = dict([(field, field) for field in
+ ignore_fields])
+ expect_image['id'] = 'pass'
+ expect_image['name'] = 'IMG-01'
+ expect_image['disk_format'] = 'vhd'
+ expect_image['container_format'] = 'bare'
+ expect_image['status'] = 'queued'
+ mocked_create.return_value = expect_image
+ mocked_get.return_value = expect_image
+ mocked_info.return_value = self.import_info_response
+ mock_stdin.isatty = lambda: True
+
+ test_shell.do_image_create_via_import(self.gc, args)
+ mock_do_image_stage.assert_not_called()
+ mock_do_image_import.assert_called_once()
+ mocked_create.assert_called_once_with(**temp_args)
+ mocked_get.assert_called_with('pass')
+ utils.print_dict.assert_called_with({
+ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd',
+ 'container_format': 'bare', 'status': 'queued'})
+
def test_do_image_update_no_user_props(self):
args = self._make_args({'id': 'pass', 'name': 'IMG-01',
'disk_format': 'vhd',
@@ -577,6 +1104,204 @@ class ShellV2Test(testtools.TestCase):
test_shell.do_image_upload(self.gc, args)
mocked_upload.assert_called_once_with('IMG-01', 'testfile', 1024)
+ @mock.patch('glanceclient.common.utils.exit')
+ def test_neg_image_import_not_available(self, mock_utils_exit):
+ expected_msg = 'Target Glance does not support Image Import workflow'
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ args = self._make_args(
+ {'id': 'IMG-01', 'import_method': 'smarty-pants', 'uri': None})
+ with mock.patch.object(self.gc.images, 'import') as mocked_import:
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.side_effect = exc.HTTPNotFound
+ try:
+ test_shell.do_image_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+ mocked_import.assert_not_called()
+
+ @mock.patch('glanceclient.common.utils.exit')
+ def test_neg_image_import_bad_method(self, mock_utils_exit):
+ expected_msg = ('Import method \'smarty-pants\' is not valid for this '
+ 'cloud. Valid values can be retrieved with '
+ 'import-info command.')
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ args = self._make_args(
+ {'id': 'IMG-01', 'import_method': 'smarty-pants', 'uri': None})
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ def test_neg_image_import_no_methods_configured(self, mock_utils_exit):
+ expected_msg = ('Import method \'glance-direct\' is not valid for '
+ 'this cloud. Valid values can be retrieved with '
+ 'import-info command.')
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ args = self._make_args(
+ {'id': 'IMG-01', 'import_method': 'glance-direct', 'uri': None})
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_info.return_value = {"import-methods": {"value": []}}
+ try:
+ test_shell.do_image_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ def test_neg_image_import_glance_direct_image_not_uploading_status(
+ self, mock_utils_exit):
+ expected_msg = ('The \'glance-direct\' import method can only be '
+ 'applied to an image in status \'uploading\'')
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ args = self._make_args(
+ {'id': 'IMG-01', 'import_method': 'glance-direct', 'uri': None})
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ mocked_get.return_value = {'status': 'queued',
+ 'container_format': 'bare',
+ 'disk_format': 'raw'}
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ def test_neg_image_import_web_download_image_not_queued_status(
+ self, mock_utils_exit):
+ expected_msg = ('The \'web-download\' import method can only be '
+ 'applied to an image in status \'queued\'')
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ args = self._make_args(
+ {'id': 'IMG-01', 'import_method': 'web-download',
+ 'uri': 'http://joes-image-shack.com/funky.qcow2'})
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ mocked_get.return_value = {'status': 'uploading',
+ 'container_format': 'bare',
+ 'disk_format': 'raw'}
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ def test_neg_image_import_image_no_container_format(
+ self, mock_utils_exit):
+ expected_msg = ('The \'container_format\' and \'disk_format\' '
+ 'properties must be set on an image before it can be '
+ 'imported.')
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ args = self._make_args(
+ {'id': 'IMG-01', 'import_method': 'web-download',
+ 'uri': 'http://joes-image-shack.com/funky.qcow2'})
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ mocked_get.return_value = {'status': 'uploading',
+ 'disk_format': 'raw'}
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ @mock.patch('glanceclient.common.utils.exit')
+ def test_neg_image_import_image_no_disk_format(
+ self, mock_utils_exit):
+ expected_msg = ('The \'container_format\' and \'disk_format\' '
+ 'properties must be set on an image before it can be '
+ 'imported.')
+ mock_utils_exit.side_effect = self._mock_utils_exit
+ args = self._make_args(
+ {'id': 'IMG-01', 'import_method': 'web-download',
+ 'uri': 'http://joes-image-shack.com/funky.qcow2'})
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ mocked_get.return_value = {'status': 'uploading',
+ 'container_format': 'bare'}
+ mocked_info.return_value = self.import_info_response
+ try:
+ test_shell.do_image_import(self.gc, args)
+ self.fail("utils.exit should have been called")
+ except SystemExit:
+ pass
+ mock_utils_exit.assert_called_once_with(expected_msg)
+
+ def test_image_import_glance_direct(self):
+ args = self._make_args(
+ {'id': 'IMG-01', 'import_method': 'glance-direct', 'uri': None})
+ with mock.patch.object(self.gc.images, 'image_import') as mock_import:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_get.return_value = {'status': 'uploading',
+ 'container_format': 'bare',
+ 'disk_format': 'raw'}
+ mocked_info.return_value = self.import_info_response
+ mock_import.return_value = None
+ test_shell.do_image_import(self.gc, args)
+ mock_import.assert_called_once_with(
+ 'IMG-01', 'glance-direct', None)
+
+ def test_image_import_web_download(self):
+ args = self._make_args(
+ {'id': 'IMG-01', 'uri': 'http://example.com/image.qcow',
+ 'import_method': 'web-download'})
+ with mock.patch.object(self.gc.images, 'image_import') as mock_import:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_get.return_value = {'status': 'queued',
+ 'container_format': 'bare',
+ 'disk_format': 'raw'}
+ mocked_info.return_value = self.import_info_response
+ mock_import.return_value = None
+ test_shell.do_image_import(self.gc, args)
+ mock_import.assert_called_once_with(
+ 'IMG-01', 'web-download',
+ 'http://example.com/image.qcow')
+
+ @mock.patch('glanceclient.common.utils.print_image')
+ def test_image_import_no_print_image(self, mocked_utils_print_image):
+ args = self._make_args(
+ {'id': 'IMG-02', 'uri': None, 'import_method': 'glance-direct',
+ 'from_create': True})
+ with mock.patch.object(self.gc.images, 'image_import') as mock_import:
+ with mock.patch.object(self.gc.images, 'get') as mocked_get:
+ with mock.patch.object(self.gc.images,
+ 'get_import_info') as mocked_info:
+ mocked_get.return_value = {'status': 'uploading',
+ 'container_format': 'bare',
+ 'disk_format': 'raw'}
+ mocked_info.return_value = self.import_info_response
+ mock_import.return_value = None
+ test_shell.do_image_import(self.gc, args)
+ mock_import.assert_called_once_with(
+ 'IMG-02', 'glance-direct', None)
+ mocked_utils_print_image.assert_not_called()
+
def test_image_download(self):
args = self._make_args(
{'id': 'IMG-01', 'file': 'test', 'progress': True})
@@ -680,10 +1405,13 @@ class ShellV2Test(testtools.TestCase):
self.assert_exits_with_msg(func=test_shell.do_image_delete,
func_args=args)
+ @mock.patch('sys.stdout', autospec=True)
@mock.patch.object(utils, 'print_err')
- def test_do_image_download_with_forbidden_id(self, mocked_print_err):
+ def test_do_image_download_with_forbidden_id(self, mocked_print_err,
+ mocked_stdout):
args = self._make_args({'id': 'IMG-01', 'file': None,
'progress': False})
+ mocked_stdout.isatty = lambda: False
with mock.patch.object(self.gc.images, 'data') as mocked_data:
mocked_data.side_effect = exc.HTTPForbidden
try:
@@ -695,10 +1423,12 @@ class ShellV2Test(testtools.TestCase):
self.assertEqual(1, mocked_data.call_count)
self.assertEqual(1, mocked_print_err.call_count)
+ @mock.patch('sys.stdout', autospec=True)
@mock.patch.object(utils, 'print_err')
- def test_do_image_download_with_500(self, mocked_print_err):
+ def test_do_image_download_with_500(self, mocked_print_err, mocked_stdout):
args = self._make_args({'id': 'IMG-01', 'file': None,
'progress': False})
+ mocked_stdout.isatty = lambda: False
with mock.patch.object(self.gc.images, 'data') as mocked_data:
mocked_data.side_effect = exc.HTTPInternalServerError
try:
diff --git a/glanceclient/v2/image_schema.py b/glanceclient/v2/image_schema.py
index d4fdd69..1ff20bc 100644
--- a/glanceclient/v2/image_schema.py
+++ b/glanceclient/v2/image_schema.py
@@ -13,7 +13,7 @@
# 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
+_doc_url = "https://docs.openstack.org/python-glanceclient/latest/cli/property-keys.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
@@ -129,7 +129,8 @@ _BASE_SCHEMA = {
},
"status": {
"readOnly": True,
- "enum": ["queued", "saving", "active", "killed", "deleted",
+ "enum": ["queued", "saving", "active", "killed",
+ "deleted", "uploading", "importing",
"pending_delete", "deactivated"],
"type": "string",
"description": "Status of the image"
diff --git a/glanceclient/v2/images.py b/glanceclient/v2/images.py
index 29abc30..fbb67a9 100644
--- a/glanceclient/v2/images.py
+++ b/glanceclient/v2/images.py
@@ -125,7 +125,7 @@ class Controller(object):
if limit:
limit -= 1
if limit <= 0:
- raise StopIteration
+ return
try:
next_url = body['next']
@@ -254,10 +254,16 @@ class Controller(object):
return body, resp
@utils.add_req_id_to_object()
- def image_import(self, image_id, method='glance-direct'):
+ def image_import(self, image_id, method='glance-direct', uri=None):
"""Import Image via method."""
url = '/v2/images/%s/import' % image_id
data = {'method': {'name': method}}
+ if uri:
+ if method == 'web-download':
+ data['method']['uri'] = uri
+ else:
+ raise exc.HTTPBadRequest('URI is only supported with method: '
+ '"web-download"')
resp, body = self.http_client.post(url, data=data)
return body, resp
diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py
index c9f1fe1..d837ba1 100644
--- a/glanceclient/v2/shell.py
+++ b/glanceclient/v2/shell.py
@@ -30,7 +30,7 @@ import os
MEMBER_STATUS_VALUES = image_members.MEMBER_STATUS_VALUES
IMAGE_SCHEMA = None
-DATA_FIELDS = ('location', 'copy_from', 'file')
+DATA_FIELDS = ('location', 'copy_from', 'file', 'uri')
def get_image_schema():
@@ -102,12 +102,34 @@ def do_image_create(gc, args):
'passed to the client via stdin.'))
@utils.arg('--progress', action='store_true', default=False,
help=_('Show upload progress bar.'))
-@utils.arg('--import-method', metavar='<METHOD>', default='glance-direct',
+@utils.arg('--import-method', metavar='<METHOD>',
+ default=utils.env('OS_IMAGE_IMPORT_METHOD', default=None),
help=_('Import method used for Image Import workflow. '
- 'Valid values can be retrieved with import-info command.'))
+ 'Valid values can be retrieved with import-info command. '
+ 'Defaults to env[OS_IMAGE_IMPORT_METHOD] or if that is '
+ 'undefined uses \'glance-direct\' if data is provided using '
+ '--file or stdin. Otherwise, simply creates an image '
+ 'record if no import-method and no data is supplied'))
+@utils.arg('--uri', metavar='<IMAGE_URL>', default=None,
+ help=_('URI to download the external image.'))
@utils.on_data_require_fields(DATA_FIELDS)
def do_image_create_via_import(gc, args):
- """EXPERIMENTAL: Create a new image via image import."""
+ """EXPERIMENTAL: Create a new image via image import.
+
+ Use the interoperable image import workflow to create an image. This
+ command is designed to be backward compatible with the current image-create
+ command, so its behavior is as follows:
+
+ * If an import-method is specified (either on the command line or through
+ the OS_IMAGE_IMPORT_METHOD environment variable, then you must provide a
+ data source appropriate to that method (for example, --file for
+ glance-direct, or --uri for web-download).
+ * If no import-method is specified AND you provide either a --file or
+ data to stdin, the command will assume you are using the 'glance-direct'
+ import-method and will act accordingly.
+ * If no import-method is specified and no data is supplied via --file or
+ stdin, the command will simply create an image record in 'queued' status.
+ """
schema = gc.schemas.get("image")
_args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()]
fields = dict(filter(lambda x: x[1] is not None and
@@ -121,23 +143,60 @@ def do_image_create_via_import(gc, args):
fields[key] = value
file_name = fields.pop('file', None)
- if file_name is not None and os.access(file_name, os.R_OK) is False:
- utils.exit("File %s does not exist or user does not have read "
- "privileges to it" % file_name)
+ using_stdin = not sys.stdin.isatty()
+
+ # special processing for backward compatibility with image-create
+ if args.import_method is None and (file_name or using_stdin):
+ args.import_method = 'glance-direct'
+
+ # determine whether the requested import method is valid
import_methods = gc.images.get_import_info().get('import-methods')
- if file_name and (not import_methods or
- 'glance-direct' not in import_methods.get('value')):
- utils.exit("No suitable import method available for direct upload, "
- "please use image-create instead.")
+ if args.import_method and args.import_method not in import_methods.get(
+ 'value'):
+ utils.exit("Import method '%s' is not valid for this cloud. "
+ "Valid values can be retrieved with import-info command." %
+ args.import_method)
+
+ # make sure we have all and only correct inputs for the requested method
+ if args.import_method is None:
+ if args.uri:
+ utils.exit("You cannot use --uri without specifying an import "
+ "method.")
+ if args.import_method == 'glance-direct':
+ if args.uri:
+ utils.exit("You cannot specify a --uri with the glance-direct "
+ "import method.")
+ if file_name is not None and os.access(file_name, os.R_OK) is False:
+ utils.exit("File %s does not exist or user does not have read "
+ "privileges to it." % file_name)
+ if file_name is not None and using_stdin:
+ utils.exit("You cannot use both --file and stdin with the "
+ "glance-direct import method.")
+ if not file_name and not using_stdin:
+ utils.exit("You must specify a --file or provide data via stdin "
+ "for the glance-direct import method.")
+ if args.import_method == 'web-download':
+ if not args.uri:
+ utils.exit("URI is required for web-download import method. "
+ "Please use '--uri <uri>'.")
+ if file_name:
+ utils.exit("You cannot specify a --file with the web-download "
+ "import method.")
+ if using_stdin:
+ utils.exit("You cannot pass data via stdin with the web-download "
+ "import method.")
+
+ # process
image = gc.images.create(**fields)
try:
- if utils.get_data_file(args) is not None:
- args.id = image['id']
- args.size = None
- do_image_stage(gc, args)
+ args.id = image['id']
+ if args.import_method:
+ if utils.get_data_file(args) is not None:
+ args.size = None
+ do_image_stage(gc, args)
args.from_create = True
do_image_import(gc, args)
- image = gc.images.get(args.id)
+ image = gc.images.get(args.id)
finally:
utils.print_image(image)
@@ -418,18 +477,59 @@ def do_image_stage(gc, args):
'Valid values can be retrieved with import-info command '
'and the default "glance-direct" is used with '
'"image-stage".'))
+@utils.arg('--uri', metavar='<IMAGE_URL>', default=None,
+ help=_('URI to download the external image.'))
@utils.arg('id', metavar='<IMAGE_ID>',
help=_('ID of image to import.'))
def do_image_import(gc, args):
"""Initiate the image import taskflow."""
+
+ if getattr(args, 'from_create', False):
+ # this command is being called "internally" so we can skip
+ # validation -- just do the import and get out of here
+ gc.images.image_import(args.id, args.import_method, args.uri)
+ return
+
+ # do input validation
try:
- gc.images.image_import(args.id, args.import_method)
+ import_methods = gc.images.get_import_info().get('import-methods')
except exc.HTTPNotFound:
utils.exit('Target Glance does not support Image Import workflow')
- else:
- if not getattr(args, 'from_create', False):
- image = gc.images.get(args.id)
- utils.print_image(image)
+
+ if args.import_method not in import_methods.get('value'):
+ utils.exit("Import method '%s' is not valid for this cloud. "
+ "Valid values can be retrieved with import-info command." %
+ args.import_method)
+
+ if args.import_method == 'web-download' and not args.uri:
+ utils.exit("Provide URI for web-download import method.")
+ if args.uri and args.import_method != 'web-download':
+ utils.exit("Import method should be 'web-download' if URI is "
+ "provided.")
+
+ # check image properties
+ image = gc.images.get(args.id)
+ container_format = image.get('container_format')
+ disk_format = image.get('disk_format')
+ if not (container_format and disk_format):
+ utils.exit("The 'container_format' and 'disk_format' properties "
+ "must be set on an image before it can be imported.")
+
+ image_status = image.get('status')
+ if args.import_method == 'glance-direct':
+ if image_status != 'uploading':
+ utils.exit("The 'glance-direct' import method can only be applied "
+ "to an image in status 'uploading'")
+ if args.import_method == 'web-download':
+ if image_status != 'queued':
+ utils.exit("The 'web-download' import method can only be applied "
+ "to an image in status 'queued'")
+
+ # finally, do the import
+ gc.images.image_import(args.id, args.import_method, args.uri)
+
+ image = gc.images.get(args.id)
+ utils.print_image(image)
@utils.arg('id', metavar='<IMAGE_ID>', nargs='+',
diff --git a/lower-constraints.txt b/lower-constraints.txt
new file mode 100644
index 0000000..a02f7fb
--- /dev/null
+++ b/lower-constraints.txt
@@ -0,0 +1,81 @@
+alabaster==0.7.10
+appdirs==1.3.0
+asn1crypto==0.23.0
+Babel==2.3.4
+cffi==1.7.0
+cliff==2.8.0
+cmd2==0.8.0
+coverage==4.0
+cryptography==2.1
+debtcollector==1.2.0
+docutils==0.11
+dulwich==0.15.0
+extras==1.0.0
+fasteners==0.7.0
+fixtures==3.0.0
+flake8==2.5.5
+future==0.16.0
+hacking==0.12.0
+idna==2.6
+imagesize==0.7.1
+iso8601==0.1.11
+Jinja2==2.10
+jsonpatch==1.16
+jsonpointer==1.13
+jsonschema==2.6.0
+keystoneauth1==3.6.2
+linecache2==1.0.0
+MarkupSafe==1.0
+mccabe==0.2.1
+mock==2.0.0
+monotonic==0.6
+msgpack-python==0.4.0
+netaddr==0.7.18
+netifaces==0.10.4
+openstackdocstheme==1.18.1
+ordereddict==1.1
+os-client-config==1.28.0
+os-testr==1.0.0
+oslo.concurrency==3.25.0
+oslo.config==5.2.0
+oslo.context==2.19.2
+oslo.i18n==3.15.3
+oslo.log==3.36.0
+oslo.serialization==2.18.0
+oslo.utils==3.33.0
+paramiko==2.0.0
+pbr==2.0.0
+pep8==1.5.7
+prettytable==0.7.1
+pyasn1==0.1.8
+pycparser==2.18
+pyflakes==0.8.1
+Pygments==2.2.0
+pyinotify==0.9.6
+pyOpenSSL==17.1.0
+pyparsing==2.1.0
+pyperclip==1.5.27
+python-dateutil==2.5.3
+python-mimeparse==1.6.0
+python-subunit==1.0.0
+pytz==2013.6
+PyYAML==3.12
+reno==2.5.0
+requests-mock==1.2.0
+requests==2.14.2
+requestsexceptions==1.2.0
+rfc3986==0.3.1
+six==1.10.0
+snowballstemmer==1.2.1
+Sphinx==1.6.2
+sphinxcontrib-websupport==1.0.1
+stestr==2.0.0
+stevedore==1.20.0
+tempest==17.1.0
+testscenarios==0.4
+testtools==2.2.0
+traceback2==1.4.0
+unittest2==1.1.0
+urllib3==1.21.1
+warlock==1.2.0
+wrapt==1.7.0
diff --git a/releasenotes/notes/http-headers-per-rfc-8187-aafa3199f863be81.yaml b/releasenotes/notes/http-headers-per-rfc-8187-aafa3199f863be81.yaml
new file mode 100644
index 0000000..ab21b3c
--- /dev/null
+++ b/releasenotes/notes/http-headers-per-rfc-8187-aafa3199f863be81.yaml
@@ -0,0 +1,14 @@
+---
+fixes:
+ - |
+ Bug 1766235_: Handle HTTP headers per RFC 8187
+
+ Previously the glanceclient encoded HTTP headers as UTF-8
+ bytes. According to `RFC 8187`_, however, headers should be
+ encoded as 7-bit ASCII. The glanceclient now sends all headers
+ as 7-bit ASCII. It handles unicode strings by percent-encoding_
+ them before sending them in headers.
+
+ .. _1766235: https://code.launchpad.net/bugs/1766235
+ .. _RFC 8187: https://tools.ietf.org/html/rfc8187
+ .. _percent-encoding: https://tools.ietf.org/html/rfc3986#section-2.1
diff --git a/releasenotes/notes/rocky-2.11.0-ba936fd5e969198d.yaml b/releasenotes/notes/rocky-2.11.0-ba936fd5e969198d.yaml
new file mode 100644
index 0000000..67409f3
--- /dev/null
+++ b/releasenotes/notes/rocky-2.11.0-ba936fd5e969198d.yaml
@@ -0,0 +1,12 @@
+---
+issues:
+ - |
+ Help texts for some properties has possibly outdated links. Please refer
+ to the documentation of the deployment while we try to find a way how to
+ document these references in a way that they do not point user to false
+ information.
+fixes:
+ - |
+ * Bug 1762044_: Sync schema with glance-api service
+
+ .. _1762044: https://code.launchpad.net/bugs/1762044
diff --git a/requirements.txt b/requirements.txt
index 68abc02..25b670a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,11 +3,11 @@
# process, which may cause wedges in the gate later.
pbr!=2.1.0,>=2.0.0 # Apache-2.0
PrettyTable<0.8,>=0.7.1 # BSD
-keystoneauth1>=3.4.0 # Apache-2.0
+keystoneauth1>=3.6.2 # Apache-2.0
requests>=2.14.2 # Apache-2.0
warlock<2,>=1.2.0 # Apache-2.0
six>=1.10.0 # MIT
oslo.utils>=3.33.0 # Apache-2.0
oslo.i18n>=3.15.3 # Apache-2.0
wrapt>=1.7.0 # BSD License
-pyOpenSSL>=16.2.0 # Apache-2.0
+pyOpenSSL>=17.1.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index 8cf087e..1cc748d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -33,21 +33,5 @@ setup-hooks =
console_scripts =
glance = glanceclient.shell:main
-[build_sphinx]
-builders = html,man
-all-files = 1
-warning-is-error = 1
-source-dir = doc/source
-build-dir = doc/build
-
-[upload_sphinx]
-upload-dir = doc/build/html
-
[wheel]
universal = 1
-
-[pbr]
-autodoc_index_modules = True
-autodoc_exclude_modules =
- glanceclient.tests.*
-api_doc_dir = reference/api
diff --git a/test-requirements.txt b/test-requirements.txt
index 8bec3aa..0424393 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -5,14 +5,10 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
mock>=2.0.0 # BSD
-ordereddict>=1.1 # MIT
os-client-config>=1.28.0 # Apache-2.0
-openstackdocstheme>=1.18.1 # Apache-2.0
-reno>=2.5.0 # Apache-2.0
-sphinx!=1.6.6,>=1.6.2 # BSD
-testrepository>=0.0.18 # Apache-2.0/BSD
+stestr>=2.0.0 # Apache-2.0
testtools>=2.2.0 # MIT
testscenarios>=0.4 # Apache-2.0/BSD
fixtures>=3.0.0 # Apache-2.0/BSD
-requests-mock>=1.1.0 # Apache-2.0
+requests-mock>=1.2.0 # Apache-2.0
tempest>=17.1.0 # Apache-2.0
diff --git a/tools/fix_ca_bundle.sh b/tools/fix_ca_bundle.sh
index 8e3dba2..cd35d8d 100755
--- a/tools/fix_ca_bundle.sh
+++ b/tools/fix_ca_bundle.sh
@@ -6,10 +6,12 @@
# assumptions:
# - devstack is running
# - the devstack tls-proxy service is running
+# - the environment var OS_TESTENV_NAME is set in tox.ini (defaults
+# to 'functional'
#
# This code based on a function in devstack lib/tls
function set_ca_bundle {
- local python_cmd='.tox/functional/bin/python'
+ local python_cmd=".tox/${OS_TESTENV_NAME:-functional}/bin/python"
local capath=$($python_cmd -c $'try:\n from requests import certs\n print (certs.where())\nexcept ImportError: pass')
# of course, each distro keeps the CA store in a different location
local fedora_CA='/etc/pki/tls/certs/ca-bundle.crt'
diff --git a/tox.ini b/tox.ini
index e6915cc..85f5f3a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,18 +9,19 @@ install_command = pip install {opts} {packages}
setenv = VIRTUAL_ENV={envdir}
OS_STDOUT_NOCAPTURE=False
OS_STDERR_NOCAPTURE=False
- PYTHONHASHSEED=0
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
-commands = python setup.py testr --testr-args='{posargs}'
+commands = stestr run --slowest {posargs}
[testenv:pep8]
+basepython = python3
commands = flake8
[testenv:venv]
+basepython = python3
commands = {posargs}
[pbr]
@@ -30,23 +31,47 @@ warnerror = True
# See glanceclient/tests/functional/README.rst
# for information on running the functional tests.
setenv =
- OS_TEST_PATH = ./glanceclient/tests/functional
+ OS_TEST_PATH = ./glanceclient/tests/functional/v2
+ OS_TESTENV_NAME = {envname}
whitelist_externals =
bash
commands =
bash tools/fix_ca_bundle.sh
- python setup.py testr --testr-args='{posargs}'
+ stestr run --slowest {posargs}
+
+[testenv:functional-v1]
+# TODO(rosmaita): remove this testenv at the beginning
+# of the 'S' cycle
+setenv =
+ OS_TEST_PATH = ./glanceclient/tests/functional/v1
+ OS_TESTENV_NAME = {envname}
+whitelist_externals =
+ bash
+commands =
+ bash tools/fix_ca_bundle.sh
+ stestr run --slowest {posargs}
[testenv:cover]
-commands = python setup.py testr --coverage --testr-args='{posargs}'
- coverage report
+basepython = python3
+setenv =
+ PYTHON=coverage run --source glanceclient --parallel-mode
+commands =
+ stestr run {posargs}
+ coverage combine
+ coverage html -d cover
+ coverage xml -o cover/coverage.xml
[testenv:docs]
-commands=
- python setup.py build_sphinx
+basepython = python3
+deps = -r{toxinidir}/doc/requirements.txt
+commands =
+ sphinx-build -W -b html doc/source doc/build/html
[testenv:releasenotes]
-commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
+basepython = python3
+deps = -r{toxinidir}/doc/requirements.txt
+commands =
+ sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[flake8]
ignore = F403,F812,F821
@@ -55,3 +80,10 @@ exclude = .venv*,.tox,dist,*egg,build,.git,doc,*lib/python*,.update-venv
[hacking]
import_exceptions = six.moves,glanceclient._i18n
+
+[testenv:lower-constraints]
+basepython = python3
+deps =
+ -c{toxinidir}/lower-constraints.txt
+ -r{toxinidir}/test-requirements.txt
+ -r{toxinidir}/requirements.txt