diff options
| author | Thomas Goirand <zigo@debian.org> | 2016-01-14 05:30:14 +0000 |
|---|---|---|
| committer | git-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com> | 2016-01-14 10:10:52 +0000 |
| commit | e0d4c1dc3577ed26f13556ccead5c4d478c7b45f (patch) | |
| tree | 55f2c7a117edc5ab3e2ea0c3b4a60a2aee0e0722 | |
| parent | bf2067db8c46b8842569fa37ba675f3e1d4246b8 (diff) | |
2.0.0-1 (patches unapplied)import/2.0.0-1
Imported using git-ubuntu import.
Notes
Notes:
* New upstream release.
* Fixed (build-)depends for this release.
143 files changed, 5318 insertions, 1199 deletions
@@ -20,3 +20,5 @@ doc/source/api/ .project .pydevproject cover +# Files created by releasenotes build +releasenotes/build @@ -174,29 +174,3 @@ incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. @@ -2,6 +2,14 @@ OpenStackClient =============== +.. image:: https://img.shields.io/pypi/v/python-openstackclient.svg + :target: https://pypi.python.org/pypi/python-openstackclient/ + :alt: Latest Version + +.. image:: https://img.shields.io/pypi/dm/python-openstackclient.svg + :target: https://pypi.python.org/pypi/python-openstackclient/ + :alt: Downloads + OpenStackClient (aka OSC) is a command-line client for OpenStack that brings the command set for Compute, Identity, Image, Object Store and Volume APIs together in a single shell with a uniform command structure. diff --git a/debian/changelog b/debian/changelog index 5119fa4..0947526 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +python-openstackclient (2.0.0-1) experimental; urgency=medium + + * New upstream release. + * Fixed (build-)depends for this release. + + -- Thomas Goirand <zigo@debian.org> Thu, 14 Jan 2016 05:30:14 +0000 + python-openstackclient (1.7.1-1) unstable; urgency=medium [ Corey Bryant ] diff --git a/debian/control b/debian/control index 7ae2058..15f665a 100644 --- a/debian/control +++ b/debian/control @@ -11,35 +11,44 @@ Build-Depends: debhelper (>= 9), python-pbr (>= 1.8), python-setuptools, python-sphinx, -Build-Depends-Indep: cliff-tablib, - python-babel, +Build-Depends-Indep: python-babel, + python-barbicanclient (>= 3.3.0), python-cinderclient (>= 1:1.3.1), - python-cliff (>= 1.14.0), + python-cliff (>= 1.15.0), + python-congressclient (>= 1.0.0), python-coverage, python-crypto, + python-designateclient (>= 1.5.0), python-fixtures (>= 1.3.1), python-glanceclient (>= 1:0.18.0), python-hacking (>= 0.10.0), + python-heatclient (>= 0.6.0), + python-ironicclient (>= 0.8.0), python-keyring, + python-keystoneauth1 (>= 1.0.0), python-keystoneclient (>= 1:1.6.0), + python-mistralclient (>= 1.0.0), python-mock (>= 1.3), python-neutronclient (>= 1:2.6.0), - python-novaclient (>= 2:2.28.1), + python-novaclient (>= 2:2.30.0), python-os-client-config (>= 1.4.0), - python-os-testr, - python-oslo.config (>= 1:2.3.0), + python-os-testr (>= 0.4.2), + python-oslo.config (>= 1:3.2.0), python-oslo.i18n (>= 1.5.0), python-oslo.serialization (>= 1.2.0), - python-oslo.utils (>= 2.0.0), + python-oslo.utils (>= 3.3.0), python-oslosphinx (>= 2.5.0), python-oslotest (>= 1.10.0), - python-requests (>= 2.5.2), + python-requests (>= 2.8.1), python-requests-mock (>= 0.6.0), + python-saharaclient (>= 0.10.0), python-six (>= 1.9.0), python-stevedore (>= 1.5.0), - python-tempest-lib (>= 0.8.0), + python-tempest-lib (>= 0.11.0), python-testtools (>= 1.4.0), + python-tuskarclient (>= 0.1.17), python-webob, + python-zaqarclient (>= 0.3.0), subunit, testrepository, Standards-Version: 3.9.6 @@ -49,23 +58,23 @@ Homepage: http://wiki.openstack.org/OpenStackClient Package: python-openstackclient Architecture: all -Depends: cliff-tablib, - python-babel, +Depends: python-babel, python-cinderclient (>= 1:1.3.1), - python-cliff (>= 1.14.0), + python-cliff (>= 1.15.0), python-crypto, python-glanceclient (>= 1:0.18.0), python-keyring, + python-keystoneauth1 (>= 1.0.0), python-keystoneclient (>= 1:1.6.0), python-neutronclient (>= 1:2.6.0), - python-novaclient (>= 2:2.28.1), + python-novaclient (>= 2:2.30.0), python-os-client-config (>= 1.4.0), - python-oslo.config (>= 1:2.3.0), + python-oslo.config (>= 1:3.2.0), python-oslo.i18n (>= 1.5.0), python-oslo.serialization (>= 1.2.0), - python-oslo.utils (>= 2.0.0), + python-oslo.utils (>= 3.3.0), python-pbr (>= 1.8), - python-requests (>= 2.5.2), + python-requests (>= 2.8.1), python-six (>= 1.9.0), python-stevedore (>= 1.5.0), ${misc:Depends}, diff --git a/debian/gbp.conf b/debian/gbp.conf index 0ae05ad..7436424 100644 --- a/debian/gbp.conf +++ b/debian/gbp.conf @@ -1,6 +1,6 @@ [DEFAULT] upstream-branch = master -debian-branch = debian/liberty +debian-branch = debian/mitaka upstream-tag = %(version)s compression = xz diff --git a/debian/rules b/debian/rules index 1f1e016..a67ddb9 100755 --- a/debian/rules +++ b/debian/rules @@ -37,3 +37,9 @@ ifeq (,$(findstring nodocs, $(DEB_BUILD_OPTIONS))) PYTHONPATH=. sphinx-build -b html doc/source $(CURDIR)/debian/python-openstackclient/usr/share/doc/python-openstackclient/html dh_sphinxdoc -O--buildsystem=python2 endif + +override_dh_installman: +ifeq (,$(findstring nodocs, $(DEB_BUILD_OPTIONS))) + sphinx-build -b man doc/source doc/build/man + dh_installman +endif diff --git a/doc/source/authentication.rst b/doc/source/authentication.rst index bf23b66..a3986ee 100644 --- a/doc/source/authentication.rst +++ b/doc/source/authentication.rst @@ -83,7 +83,7 @@ by the ``ClientManager`` object. * Load the selected plugin class. * When an operation that requires authentication is attempted ``ClientManager`` - makes the actual inital request to the Identity service. + makes the actual initial request to the Identity service. * if ``--os-auth-url`` is not supplied for any of the types except Token/Endpoint, exit with an error. @@ -132,7 +132,7 @@ If using a domain as authorization scope, set either it's name or ID. Note that if the user and project share the same domain, then simply setting ``os-default-domain`` or ``OS_DEFAULT_DOMAIN`` is sufficient. -Thus, a minimal set of of environment variables would be: +Thus, a minimal set of environment variables would be: .. code-block:: bash diff --git a/doc/source/backwards-incompatible.rst b/doc/source/backwards-incompatible.rst index e89cc3a..94873c1 100644 --- a/doc/source/backwards-incompatible.rst +++ b/doc/source/backwards-incompatible.rst @@ -90,6 +90,30 @@ List of Backwards Incompatible Changes * Bug: https://bugs.launchpad.net/python-openstackclient/+bug/1453229 * Commit: https://review.openstack.org/#/c/181514/ +7. `image set` commands will no longer return the modified resource + + Previously, modifying an image would result in the new image being displayed + to the user. To keep things consistent with other `set` commands, we will + no longer be showing the modified resource. + + * In favor of: Use `set` then `show` + * As of: NA + * Removed in: NA + * Bug: NA + * Commit: NA + +8. `region` commands no longer support `url` + + The Keystone team removed support for thr `url` attribute from the client + and server side. Changes to the `create`, `set` and `list` commands for + regions have been affected. + + * In favor of: NA + * As of 1.9.0 + * Removed in: NA + * Bug: https://launchpad.net/bugs/1506841 + * Commit: https://review.openstack.org/#/c/236736/ + For Developers ============== diff --git a/doc/source/command-objects/compute-agent.rst b/doc/source/command-objects/compute-agent.rst new file mode 100644 index 0000000..395ee4f --- /dev/null +++ b/doc/source/command-objects/compute-agent.rst @@ -0,0 +1,100 @@ +============= +compute agent +============= + +Compute v2 + +compute agent create +-------------------- + +Create compute agent + +.. program:: compute agent create +.. code:: bash + + os compute agent create + <os> <architecture> <version> <url> <md5hash> + <hypervisor> + +.. _compute_agent-create: +.. describe:: <os> + + Type of OS + +.. describe:: <architecture> + + Type of architecture + +.. describe:: <version> + + Version + +.. describe:: <url> + + URL + +.. describe:: <md5hash> + + MD5 hash + +.. describe:: <hypervisor> + + Type of hypervisor + +compute agent delete +-------------------- + +Delete compute agent command + +.. program:: compute agent delete +.. code:: bash + + os compute agent delete <id> + +.. _compute_agent-delete: +.. describe:: <id> + + ID of agent to delete + +compute agent list +------------------ + +List compute agent command + +.. program:: compute agent list +.. code:: bash + + os compute agent list [--hypervisor <hypervisor>] + +.. _compute_agent-list: +.. describe:: --hypervisor <hypervisor> + + Optional type of hypervisor + +compute agent set +----------------- + +Set compute agent command + +.. program:: agent set +.. code:: bash + + os compute agent set + <id> <version> <url> <md5hash> + +.. _compute_agent-set: +.. describe:: <id> + + ID of the agent + +.. describe:: <version> + + Version of the agent + +.. describe:: <url> + + URL + +.. describe:: <md5hash> + + MD5 hash diff --git a/doc/source/command-objects/compute-service.rst b/doc/source/command-objects/compute-service.rst new file mode 100644 index 0000000..95a77bd --- /dev/null +++ b/doc/source/command-objects/compute-service.rst @@ -0,0 +1,73 @@ +=============== +compute service +=============== + +Compute v2 + +compute service delete +---------------------- + +Delete service command + +.. program:: compute service delete +.. code:: bash + + os compute service delete + <service> + +.. _compute-service-delete: +.. describe:: <service> + + Compute service to delete (ID only) + +compute service list +-------------------- + +List service command + +.. program:: compute service list +.. code:: bash + + os compute service list + [--host <host>] + [--service <service>] + +.. _compute-service-list: +.. describe:: --host <host> + + Name of host + +.. describe:: --service <service> + + Name of service + + +compute service set +------------------- + +Set service command + +.. program:: compute service set +.. code:: bash + + os compute service list + [--enable | --disable] + <host> <service> + +.. _compute-service-set: +.. describe:: --enable + + Enable service + +.. describe:: --disable + + Disable service + +.. describe:: <host> + + Name of host + +.. describe:: <service> + + Name of service + diff --git a/doc/source/command-objects/container.rst b/doc/source/command-objects/container.rst index 3372f4d..e6517e9 100644 --- a/doc/source/command-objects/container.rst +++ b/doc/source/command-objects/container.rst @@ -2,7 +2,7 @@ container ========= -Object Store v1 +Object Storage v1 container create ---------------- @@ -89,6 +89,26 @@ Save container contents locally Container to save +container set +------------- + +Set container properties + +.. program:: container set +.. code:: bash + + os container set + [--property <key=value> [...] ] + [<container>] + +.. option:: --property <key=value> + + Set a property on this container (repeat option to set multiple properties) + +.. describe:: <container> + + Container to modify + container show -------------- @@ -103,3 +123,23 @@ Display container details .. describe:: <container> Container to display + +container unset +--------------- + +Unset container properties + +.. program:: container unset +.. code:: bash + + os container unset + [--property <key>] + [<container>] + +.. option:: --property <key> + + Property to remove from container (repeat option to remove multiple properties) + +.. describe:: <container> + + Container to modify diff --git a/doc/source/command-objects/flavor.rst b/doc/source/command-objects/flavor.rst index 5254b12..2389f7d 100644 --- a/doc/source/command-objects/flavor.rst +++ b/doc/source/command-objects/flavor.rst @@ -91,6 +91,8 @@ List flavors os flavor list [--public | --private | --all] [--long] + [--marker <marker>] + [--limit <limit>] .. option:: --public @@ -108,6 +110,14 @@ List flavors List additional fields in output +.. option:: --marker <marker> + + The last flavor ID of the previous page + +.. option:: --limit <limit> + + Maximum number of flavors to display + flavor show ----------- diff --git a/doc/source/command-objects/identity-provider.rst b/doc/source/command-objects/identity-provider.rst index 90f0b49..ac46273 100644 --- a/doc/source/command-objects/identity-provider.rst +++ b/doc/source/command-objects/identity-provider.rst @@ -81,6 +81,7 @@ Set identity provider properties os identity provider set [--remote-id <remote-id> [...] | --remote-id-file <file-name>] + [--description <description>] [--enable | --disable] <identity-provider> @@ -94,6 +95,10 @@ Set identity provider properties Name of a file that contains many remote IDs to associate with the identity provider, one per line +.. option:: --description + + Set identity provider description + .. option:: --enable Enable the identity provider diff --git a/doc/source/command-objects/image.rst b/doc/source/command-objects/image.rst index d2698b0..d4b9916 100644 --- a/doc/source/command-objects/image.rst +++ b/doc/source/command-objects/image.rst @@ -57,8 +57,6 @@ Create/upload an image Image owner project name or ID - *Image version 1 only.* - .. option:: --size <size> Image size, in bytes (only used with --location and --copy-from) @@ -238,6 +236,13 @@ Set image properties [--checksum <checksum>] [--stdin] [--property <key=value> [...] ] + [--tag <tag> [...] ] + [--architecture <architecture>] + [--instance-id <instance-id>] + [--kernel-id <kernel-id>] + [--os-distro <os-distro>] + [--os-version <os-version>] + [--ramdisk-id <ramdisk-id>] <image> .. option:: --name <name> @@ -258,14 +263,11 @@ Set image properties .. option:: --container-format <container-format> - Container format of image. - Acceptable formats: ['ami', 'ari', 'aki', 'bare', 'ovf'] + Image container format (default: bare) .. option:: --disk-format <disk-format> - Disk format of image. - Acceptable formats: ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', - 'vdi', 'iso'] + Image disk format (default: raw) .. option:: --size <size> @@ -339,45 +341,49 @@ Set image properties .. option:: --property <key=value> - Set a property on this image (repeat for multiple values) + Set a property on this image (repeat option to set multiple properties) - *Image version 1 only.* + .. versionadded:: 2 + +.. option:: --tag <tag> + + Set a tag on this image (repeat for multiple values) + + .. versionadded:: 2 .. option:: --architecture <architecture> - Operating system Architecture + Operating system architecture .. versionadded:: 2 -.. option:: --ramdisk-id <ramdisk-id> +.. option:: --instance-id <instance-id> - ID of image stored in Glance that should be used as - the ramdisk when booting an AMI-style image + ID of server instance used to create this image .. versionadded:: 2 -.. option:: --os-distro <os-distro> +.. option:: --kernel-id <kernel-id> - Common name of operating system distribution + ID of kernel image used to boot this disk image .. versionadded:: 2 -.. option:: --os-version <os-version> +.. option:: --os-distro <os-distro> - Operating system version as specified by the distributor + Operating system distribution name .. versionadded:: 2 -.. option:: --kernel-id <kernel-id> +.. option:: --os-version <os-version> - ID of image in Glance that should be used as the - kernel when booting an AMI-style image + Operating system distribution version .. versionadded:: 2 -.. option:: --instance-uuid <instance_uuid> +.. option:: --ramdisk-id <ramdisk-id> - ID of instance used to create this image + ID of ramdisk image used to boot this disk image .. versionadded:: 2 diff --git a/doc/source/command-objects/ip-fixed.rst b/doc/source/command-objects/ip-fixed.rst new file mode 100644 index 0000000..3a55b99 --- /dev/null +++ b/doc/source/command-objects/ip-fixed.rst @@ -0,0 +1,45 @@ +======== +ip fixed +======== + +Compute v2 + +ip fixed add +------------ + +Add fixed IP address to server + +.. program:: ip fixed add +.. code:: bash + + os ip fixed add + <network> + <server> + +.. describe:: <network> + + Network to fetch an IP address from (name or ID) + +.. describe:: <server> + + Server to receive the IP address (name or ID) + +ip fixed remove +--------------- + +Remove fixed IP address from server + +.. program:: ip fixed remove +.. code:: bash + + os ip fixed remove + <ip-address> + <server> + +.. describe:: <ip-address> + + IP address to remove from server (name only) + +.. describe:: <server> + + Server to remove the IP address from (name or ID) diff --git a/doc/source/command-objects/ip-floating-pool.rst b/doc/source/command-objects/ip-floating-pool.rst new file mode 100644 index 0000000..63a450e --- /dev/null +++ b/doc/source/command-objects/ip-floating-pool.rst @@ -0,0 +1,15 @@ +================ +ip floating pool +================ + +Compute v2 + +ip floating pool list +--------------------- + +List pools of floating IP addresses + +.. program:: ip floating pool list +.. code:: bash + + os ip floating pool list diff --git a/doc/source/command-objects/floatingip.rst b/doc/source/command-objects/ip-floating.rst index 1ae3041..6bfd7f4 100644 --- a/doc/source/command-objects/floatingip.rst +++ b/doc/source/command-objects/ip-floating.rst @@ -1,24 +1,24 @@ -========== -floatingip -========== +=========== +ip floating +=========== Compute v2 ip floating add --------------- -Add floating-ip to server +Add floating IP address to server .. program:: ip floating add .. code:: bash os ip floating add - <ip_address> + <ip-address> <server> -.. describe:: <ip_address> +.. describe:: <ip-address> - IP address to add to server + IP address to add to server (name only) .. describe:: <server> @@ -27,7 +27,7 @@ Add floating-ip to server ip floating create ------------------ -Create new floating-ip +Create new floating IP address .. program:: ip floating create .. code:: bash @@ -37,27 +37,27 @@ Create new floating-ip .. describe:: <pool> - Pool to fetch floating IP from + Pool to fetch IP address from (name or ID) ip floating delete ------------------ -Delete a floating-ip +Delete a floating IP address .. program:: ip floating delete .. code:: bash os ip floating delete - <ip_address> + <ip-address> -.. describe:: <ip_address> +.. describe:: <ip-address> - IP address to delete + IP address to delete (ID only) ip floating list ---------------- -List floating-ips +List floating IP addresses .. program:: ip floating list .. code:: bash @@ -67,18 +67,18 @@ List floating-ips ip floating remove ------------------ -Remove floating-ip from server +Remove floating IP address from server .. program:: ip floating remove .. code:: bash os ip floating remove - <ip_address> + <ip-address> <server> -.. describe:: <ip_address> +.. describe:: <ip-address> - IP address to remove from server + IP address to remove from server (name only) .. describe:: <server> diff --git a/doc/source/command-objects/object-store-account.rst b/doc/source/command-objects/object-store-account.rst new file mode 100644 index 0000000..ba37078 --- /dev/null +++ b/doc/source/command-objects/object-store-account.rst @@ -0,0 +1,45 @@ +==================== +object store account +==================== + +Object Storage v1 + +object store account set +------------------------ + +Set account properties + +.. program:: object store account set +.. code:: bash + + os object store account set + [--property <key=value> [...] ] + +.. option:: --property <key=value> + + Set a property on this account (repeat option to set multiple properties) + +object store account show +------------------------- + +Display account details + +.. program:: object store account show +.. code:: bash + + os object store account show + +object store account unset +-------------------------- + +Unset account properties + +.. program:: object store account unset +.. code:: bash + + os object store account unset + [--property <key>] + +.. option:: --property <key> + + Property to remove from account (repeat option to remove multiple properties) diff --git a/doc/source/command-objects/object.rst b/doc/source/command-objects/object.rst index 90bcfa1..5aaad8a 100644 --- a/doc/source/command-objects/object.rst +++ b/doc/source/command-objects/object.rst @@ -2,7 +2,7 @@ object ====== -Object Store v1 +Object Storage v1 object create ------------- @@ -119,6 +119,31 @@ Save object locally Object to save +object set +---------- + +Set object properties + +.. program:: object set +.. code:: bash + + os object set + [--property <key=value> [...] ] + <container> + [<object>] + +.. option:: --property <key=value> + + Set a property on this object (repeat option to set multiple properties) + +.. describe:: <container> + + Modify <object> from <container> + +.. describe:: <object> + + Object to modify + object show ----------- @@ -138,3 +163,28 @@ Display object details .. describe:: <object> Object to display + +object unset +------------ + +Unset object properties + +.. program:: object unset +.. code:: bash + + os object unset + [--property <key>] + <container> + [<object>] + +.. option:: --property <key> + + Property to remove from object (repeat option to remove multiple properties) + +.. describe:: <container> + + Modify <object> from <container> + +.. describe:: <object> + + Object to modify diff --git a/doc/source/command-objects/region.rst b/doc/source/command-objects/region.rst index cb4a059..1892fc2 100644 --- a/doc/source/command-objects/region.rst +++ b/doc/source/command-objects/region.rst @@ -15,7 +15,6 @@ Create new region os region create [--parent-region <region-id>] [--description <description>] - [--url <url>] <region-id> .. option:: --parent-region <region-id> @@ -26,10 +25,6 @@ Create new region New region description -.. option:: --url <url> - - New region URL - .. _region_create-region-id: .. describe:: <region-id> @@ -77,7 +72,6 @@ Set region properties os region set [--parent-region <region-id>] [--description <description>] - [--url <url>] <region-id> .. option:: --parent-region <region-id> @@ -88,10 +82,6 @@ Set region properties New region description -.. option:: --url <url> - - New region URL - .. _region_set-region-id: .. describe:: <region-id> diff --git a/doc/source/command-objects/security-group-rule.rst b/doc/source/command-objects/security-group-rule.rst new file mode 100644 index 0000000..8bd0d06 --- /dev/null +++ b/doc/source/command-objects/security-group-rule.rst @@ -0,0 +1,65 @@ +=================== +security group rule +=================== + +Compute v2 + +security group rule create +-------------------------- + +Create a new security group rule + +.. program:: security group rule create +.. code:: bash + + os security group rule create + [--proto <proto>] + [--src-ip <ip-address>] + [--dst-port <port-range>] + <group> + +.. option:: --proto <proto> + + IP protocol (icmp, tcp, udp; default: tcp) + +.. option:: --src-ip <ip-address> + + Source IP (may use CIDR notation; default: 0.0.0.0/0) + +.. option:: --dst-port <port-range> + + Destination port, may be a range: 137:139 (default: 0; only required for proto tcp and udp) + +.. describe:: <group> + + Create rule in this security group (name or ID) + +security group rule delete +-------------------------- + +Delete a security group rule + +.. program:: security group rule delete +.. code:: bash + + os security group rule delete + <rule> + +.. describe:: <rule> + + Security group rule to delete (ID only) + +security group rule list +------------------------ + +List security group rules + +.. program:: security group rule list +.. code:: bash + + os security group rule list + <group> + +.. describe:: <group> + + List all rules in this security group (name or ID) diff --git a/doc/source/command-objects/security-group.rst b/doc/source/command-objects/security-group.rst new file mode 100644 index 0000000..60de41d --- /dev/null +++ b/doc/source/command-objects/security-group.rst @@ -0,0 +1,95 @@ +============== +security group +============== + +Compute v2 + +security group create +--------------------- + +Create a new security group + +.. program:: security group create +.. code:: bash + + os security group create + [--description <description>] + <name> + +.. option:: --description <description> + + Security group description + +.. describe:: <name> + + New security group name + +security group delete +--------------------- + +Delete a security group + +.. program:: security group delete +.. code:: bash + + os security group delete + <group> + +.. describe:: <group> + + Security group to delete (name or ID) + +security group list +------------------- + +List security groups + +.. program:: security group list +.. code:: bash + + os security group list + [--all-projects] + +.. option:: --all-projects + + Display information from all projects (admin only) + +security group set +------------------ + +Set security group properties + +.. program:: security group set +.. code:: bash + + os security group set + [--name <new-name>] + [--description <description>] + <group> + +.. option:: --name <new-name> + + New security group name + +.. option:: --description <description> + + New security group description + +.. describe:: <group> + + Security group to modify (name or ID) + +security group show +------------------- + +Display security group details + +.. program:: security group show +.. code:: bash + + os security group show + <group> + +.. describe:: <group> + + Security group to display (name or ID) diff --git a/doc/source/command-objects/server.rst b/doc/source/command-objects/server.rst index 9ff4843..506a82d 100644 --- a/doc/source/command-objects/server.rst +++ b/doc/source/command-objects/server.rst @@ -165,7 +165,7 @@ Delete server(s) .. describe:: <server> - Server to delete (name or ID) + Server(s) to delete (name or ID) server list ----------- @@ -187,6 +187,8 @@ List servers [--all-projects] [--project <project> [--project-domain <project-domain>]] [--long] + [--marker <server>] + [--limit <limit>] .. option:: --reservation-id <reservation-id> @@ -250,20 +252,31 @@ List servers List additional fields in output +.. option:: --marker <server> + + The last server (name or ID) of the previous page. Display list of servers + after marker. Display all servers if not specified. + +.. option:: --limit <limit> + + Maximum number of servers to display. If limit equals -1, all servers will + be displayed. If limit is greater than 'osapi_max_limit' option of Nova + API, 'osapi_max_limit' will be used instead. + server lock ----------- -Lock a server. A non-admin user will not be able to execute actions +Lock server(s). A non-admin user will not be able to execute actions .. program:: server lock .. code:: bash os server lock - <server> + <server> [<server> ...] .. describe:: <server> - Server (name or ID) + Server(s) to lock (name or ID) server migrate -------------- @@ -311,17 +324,17 @@ Migrate server to different host server pause ------------ -Pause server +Pause server(s) .. program:: server pause .. code:: bash os server pause - <server> + <server> [<server> ...] .. describe:: <server> - Server (name or ID) + Server(s) to pause (name or ID) server reboot ------------- @@ -483,17 +496,17 @@ a revert to release the new server and restart the old one. server resume ------------- -Resume server +Resume server(s) .. program:: server resume .. code:: bash os server resume - <server> + <server> [<server> ...] .. describe:: <server> - Server (name or ID) + Server(s) to resume (name or ID) server set ---------- @@ -527,6 +540,21 @@ Set server properties Server (name or ID) +server shelve +------------- + +Shelve server(s) + +.. program:: server shelve +.. code:: bash + + os server shelve + <server> [<server> ...] + +.. describe:: <server> + + Server(s) to shelve (name or ID) + server show ----------- @@ -595,50 +623,80 @@ Ssh to server Server (name or ID) +server start +------------ + +Start server(s) + +.. program:: server start +.. code:: bash + + os server start + <server> [<server> ...] + +.. describe:: <server> + + Server(s) to start (name or ID) + +server stop +----------- + +Stop server(s) + +.. program:: server stop +.. code:: bash + + os server stop + <server> [<server> ...] + +.. describe:: <server> + + Server(s) to stop (name or ID) + server suspend -------------- -Suspend server +Suspend server(s) .. program:: server suspend .. code:: bash os server suspend - <server> + <server> [<server> ...] .. describe:: <server> - Server (name or ID) + Server(s) to suspend (name or ID) server unlock ------------- -Unlock server +Unlock server(s) .. program:: server unlock .. code:: bash os server unlock - <server> + <server> [<server> ...] .. describe:: <server> - Server (name or ID) + Server(s) to unlock (name or ID) server unpause -------------- -Unpause server +Unpause server(s) .. program:: server unpause .. code:: bash os server unpause - <server> + <server> [<server> ...] .. describe:: <server> - Server (name or ID) + Server(s) to unpause (name or ID) server unrescue --------------- @@ -675,3 +733,18 @@ Unset server properties .. describe:: <server> Server (name or ID) + +server unshelve +--------------- + +Unshelve server(s) + +.. program:: server unshelve +.. code:: bash + + os server unshelve + <server> [<server> ...] + +.. describe:: <server> + + Server(s) to unshelve (name or ID) diff --git a/doc/source/command-objects/snapshot.rst b/doc/source/command-objects/snapshot.rst index 7bfd1d9..e05673b 100644 --- a/doc/source/command-objects/snapshot.rst +++ b/doc/source/command-objects/snapshot.rst @@ -60,6 +60,11 @@ List snapshots .. code:: bash os snapshot list + [--all-projects] + +.. option:: --all-projects + + Include all projects (admin only) .. option:: --long diff --git a/doc/source/command-objects/volume.rst b/doc/source/command-objects/volume.rst index 556d164..cb52c56 100644 --- a/doc/source/command-objects/volume.rst +++ b/doc/source/command-objects/volume.rst @@ -102,18 +102,46 @@ List volumes .. code:: bash os volume list - [--status <status>] - [--name <name>] [--all-projects] + [--project <project> [--project-domain <project-domain>]] + [--user <user> [--user-domain <user-domain>]] + [--name <name>] + [--status <status>] [--long] -.. option:: --status <status> +.. option:: --project <project> - Filter results by status + Filter results by project (name or ID) (admin only) + + *Volume version 2 only* + +.. option:: --project-domain <project-domain> + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + + *Volume version 2 only* + +.. option:: --user <user> + + Filter results by user (name or ID) (admin only) + + *Volume version 2 only* + +.. option:: --user-domain <user-domain> + + Domain the user belongs to (name or ID). + This can be used in case collisions between user names exist. + + *Volume version 2 only* .. option:: --name <name> - Filter results by name + Filter results by volume name + +.. option:: --status <status> + + Filter results by status .. option:: --all-projects diff --git a/doc/source/command-options.rst b/doc/source/command-options.rst new file mode 100644 index 0000000..edafb15 --- /dev/null +++ b/doc/source/command-options.rst @@ -0,0 +1,135 @@ +=============== +Command Options +=============== + +OpenStackClient commands all have a set of zero or more options unique to +the command, however there are of course ways in which these options are +common and consistent across all of the commands that include them. + +These are the set of guidelines for OSC developers that help keep the +interface and commands consistent. + +In some cases (like the boolean variables below) we use the same pattern +for defining and using options in all situations. The alternative of only +using it when necessary leads to errors when copy-n-paste is used for a +new command without understanding why or why not that instance is correct. + +Boolean Options +=============== + +Boolean options for any command that sets a resource state, such as 'enabled' +or 'public', shall always have both positive and negative options defined. +The names of those options shall either be a naturally occuring pair of +words (in English) or a positive option and a negative option with `no-` +prepended (such as in the traditional GNU option usage) like `--share` and +`--no-share`. + +In order to handle those APIs that behave differently when a field is set to +`None` and when the field is not present in a passed argument list or dict, +each of the boolean options shall set its own variable to `True` as part of +a mutiually exclusive group, rather than the more common configuration of setting a +single destination variable `True` or `False` directly. This allows us to +detect the situation when neither option is present (both variables will be +`False`) and act accordingly for those APIs where this matters. + +This also requires that each of the boolean values be tested in the +`take_action()` method to correctly set (or not) the underlying API field +values. + +.. option:: --enable + + Enable <resource> (default) + +.. option:: --disable + + Disable <resource> + +Implementation +~~~~~~~~~~~~~~ + +The parser declaration should look like this:: + +.. code-block: python + + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--enable', + action='store_true', + help=_('Enable <resource> (default)'), + ) + enable_group.add_argument( + '--disable', + action='store_true', + help=_('Disable <resource>'), + ) + +An example handler in `take_action()`:: + + # This leaves 'enabled' undefined if neither option is present + if parsed_args.enable: + kwargs['enabled'] = True + if parsed_args.disable: + kwargs['enabled'] = False + +List Command Options +==================== + +Additional Fields +----------------- + +Most list commands only return a subset of the available fields by default. +Additional fields are available with the `--long` option. All list +commands should allow `--long` even if they return all fields by default. + +.. option:: --long + + List additional fields in output + +Implementation +~~~~~~~~~~~~~~ + +The parser declaration should look like this:: + +.. code-block: python + + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) + +Pagination +---------- + +There are many ways to do pagination, some OpenStack APIs support it, some don't. +OpenStackClient attempts to define a single common way to specify pagination on +the command line. + +.. option:: --marker <marker> + + Anchor for paging + +.. option:: --limit <limit> + + Limit number of <resource> returned (*integer*) + +Implementation +~~~~~~~~~~~~~~ + +The parser declaration should look like this:: + +.. code-block: python + + parser.add_argument( + "--marker", + metavar="<marker>", + help="Anchor for paging", + ) + + parser.add_argument( + "--limit", + metavar="<limit>", + type=int, + help="Limit the number of <resource> returned", + ) diff --git a/doc/source/command-wrappers.rst b/doc/source/command-wrappers.rst new file mode 100644 index 0000000..b14eccd --- /dev/null +++ b/doc/source/command-wrappers.rst @@ -0,0 +1,52 @@ +====================== +Command Class Wrappers +====================== + +When we want to deprecate a command, policy says we need to alert the user. +We do this with a message logged at WARNING level before any command output +is emitted. + +OpenStackClient command classes are derived from the ``cliff`` classes. +Cliff uses ``setuptools`` entry points for dispatching the parsed command +to the respective handler classes. This lends itself to modifying the +command execution at run-time. + +The obvious approach to adding the deprecation message would be to just add +the message to the command class ``take_action()`` method directly. But then +the various deprecations are scattered throughout the code base. If we +instead wrap the deprecated command class with a new class we can put all of +the wrappers into a separate, dedicated module. This also lets us leave the +original class unmodified and puts all of the deprecation bits in one place. + +This is an example of a minimal wrapper around a command class that logs a +deprecation message as a warning to the user then calls the original class. + +* Subclass the deprecated command. + +* Set class attribute ``deprecated`` to ``True`` to signal cliff to not + emit help text for this command. + +* Log the deprecation message at WARNING level and refer to the replacement + for the deprecated command in the log warning message. + +* Change the entry point class in ``setup.cfg`` to point to the new class. + +Example Deprecation Class +------------------------- + +.. code-block: python + + class ListFooOld(ListFoo): + """List resources""" + + # This notifies cliff to not display the help for this command + deprecated = True + + log = logging.getLogger('deprecated') + + def take_action(self, parsed_args): + self.log.warning( + "%s is deprecated, use 'foobar list'", + getattr(self, 'cmd_name', 'this command'), + ) + return super(ListFooOld, self).take_action(parsed_args) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index b52d945..6069895 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -77,10 +77,10 @@ referring to both Compute and Volume quotas. * ``console log``: (**Compute**) server console text dump * ``console url``: (**Compute**) server remote console URL * ``consumer``: (**Identity**) OAuth-based delegatee -* ``container``: (**Object Store**) a grouping of objects +* ``container``: (**Object Storage**) a grouping of objects * ``credentials``: (**Identity**) specific to identity providers * ``domain``: (**Identity**) a grouping of projects -* ``ec2 cedentials``: (**Identity**) AWS EC2-compatible credentials +* ``ec2 credentials``: (**Identity**) AWS EC2-compatible credentials * ``endpoint``: (**Identity**) the base URL used to contact a specific service * ``extension``: (**Compute**, **Identity**, **Volume**) OpenStack server API extensions * ``federation protocol``: (**Identity**) the underlying protocol used while federating identities @@ -91,14 +91,15 @@ referring to both Compute and Volume quotas. * ``hypervisor stats``: (**Compute**) hypervisor statistics over all compute nodes * ``identity provider``: (**Identity**) a source of users and authentication * ``image``: (**Image**) a disk image -* ``ip fixed``: Compute, Network - an internal IP address assigned to a server -* ``ip floating``: Compute, Network - a public IP address that can be mapped to a server +* ``ip fixed``: (**Compute**, **Network**) - an internal IP address assigned to a server +* ``ip floating``: (**Compute**, **Network**) - a public IP address that can be mapped to a server +* ``ip floating pool``: (**Compute**, **Network**) - a pool of public IP addresses * ``keypair``: (**Compute**) an SSH public key * ``limits``: (**Compute**, **Volume**) resource usage limits * ``mapping``: (**Identity**) a definition to translate identity provider attributes to Identity concepts * ``module``: internal - installed Python modules in the OSC process -* ``network``: Network - a virtual network for connecting servers and other resources -* ``object``: (**Object Store**) a single file in the Object Store +* ``network``: (**Network**) - a virtual network for connecting servers and other resources +* ``object``: (**Object Storage**) a single file in the Object Storage * ``policy``: (**Identity**) determines authorization * ``project``: (**Identity**) owns a group of resources * ``quota``: (**Compute**, **Volume**) resource usage restrictions @@ -106,8 +107,8 @@ referring to both Compute and Volume quotas. * ``request token``: (**Identity**) temporary OAuth-based token * ``role``: (**Identity**) a policy object used to determine authorization * ``role assignment``: (**Identity**) a relationship between roles, users or groups, and domains or projects -* ``security group``: Compute, Network - groups of network access rules -* ``security group rule``: Compute, Network - the individual rules that define protocol/IP/port access +* ``security group``: (**Compute**, **Network**) - groups of network access rules +* ``security group rule``: (**Compute**, **Network**) - the individual rules that define protocol/IP/port access * ``server``: (**Compute**) virtual machine instance * ``server image``: (**Compute**) saved server disk image * ``service``: (**Identity**) a cloud service @@ -120,6 +121,41 @@ referring to both Compute and Volume quotas. * ``volume``: (**Volume**) block volumes * ``volume type``: (**Volume**) deployment-specific types of volumes available + +Plugin Objects +-------------- + +The following are known `Objects` used by OpenStack :doc:`plugins`. These are +listed here to avoid name conflicts when creating new plugins. For a complete +list check out :doc:`plugin-commands`. + +* ``baremetal``: (**Baremetal (Ironic)**) +* ``congress datasource``: (**Policy (Congress)**) +* ``congress driver``: (**Policy (Congress)**) +* ``congress policy``: (**Policy (Congress)**) +* ``congress policy rule``: (**Policy (Congress)**) +* ``dataprocessing data source``: (**Data Processing (Sahara)**) +* ``dataprocessing image``: (**Data Processing (Sahara)**) +* ``dataprocessing image tags``: (**Data Processing (Sahara)**) +* ``dataprocessing plugin``: (**Data Processing (Sahara)**) +* ``management plan``: (**Management (Tuskar)**) +* ``management role``: (**Management (Tuskar)**) +* ``message-broker cluster``: (**Message Broker (Cue)**) +* ``message flavor``: (**Messaging (Zaqar)**) +* ``pool``: (**Messaging (Zaqar)**) +* ``ptr record``: (**DNS (Designate)**) +* ``queue``: (**Messaging (Zaqar)**) +* ``recordset``: (**DNS (Designate)**) +* ``secret``: (**Key Manager (Barbican)**) +* ``secret container``: (**Key Manager (Barbican)**) +* ``secret order``: (**Key Manager (Barbican)**) +* ``stack``: (**Orchestration (Heat)**) +* ``tld``: (**DNS (Designate)**) +* ``zone``: (**DNS (Designate)**) +* ``zone blacklist``: (**DNS (Designate)**) +* ``zone transfer``: (**DNS (Designate)**) + + Actions ------- @@ -132,28 +168,32 @@ Those actions with an opposite action are noted in parens if applicable. is built in the order of ``container add object <container> <object>``, the positional arguments appear in the same order * ``create`` (``delete``) - create a new occurrence of the specified object -* ``delete`` (``create``) - delete a specific occurrence of the specified object +* ``delete`` (``create``) - delete specific occurrences of the specified objects * ``issue`` (``revoke``) - issue a token * ``list`` - display summary information about multiple objects -* ``lock`` (``unlock``) +* ``lock`` (``unlock``) - lock one or more servers so that non-admin user won't be able to execute actions * ``migrate`` - move a server to a different host; ``--live`` performs a live migration if possible -* ``pause`` (``unpause``) - stop a server and leave it in memory +* ``pause`` (``unpause``) - stop one or more servers and leave them in memory * ``reboot`` - forcibly reboot a server * ``rebuild`` - rebuild a server using (most of) the same arguments as in the original create * ``remove`` (``add``) - remove an object from a group of objects * ``rescue`` (``unrescue``) - reboot a server in a special rescue mode allowing access to the original disks * ``resize`` - change a server's flavor -* ``resume`` (``suspend``) - return a suspended server to running state +* ``resume`` (``suspend``) - return one or more suspended servers to running state * ``revoke`` (``issue``) - revoke a token * ``save`` - download an object locally * ``set`` (``unset``) - set a property on the object, formerly called metadata +* ``shelve`` (``unshelve``) - shelve one or more servers * ``show`` - display detailed information about the specific object -* ``suspend`` (``resume``) - stop a server and save to disk freeing memory -* ``unlock`` (``lock``) -* ``unpause`` (``pause``) - return a paused server to running state +* ``start`` (``stop``) - start one or more servers +* ``stop`` (``start``) - stop one or more servers +* ``suspend`` (``resume``) - stop one or more servers and save to disk freeing memory +* ``unlock`` (``lock``) - unlock one or more servers +* ``unpause`` (``pause``) - return one or more paused servers to running state * ``unrescue`` (``rescue``) - return a server to normal boot mode * ``unset`` (``set``) - remove an attribute of the object +* ``unshelve`` (``shelve``) - unshelve one or more servers Implementation diff --git a/doc/source/conf.py b/doc/source/conf.py index f4434ec..792ba40 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -38,6 +38,7 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'oslosphinx', 'ext.apidoc', + 'stevedore.sphinxext', ] # Add any paths that contain templates here, relative to this directory. diff --git a/doc/source/developing.rst b/doc/source/developing.rst index c41fbf3..9f8e55c 100644 --- a/doc/source/developing.rst +++ b/doc/source/developing.rst @@ -98,6 +98,41 @@ create HTML docs, run the following: The resultant HTML will be the ``doc/build/html`` directory. +Release Notes +------------- + +The release notes for a patch should be included in the patch. See the +`Project Team Guide`_ for more information on using reno in OpenStack. + +.. _`Project Team Guide`: http://docs.openstack.org/project-team-guide/release-management.html#managing-release-notes + +If any of the following applies to the patch, a release note is required: + +* The deployer needs to take an action when upgrading +* The plugin interface changes +* A new feature is implemented +* A command or option is removed +* Current behavior is changed +* A security bug is fixed + +Reno is used to generate release notes. Please read the docs for details. In summary, use + +.. code-block:: bash + + $ tox -e venv -- reno new <bug-,bp-,whatever> + +Then edit the sample file that was created and push it with your change. + +To see the results: + +.. code-block:: bash + + $ git commit # Commit the change because reno scans git log. + + $ tox -e releasenotes + +Then look at the generated release notes files in releasenotes/build/html in your favorite browser. + Testing new code ---------------- diff --git a/doc/source/history.rst b/doc/source/history.rst new file mode 100644 index 0000000..69ed4fe --- /dev/null +++ b/doc/source/history.rst @@ -0,0 +1 @@ +.. include:: ../../ChangeLog diff --git a/doc/source/index.rst b/doc/source/index.rst index c90b8e5..4992997 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,7 +3,7 @@ OpenStackClient =============== OpenStackClient (aka OSC) is a command-line client for OpenStack that -brings the command set for Compute, Identity, Image, Object Store and Volume +brings the command set for Compute, Identity, Image, Object Storage and Volume APIs together in a single shell with a uniform command structure. User Documentation @@ -16,11 +16,11 @@ User Documentation commands configuration plugins + plugin-commands authentication interactive humaninterfaceguide backwards-incompatible - releases man/openstack Getting Started @@ -30,6 +30,15 @@ Getting Started * Read the source `on OpenStack's Git server`_ * Install OpenStackClient from `PyPi`_ or a `tarball`_ +Release Notes +------------- + +.. toctree:: + :maxdepth: 1 + + releases + history + Developer Documentation ----------------------- @@ -37,6 +46,8 @@ Developer Documentation :maxdepth: 1 developing + command-options + command-wrappers Project Goals ------------- diff --git a/doc/source/plugin-commands.rst b/doc/source/plugin-commands.rst new file mode 100644 index 0000000..893c4c4 --- /dev/null +++ b/doc/source/plugin-commands.rst @@ -0,0 +1,32 @@ +================= + Plugin Commands +================= + +.. list-plugins:: openstack.cli.extension + +.. list-plugins:: openstack.key_manager.v1 + :detailed: + +.. list-plugins:: openstack.baremetal.v1 + :detailed: + +.. list-plugins:: openstack.congressclient.v1 + :detailed: + +.. list-plugins:: openstack.workflow_engine.v2 + :detailed: + +.. list-plugins:: openstack.data_processing.v1 + :detailed: + +.. list-plugins:: openstack.dns.v1 + :detailed: + +.. list-plugins:: openstack.management.v1 + :detailed: + +.. list-plugins:: openstack.messaging.v1 + :detailed: + +.. list-plugins:: openstack.orchestration.v1 + :detailed: diff --git a/doc/source/plugins.rst b/doc/source/plugins.rst index 469742d..5f82cc6 100644 --- a/doc/source/plugins.rst +++ b/doc/source/plugins.rst @@ -24,20 +24,20 @@ plugin. ============================= ====================================== project notes ============================= ====================================== -python-barbicanclient patch in progress (https://review.openstack.org/#/c/198732/) +python-barbicanclient using OpenStackClient python-ceilometerclient using argparse python-congressclient using OpenStackClient python-cueclient using OpenStackClient -python-designateclient patch in progress (https://review.openstack.org/#/c/133676/) +python-designateclient using OpenStackClient python-heatclient patch in progress (https://review.openstack.org/#/c/195867/) -python-ironicclient patch in progress (https://review.openstack.org/#/c/171672/) +python-ironicclient Using OpenStackClient python-magnumclient using argparse python-manilaclient using argparse -python-mistralclient using cliff, but not OpenStackClient +python-mistralclient using cliff python-muranoclient using argparse -python-saharaclient using argparse +python-saharaclient using OpenStackClient python-troveclient using argparse -python-tuskarclient using OpenStackClient and their own shell +python-tuskarclient using OpenStackClient python-zaqarclient using OpenStackClient ============================= ====================================== diff --git a/doc/source/releases.rst b/doc/source/releases.rst index 582f584..853ef95 100644 --- a/doc/source/releases.rst +++ b/doc/source/releases.rst @@ -2,6 +2,95 @@ Release Notes ============= +As of release 2.0 the release notes can be found on the OpenStack `Release Notes site`_. + +.. _`Release Notes site`: http://docs.openstack.org/releasenotes/python-openstackclient + +1.9.0 (17 Nov 2015) +=================== + +* Several updates to `openstack server` + Blueprint `servers <https://blueprints.launchpad.net/python-openstackclient/+spec/cmd-with-multi-servers>`_ + + * `openstack server start` + * `openstack server stop` + * `openstack server pause` + * `openstack server unpause` + * `openstack server lock` + * `openstack server unlock` + +* Obfuscate passwords when used in debug mode + Bug `1501598 <https://bugs.launchpad.net/bugs/1501598>`_ + +* Clean up `identity provider show` + Bug `1478995 <https://bugs.launchpad.net/bugs/1478995>`_ + +* Add `description` to `identity provider set` + Bug `1515815 <https://bugs.launchpad.net/bugs/1515815>`_ + +* Add `compute service delete` + Bug `1503510 <https://bugs.launchpad.net/bugs/1503510>`_ + +* Log a warning when setting non-ascii object store properties + Bug `1503898 <https://bugs.launchpad.net/bugs/1503898>`_ + +* Add 'marker' and 'limit' to `openstack flavor list` + Bug `1505874 <https://bugs.launchpad.net/bugs/1505874>`_ + +* Remove `url` from `region create` and `region set` + Bug `1506841 <https://bugs.launchpad.net/bugs/1506841>`_ + +* `openstack image add project` fails with AttributeError for image v2 + Bug `1509054 <https://bugs.launchpad.net/bugs/1509054>`_ + +* Inconsistent output with `openstack usage show` + Bug `1512220 <https://bugs.launchpad.net/bugs/1512220>`_ + +* Validate --lines with `openstack console log show` + Bug `1512263 <https://bugs.launchpad.net/bugs/1512263>`_ + +* Validate --project does not exist with `openstack quota set` + Bug `1512638 <https://bugs.launchpad.net/bugs/1512638>`_ + +* Cannot list servers while --user specified for `openstack server list` + Bug `1513701 <https://bugs.launchpad.net/bugs/1513701>`_ + +* Cannot list volumes while --user specified for `openstack volume list` + Bug `1514145 <https://bugs.launchpad.net/bugs/1514145>`_ + +* Cannot find volume with --block-device-mapping with `openstack server create` + Bug `1514394 <https://bugs.launchpad.net/bugs/1514394>`_ + +* Fix documentation for `credential set` + Bug `1418837 <https://bugs.launchpad.net/bugs/1418837>`_ + +1.8.0 (18 Oct 2015) +=================== + +* `image create --volume` command (v1) will attempt to create two images + Bug `1497221 <https://bugs.launchpad.net/bugs/1497221>`_ + +* Add filtering by project/uesr for command `volume list` + Bug `1483976 <https://bugs.launchpad.net/bugs/1483976>`_ + +* Password used by plugin shows up in debug mode + Bug `1501598 <https://bugs.launchpad.net/bugs/1501598>`_ + +* Add support for `object store account` + Bug `1501943 <https://bugs.launchpad.net/bugs/1501943>`_ + +* Add support for setting properties on objects and containers + Bug `1501945 <https://bugs.launchpad.net/bugs/1501945>`_ + +* `role list` ignores `--inherited` option + Bug `1502822 <https://bugs.launchpad.net/bugs/1502822>`_ + +* `compute service list` does not return service ID number + Bug `1503430 <https://bugs.launchpad.net/bugs/1503430>`_ + +* Containers and objects with non-ascii characters fails + Bug `1503508 <https://bugs.launchpad.net/bugs/1503508>`_ + 1.7.1 (30 Sep 2015) =================== diff --git a/examples/common.py b/examples/common.py index 840e715..6d48a8c 100755 --- a/examples/common.py +++ b/examples/common.py @@ -37,7 +37,7 @@ import os import sys import traceback -from keystoneclient import session as ksc_session +from keystoneauth1 import session as ks_session from openstackclient.api import auth @@ -226,7 +226,7 @@ def make_session(opts, **kwargs): ) auth_p = auth_plugin.load_from_options(**auth_params) - session = ksc_session.Session( + session = ks_session.Session( auth=auth_p, **kwargs ) diff --git a/functional/common/test.py b/functional/common/test.py index 50d59fd..2fc355f 100644 --- a/functional/common/test.py +++ b/functional/common/test.py @@ -28,13 +28,14 @@ EXAMPLE_DIR = os.path.join(ROOT_DIR, 'examples') def execute(cmd, fail_ok=False, merge_stderr=False): """Executes specified command for the given action.""" - cmdlist = shlex.split(cmd.encode('utf-8')) + cmdlist = shlex.split(cmd) result = '' result_err = '' stdout = subprocess.PIPE stderr = subprocess.STDOUT if merge_stderr else subprocess.PIPE proc = subprocess.Popen(cmdlist, stdout=stdout, stderr=stderr) result, result_err = proc.communicate() + result = result.decode('utf-8') if not fail_ok and proc.returncode != 0: raise exceptions.CommandFailed(proc.returncode, cmd, result, result_err) diff --git a/functional/tests/common/test_configuration.py b/functional/tests/common/test_configuration.py index f4a0209..801ee10 100644 --- a/functional/tests/common/test_configuration.py +++ b/functional/tests/common/test_configuration.py @@ -30,9 +30,15 @@ class ConfigurationTests(test.TestCase): def test_configuration_show_unmask(self): raw_output = self.openstack('configuration show --unmask ' + self.opts) - passwd = os.environ['OS_PASSWORD'] - self.assertOutput(passwd + '\n', raw_output) + # If we are using os-client-config, this will not be set. Rather than + # parse clouds.yaml to get the right value, just make sure + # we are not getting redacted. + passwd = os.environ.get('OS_PASSWORD') + if passwd: + self.assertEqual(passwd + '\n', raw_output) + else: + self.assertNotEqual(configuration.REDACTED + '\n', raw_output) def test_configuration_show_mask(self): raw_output = self.openstack('configuration show --mask ' + self.opts) - self.assertOutput(configuration.REDACTED + '\n', raw_output) + self.assertEqual(configuration.REDACTED + '\n', raw_output) diff --git a/functional/tests/compute/v2/test_server.py b/functional/tests/compute/v2/test_server.py index a6cc98e..bd1b2a8 100644 --- a/functional/tests/compute/v2/test_server.py +++ b/functional/tests/compute/v2/test_server.py @@ -32,14 +32,14 @@ class ServerTests(test.TestCase): def get_flavor(cls): raw_output = cls.openstack('flavor list -f value -c ID') ray = raw_output.split('\n') - idx = len(ray)/2 + idx = int(len(ray)/2) return ray[idx] @classmethod def get_image(cls): raw_output = cls.openstack('image list -f value -c ID') ray = raw_output.split('\n') - idx = len(ray)/2 + idx = int(len(ray)/2) return ray[idx] @classmethod @@ -49,7 +49,7 @@ class ServerTests(test.TestCase): except exceptions.CommandFailed: return '' ray = raw_output.split('\n') - idx = len(ray)/2 + idx = int(len(ray)/2) return ' --nic net-id=' + ray[idx] @classmethod diff --git a/functional/tests/identity/v3/test_identity.py b/functional/tests/identity/v3/test_identity.py index dd5f2f4..3164e8f 100644 --- a/functional/tests/identity/v3/test_identity.py +++ b/functional/tests/identity/v3/test_identity.py @@ -32,13 +32,12 @@ class IdentityTests(test.TestCase): 'enabled', 'name', 'parent_id', 'links'] ROLE_FIELDS = ['id', 'name', 'links'] SERVICE_FIELDS = ['id', 'enabled', 'name', 'type', 'description'] - REGION_FIELDS = ['description', 'enabled', 'parent_region', - 'region', 'url'] + REGION_FIELDS = ['description', 'enabled', 'parent_region', 'region'] ENDPOINT_FIELDS = ['id', 'region', 'region_id', 'service_id', 'service_name', 'service_type', 'enabled', 'interface', 'url'] - REGION_LIST_HEADERS = ['Region', 'Parent Region', 'Description', 'URL'] + REGION_LIST_HEADERS = ['Region', 'Parent Region', 'Description'] ENDPOINT_LIST_HEADERS = ['ID', 'Region', 'Service Name', 'Service Type', 'Enabled', 'Interface', 'URL'] @@ -194,7 +193,6 @@ class IdentityTests(test.TestCase): def _create_dummy_region(self, parent_region=None, add_clean_up=True): region_id = data_utils.rand_name('TestRegion') description = data_utils.rand_name('description') - url = data_utils.rand_url() parent_region_arg = '' if parent_region is not None: parent_region_arg = '--parent-region %s' % parent_region @@ -202,10 +200,8 @@ class IdentityTests(test.TestCase): 'region create ' '%(parent_region_arg)s ' '--description %(description)s ' - '--url %(url)s ' '%(id)s' % {'parent_region_arg': parent_region_arg, 'description': description, - 'url': url, 'id': region_id}) items = self.parse_show(raw_output) self.assert_show_fields(items, self.REGION_FIELDS) diff --git a/functional/tests/image/v1/test_image.py b/functional/tests/image/v1/test_image.py index 17c0c3d..fe61f83 100644 --- a/functional/tests/image/v1/test_image.py +++ b/functional/tests/image/v1/test_image.py @@ -35,10 +35,9 @@ class ImageTests(test.TestCase): @classmethod def tearDownClass(cls): # Rename test - opts = cls.get_show_opts(cls.FIELDS) - raw_output = cls.openstack( - 'image set --name ' + cls.OTHER_NAME + ' ' + cls.NAME + opts) - cls.assertOutput(cls.OTHER_NAME + "\n", raw_output) + raw_output = cls.openstack('image set --name ' + cls.OTHER_NAME + ' ' + + cls.NAME) + cls.assertOutput('', raw_output) # Delete test raw_output = cls.openstack('image delete ' + cls.OTHER_NAME) cls.assertOutput('', raw_output) @@ -56,13 +55,13 @@ class ImageTests(test.TestCase): def test_image_set(self): opts = self.get_show_opts([ "disk_format", "is_public", "min_disk", "min_ram", "name"]) - raw_output = self.openstack('image set --min-disk 4 --min-ram 5 ' + - '--disk-format qcow2 --public ' + - self.NAME + opts) + self.openstack('image set --min-disk 4 --min-ram 5 ' + + '--disk-format qcow2 --public ' + self.NAME) + raw_output = self.openstack('image show ' + self.NAME + opts) self.assertEqual("qcow2\nTrue\n4\n5\n" + self.NAME + '\n', raw_output) def test_image_metadata(self): opts = self.get_show_opts(["name", "properties"]) - raw_output = self.openstack( - 'image set --property a=b --property c=d ' + self.NAME + opts) + self.openstack('image set --property a=b --property c=d ' + self.NAME) + raw_output = self.openstack('image show ' + self.NAME + opts) self.assertEqual(self.NAME + "\na='b', c='d'\n", raw_output) diff --git a/functional/tests/network/__init__.py b/functional/tests/network/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/functional/tests/network/__init__.py diff --git a/functional/tests/network/v2/__init__.py b/functional/tests/network/v2/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/functional/tests/network/v2/__init__.py diff --git a/functional/tests/network/v2/test_network.py b/functional/tests/network/v2/test_network.py new file mode 100644 index 0000000..0b2f35e --- /dev/null +++ b/functional/tests/network/v2/test_network.py @@ -0,0 +1,50 @@ +# 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 uuid + +from functional.common import test + + +class NetworkTests(test.TestCase): + """Functional tests for network. """ + NAME = uuid.uuid4().hex + HEADERS = ['Name'] + FIELDS = ['name'] + + @classmethod + def setUpClass(cls): + opts = cls.get_show_opts(cls.FIELDS) + raw_output = cls.openstack('network create ' + cls.NAME + opts) + expected = cls.NAME + '\n' + cls.assertOutput(expected, raw_output) + + @classmethod + def tearDownClass(cls): + raw_output = cls.openstack('network delete ' + cls.NAME) + cls.assertOutput('', raw_output) + + def test_network_list(self): + opts = self.get_list_opts(self.HEADERS) + raw_output = self.openstack('network list' + opts) + self.assertIn(self.NAME, raw_output) + + def test_network_set(self): + raw_output = self.openstack('network set --disable ' + self.NAME) + opts = self.get_show_opts(['name', 'state']) + raw_output = self.openstack('network show ' + self.NAME + opts) + self.assertEqual(self.NAME + "\nDOWN\n", raw_output) + + def test_network_show(self): + opts = self.get_show_opts(self.FIELDS) + raw_output = self.openstack('network show ' + self.NAME + opts) + self.assertEqual(self.NAME + "\n", raw_output) diff --git a/functional/tests/volume/v1/test_volume.py b/functional/tests/volume/v1/test_volume.py index 596b821..874be6e 100644 --- a/functional/tests/volume/v1/test_volume.py +++ b/functional/tests/volume/v1/test_volume.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import os import uuid from functional.tests.volume.v1 import common @@ -25,6 +26,7 @@ class VolumeTests(common.BaseVolumeTests): @classmethod def setUpClass(cls): + os.environ['OS_VOLUME_API_VERSION'] = '1' opts = cls.get_show_opts(cls.FIELDS) raw_output = cls.openstack('volume create --size 1 ' + cls.NAME + opts) expected = cls.NAME + '\n' diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py index ba83ce4..97eb7e4 100644 --- a/openstackclient/api/api.py +++ b/openstackclient/api/api.py @@ -15,8 +15,9 @@ import simplejson as json -from keystoneclient import exceptions as ksc_exceptions -from keystoneclient import session as ksc_session +from keystoneauth1 import exceptions as ks_exceptions +from keystoneauth1 import session as ks_session + from openstackclient.common import exceptions @@ -24,7 +25,7 @@ class KeystoneSession(object): """Wrapper for the Keystone Session Restore some requests.session.Session compatibility; - keystoneclient.session.Session.request() has the method and url + keystoneauth1.session.Session.request() has the method and url arguments swapped from the rest of the requests-using world. """ @@ -70,7 +71,7 @@ class KeystoneSession(object): if not session: session = self.session if not session: - session = ksc_session.Session() + session = ks_session.Session() if self.endpoint: if url: @@ -255,7 +256,7 @@ class BaseAPI(KeystoneSession): return data[0] if len(data) > 1: msg = "Multiple %s exist with %s='%s'" - raise ksc_exceptions.CommandError( + raise exceptions.CommandError( msg % (resource, attr, value), ) @@ -314,7 +315,7 @@ class BaseAPI(KeystoneSession): num_bulk = len(bulk_list) if num_bulk == 0: msg = "none found" - raise ksc_exceptions.NotFound(msg) + raise exceptions.NotFound(msg) elif num_bulk > 1: msg = "many found" raise RuntimeError(msg) @@ -338,12 +339,12 @@ class BaseAPI(KeystoneSession): try: ret = self._request('GET', "/%s/%s" % (path, value)).json() - except ksc_exceptions.NotFound: + except ks_exceptions.NotFound: kwargs = {attr: value} try: ret = self.find_one("/%s/detail" % (path), **kwargs) - except ksc_exceptions.NotFound: + except ks_exceptions.NotFound: msg = "%s not found" % value - raise ksc_exceptions.NotFound(msg) + raise exceptions.NotFound(msg) return ret diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index c52eeb3..ab75a78 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -14,8 +14,11 @@ """Object Store v1 API Library""" import io +import logging import os + import six +from six.moves import urllib try: from urllib.parse import urlparse # noqa @@ -23,6 +26,7 @@ except ImportError: from urlparse import urlparse # noqa from openstackclient.api import api +from openstackclient.common import utils class APIv1(api.BaseAPI): @@ -42,11 +46,9 @@ class APIv1(api.BaseAPI): :returns: dict of returned headers """ - - response = self.create(container, method='PUT') - url_parts = urlparse(self.endpoint) + response = self.create(urllib.parse.quote(container), method='PUT') data = { - 'account': url_parts.path.split('/')[-1], + 'account': self._find_account_id(), 'container': container, 'x-trans-id': response.headers.get('x-trans-id', None), } @@ -64,7 +66,7 @@ class APIv1(api.BaseAPI): """ if container: - self.delete(container) + self.delete(urllib.parse.quote(container)) def container_list( self, @@ -140,6 +142,23 @@ class APIv1(api.BaseAPI): for object in objects: self.object_save(container=container, object=object['name']) + def container_set( + self, + container, + properties, + ): + """Set container properties + + :param string container: + name of container to modify + :param dict properties: + properties to add or update for the container + """ + + headers = self._set_properties(properties, 'X-Container-Meta-%s') + if headers: + self.create(urllib.parse.quote(container), headers=headers) + def container_show( self, container=None, @@ -152,22 +171,52 @@ class APIv1(api.BaseAPI): dict of returned headers """ - response = self._request('HEAD', container) + response = self._request('HEAD', urllib.parse.quote(container)) data = { - 'account': response.headers.get('x-container-meta-owner', None), + 'account': self._find_account_id(), 'container': container, 'object_count': response.headers.get( 'x-container-object-count', None, ), - 'bytes_used': response.headers.get('x-container-bytes-used', None), - 'read_acl': response.headers.get('x-container-read', None), - 'write_acl': response.headers.get('x-container-write', None), - 'sync_to': response.headers.get('x-container-sync-to', None), - 'sync_key': response.headers.get('x-container-sync-key', None), + 'bytes_used': response.headers.get('x-container-bytes-used', None) } + + if 'x-container-read' in response.headers: + data['read_acl'] = response.headers.get('x-container-read', None) + if 'x-container-write' in response.headers: + data['write_acl'] = response.headers.get('x-container-write', None) + if 'x-container-sync-to' in response.headers: + data['sync_to'] = response.headers.get('x-container-sync-to', None) + if 'x-container-sync-key' in response.headers: + data['sync_key'] = response.headers.get('x-container-sync-key', + None) + + properties = self._get_properties(response.headers, + 'x-container-meta-') + if properties: + data['properties'] = properties + return data + def container_unset( + self, + container, + properties, + ): + """Unset container properties + + :param string container: + name of container to modify + :param dict properties: + properties to remove from the container + """ + + headers = self._unset_properties(properties, + 'X-Remove-Container-Meta-%s') + if headers: + self.create(urllib.parse.quote(container), headers=headers) + def object_create( self, container=None, @@ -187,16 +236,16 @@ class APIv1(api.BaseAPI): # TODO(dtroyer): What exception to raise here? return {} - full_url = "%s/%s" % (container, object) + full_url = "%s/%s" % (urllib.parse.quote(container), + urllib.parse.quote(object)) with io.open(object, 'rb') as f: response = self.create( full_url, method='PUT', data=f, ) - url_parts = urlparse(self.endpoint) data = { - 'account': url_parts.path.split('/')[-1], + 'account': self._find_account_id(), 'container': container, 'object': object, 'x-trans-id': response.headers.get('X-Trans-Id', None), @@ -221,7 +270,8 @@ class APIv1(api.BaseAPI): if container is None or object is None: return - self.delete("%s/%s" % (container, object)) + self.delete("%s/%s" % (urllib.parse.quote(container), + urllib.parse.quote(object))) def object_list( self, @@ -298,7 +348,7 @@ class APIv1(api.BaseAPI): if delimiter: params['delimiter'] = delimiter - return self.list(container, **params) + return self.list(urllib.parse.quote(container), **params) def object_save( self, @@ -321,7 +371,8 @@ class APIv1(api.BaseAPI): response = self._request( 'GET', - "%s/%s" % (container, object), + "%s/%s" % (urllib.parse.quote(container), + urllib.parse.quote(object)), stream=True, ) if response.status_code == 200: @@ -332,6 +383,50 @@ class APIv1(api.BaseAPI): for chunk in response.iter_content(): f.write(chunk) + def object_set( + self, + container, + object, + properties, + ): + """Set object properties + + :param string container: + container name for object to modify + :param string object: + name of object to modify + :param dict properties: + properties to add or update for the container + """ + + headers = self._set_properties(properties, 'X-Object-Meta-%s') + if headers: + self.create("%s/%s" % (urllib.parse.quote(container), + urllib.parse.quote(object)), + headers=headers) + + def object_unset( + self, + container, + object, + properties, + ): + """Unset object properties + + :param string container: + container name for object to modify + :param string object: + name of object to modify + :param dict properties: + properties to remove from the object + """ + + headers = self._unset_properties(properties, 'X-Remove-Object-Meta-%s') + if headers: + self.create("%s/%s" % (urllib.parse.quote(container), + urllib.parse.quote(object)), + headers=headers) + def object_show( self, container=None, @@ -350,9 +445,12 @@ class APIv1(api.BaseAPI): if container is None or object is None: return {} - response = self._request('HEAD', "%s/%s" % (container, object)) + response = self._request('HEAD', "%s/%s" % + (urllib.parse.quote(container), + urllib.parse.quote(object))) + data = { - 'account': response.headers.get('x-container-meta-owner', None), + 'account': self._find_account_id(), 'container': container, 'object': object, 'content-type': response.headers.get('content-type', None), @@ -371,18 +469,107 @@ class APIv1(api.BaseAPI): 'x-object-manifest', None, ) - for key, value in six.iteritems(response.headers): - if key.startswith('x-object-meta-'): - data[key[len('x-object-meta-'):].lower()] = value - elif key not in ( - 'content-type', - 'content-length', - 'last-modified', - 'etag', - 'date', - 'x-object-manifest', - 'x-container-meta-owner', - ): - data[key.lower()] = value + properties = self._get_properties(response.headers, 'x-object-meta-') + if properties: + data['properties'] = properties + + return data + + def account_set( + self, + properties, + ): + """Set account properties + + :param dict properties: + properties to add or update for the account + """ + + headers = self._set_properties(properties, 'X-Account-Meta-%s') + if headers: + # NOTE(stevemar): The URL (first argument) in this case is already + # set to the swift account endpoint, because that's how it's + # registered in the catalog + self.create("", headers=headers) + + def account_show(self): + """Show account details""" + + # NOTE(stevemar): Just a HEAD request to the endpoint already in the + # catalog should be enough. + response = self._request("HEAD", "") + data = {} + + properties = self._get_properties(response.headers, 'x-account-meta-') + if properties: + data['properties'] = properties + + # Map containers, bytes and objects a bit nicer + data['Containers'] = response.headers.get('x-account-container-count', + None) + data['Objects'] = response.headers.get('x-account-object-count', None) + data['Bytes'] = response.headers.get('x-account-bytes-used', None) + # Add in Account info too + data['Account'] = self._find_account_id() return data + + def account_unset( + self, + properties, + ): + """Unset account properties + + :param dict properties: + properties to remove from the account + """ + + headers = self._unset_properties(properties, + 'X-Remove-Account-Meta-%s') + if headers: + self.create("", headers=headers) + + def _find_account_id(self): + url_parts = urlparse(self.endpoint) + return url_parts.path.split('/')[-1] + + def _unset_properties(self, properties, header_tag): + # NOTE(stevemar): As per the API, the headers have to be in the form + # of "X-Remove-Account-Meta-Book: x". In the case where metadata is + # removed, we can set the value of the header to anything, so it's + # set to 'x'. In the case of a Container property we use: + # "X-Remove-Container-Meta-Book: x", and the same logic applies for + # Object properties + + headers = {} + for k in properties: + header_name = header_tag % k + headers[header_name] = 'x' + return headers + + def _set_properties(self, properties, header_tag): + # NOTE(stevemar): As per the API, the headers have to be in the form + # of "X-Account-Meta-Book: MobyDick". In the case of a Container + # property we use: "X-Add-Container-Meta-Book: MobyDick", and the same + # logic applies for Object properties + + log = logging.getLogger(__name__ + '._set_properties') + + headers = {} + for k, v in properties.iteritems(): + if not utils.is_ascii(k) or not utils.is_ascii(v): + log.error('Cannot set property %s to non-ascii value', k) + continue + + header_name = header_tag % k + headers[header_name] = v + return headers + + def _get_properties(self, headers, header_tag): + # Add in properties as a top level key, this is consistent with other + # OSC commands + properties = {} + for k, v in six.iteritems(headers): + if k.startswith(header_tag): + properties[k[len(header_tag):]] = v + return properties diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 55c6fe5..edabf65 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -20,6 +20,7 @@ import logging import pkg_resources import sys +from oslo_utils import strutils import requests from openstackclient.api import auth @@ -167,7 +168,8 @@ class ClientManager(object): self._project_name = self._auth_params['tenant_name'] LOG.info('Using auth plugin: %s' % self.auth_plugin_name) - LOG.debug('Using parameters %s' % self._auth_params) + LOG.debug('Using parameters %s' % + strutils.mask_password(self._auth_params)) self.auth = auth_plugin.load_from_options(**self._auth_params) # needed by SAML authentication request_session = requests.session() diff --git a/openstackclient/common/context.py b/openstackclient/common/logs.py index 6d1aec1..6d1aec1 100644 --- a/openstackclient/common/context.py +++ b/openstackclient/common/logs.py diff --git a/openstackclient/common/parseractions.py b/openstackclient/common/parseractions.py index 8f6008e..fd90369 100644 --- a/openstackclient/common/parseractions.py +++ b/openstackclient/common/parseractions.py @@ -65,3 +65,18 @@ class RangeAction(argparse.Action): # Too many values msg = "Invalid range, too many values" raise argparse.ArgumentError(self, msg) + + +class NonNegativeAction(argparse.Action): + """A custom action to check whether the value is non-negative or not + + Ensures the value is >= 0. + """ + + def __call__(self, parser, namespace, values, option_string=None): + try: + assert(int(values) >= 0) + setattr(namespace, self.dest, values) + except Exception: + msg = "%s expected a non-negative integer" % (str(option_string)) + raise argparse.ArgumentTypeError(self, msg) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index e092fef..8a9b910 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -81,6 +81,7 @@ class SetQuota(command.Command): parser.add_argument( '--%s' % v, metavar='<%s>' % v, + dest=k, type=int, help='New value for the %s quota' % v, ) @@ -94,6 +95,7 @@ class SetQuota(command.Command): @utils.log_method(log) def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity compute_client = self.app.client_manager.compute volume_client = self.app.client_manager.volume @@ -115,23 +117,29 @@ class SetQuota(command.Command): sys.stderr.write("No quotas updated") return + if parsed_args.project: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + if parsed_args.quota_class: if compute_kwargs: compute_client.quota_classes.update( - parsed_args.project, + project.id, **compute_kwargs) if volume_kwargs: volume_client.quota_classes.update( - parsed_args.project, + project.id, **volume_kwargs) else: if compute_kwargs: compute_client.quotas.update( - parsed_args.project, + project.id, **compute_kwargs) if volume_kwargs: volume_client.quotas.update( - parsed_args.project, + project.id, **volume_kwargs) @@ -165,13 +173,19 @@ class ShowQuota(show.ShowOne): return parser def get_compute_volume_quota(self, client, parsed_args): + identity_client = self.app.client_manager.identity + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ).id + try: if parsed_args.quota_class: - quota = client.quota_classes.get(parsed_args.project) + quota = client.quota_classes.get(project) elif parsed_args.default: - quota = client.quotas.defaults(parsed_args.project) + quota = client.quotas.defaults(project) else: - quota = client.quotas.get(parsed_args.project) + quota = client.quotas.get(project) except Exception as e: if type(e).__name__ == 'EndpointNotFound': return {} diff --git a/openstackclient/common/session.py b/openstackclient/common/session.py index dda1c41..9b19fd4 100644 --- a/openstackclient/common/session.py +++ b/openstackclient/common/session.py @@ -11,9 +11,9 @@ # under the License. # -"""Subclass of keystoneclient.session""" +"""Subclass of keystoneauth1.session""" -from keystoneclient import session +from keystoneauth1 import session class TimingSession(session.Session): diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index b6726bf..42630d9 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -94,12 +94,15 @@ def find_resource(manager, name_or_id, **kwargs): if len(kwargs) == 0: kwargs = {} - # Prepare the kwargs for calling find - if 'NAME_ATTR' in manager.resource_class.__dict__: - # novaclient does this for oddball resources - kwargs[manager.resource_class.NAME_ATTR] = name_or_id - else: - kwargs['name'] = name_or_id + try: + # Prepare the kwargs for calling find + if 'NAME_ATTR' in manager.resource_class.__dict__: + # novaclient does this for oddball resources + kwargs[manager.resource_class.NAME_ATTR] = name_or_id + else: + kwargs['name'] = name_or_id + except Exception: + pass # finally try to find entity by name try: @@ -118,14 +121,30 @@ def find_resource(manager, name_or_id, **kwargs): (manager.resource_class.__name__.lower(), name_or_id) raise exceptions.CommandError(msg) else: - raise + pass + + try: + for resource in manager.list(): + # short circuit and return the first match + if (resource.get('id') == name_or_id or + resource.get('name') == name_or_id): + return resource + else: + # we found no match, keep going to bomb out + pass + except Exception: + # in case the list fails for some reason + pass + + # if we hit here, we've failed, report back this error: + msg = "Could not find resource %s" % name_or_id + raise exceptions.CommandError(msg) def format_dict(data): """Return a formatted string of key value pairs :param data: a dict - :param format: optional formatting hints :rtype: a string formatted to key='value' """ @@ -278,14 +297,16 @@ def wait_for_status(status_f, res_id, status_field='status', success_status=['active'], + error_status=['error'], sleep_time=5, callback=None): """Wait for status change on a resource during a long-running operation :param status_f: a status function that takes a single id argument :param res_id: the resource id to watch - :param success_status: a list of status strings for successful completion :param status_field: the status attribute in the returned resource object + :param success_status: a list of status strings for successful completion + :param error_status: a list of status strings for error :param sleep_time: wait this long (seconds) :param callback: called per sleep cycle, useful to display progress :rtype: True on success @@ -296,7 +317,7 @@ def wait_for_status(status_f, if status in success_status: retval = True break - elif status == 'error': + elif status in error_status: retval = False break if callback: @@ -314,6 +335,7 @@ def wait_for_delete(manager, callback=None): """Wait for resource deletion + :param manager: the manager from which we can get the resource :param res_id: the resource id to watch :param status_field: the status attribute in the returned resource object, this is used to check for error states while the resource is being @@ -355,7 +377,7 @@ def wait_for_delete(manager, def get_effective_log_level(): """Returns the lowest logging level considered by logging handlers - Retrieve an return the smallest log level set among the root + Retrieve and return the smallest log level set among the root logger's handlers (in case of multiple handlers). """ root_log = logging.getLogger() @@ -399,3 +421,11 @@ def build_kwargs_dict(arg_name, value): if value: kwargs[arg_name] = value return kwargs + + +def is_ascii(string): + try: + string.decode('ascii') + return True + except UnicodeDecodeError: + return False diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index bb0747b..aafa5d4 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -22,6 +22,7 @@ import sys from cliff import command from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -42,6 +43,7 @@ class ShowConsoleLog(command.Command): metavar='<num-lines>', type=int, default=None, + action=parseractions.NonNegativeAction, help='Number of lines to display from the end of the log ' '(default=all)', ) diff --git a/openstackclient/compute/v2/fixedip.py b/openstackclient/compute/v2/fixedip.py index d105e39..da9d85c 100644 --- a/openstackclient/compute/v2/fixedip.py +++ b/openstackclient/compute/v2/fixedip.py @@ -23,7 +23,7 @@ from openstackclient.common import utils class AddFixedIP(command.Command): - """Add fixed-ip command""" + """Add fixed IP address to server""" log = logging.getLogger(__name__ + ".AddFixedIP") @@ -32,12 +32,12 @@ class AddFixedIP(command.Command): parser.add_argument( "network", metavar="<network>", - help="Name of the network to fetch an IP address from", + help="Network to fetch an IP address from (name or ID)", ) parser.add_argument( "server", metavar="<server>", - help="Name of the server to receive the IP address", + help="Server to receive the IP address (name or ID)", ) return parser @@ -56,7 +56,7 @@ class AddFixedIP(command.Command): class RemoveFixedIP(command.Command): - """Remove fixed-ip command""" + """Remove fixed IP address from server""" log = logging.getLogger(__name__ + ".RemoveFixedIP") @@ -65,12 +65,12 @@ class RemoveFixedIP(command.Command): parser.add_argument( "ip_address", metavar="<ip-address>", - help="IP address to remove from server", + help="IP address to remove from server (name only)", ) parser.add_argument( "server", metavar="<server>", - help="Name of the server to remove the IP address from", + help="Server to remove the IP address from (name or ID)", ) return parser diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 3458cf7..7474580 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -181,6 +181,15 @@ class ListFlavor(lister.Lister): action='store_true', default=False, help='List additional fields in output') + parser.add_argument( + '--marker', + metavar="<marker>", + help='The last flavor ID of the previous page') + parser.add_argument( + '--limit', + type=int, + metavar="<limit>", + help='Maximum number of flavors to display') return parser def take_action(self, parsed_args): @@ -202,7 +211,9 @@ class ListFlavor(lister.Lister): # and flavors from their own projects only. is_public = None if parsed_args.all else parsed_args.public - data = compute_client.flavors.list(is_public=is_public) + data = compute_client.flavors.list(is_public=is_public, + marker=parsed_args.marker, + limit=parsed_args.limit) if parsed_args.long: columns = columns + ( diff --git a/openstackclient/compute/v2/floatingip.py b/openstackclient/compute/v2/floatingip.py index c557c24..65fe591 100644 --- a/openstackclient/compute/v2/floatingip.py +++ b/openstackclient/compute/v2/floatingip.py @@ -26,7 +26,7 @@ from openstackclient.common import utils class AddFloatingIP(command.Command): - """Add floating-ip to server""" + """Add floating IP address to server""" log = logging.getLogger(__name__ + ".AddFloatingIP") @@ -35,7 +35,7 @@ class AddFloatingIP(command.Command): parser.add_argument( "ip_address", metavar="<ip-address>", - help="IP address to add to server", + help="IP address to add to server (name only)", ) parser.add_argument( "server", @@ -56,7 +56,7 @@ class AddFloatingIP(command.Command): class CreateFloatingIP(show.ShowOne): - """Create new floating-ip""" + """Create new floating IP address""" log = logging.getLogger(__name__ + '.CreateFloatingIP') @@ -65,7 +65,7 @@ class CreateFloatingIP(show.ShowOne): parser.add_argument( 'pool', metavar='<pool>', - help='Pool to fetch floating IP from', + help='Pool to fetch IP address from (name or ID)', ) return parser @@ -80,7 +80,7 @@ class CreateFloatingIP(show.ShowOne): class DeleteFloatingIP(command.Command): - """Delete a floating-ip""" + """Delete a floating IP address""" log = logging.getLogger(__name__ + '.DeleteFloatingIP') @@ -89,7 +89,7 @@ class DeleteFloatingIP(command.Command): parser.add_argument( "ip_address", metavar="<ip-address>", - help="IP address to delete", + help="IP address to delete (ID only)", ) return parser @@ -107,7 +107,7 @@ class DeleteFloatingIP(command.Command): class ListFloatingIP(lister.Lister): - """List floating-ips""" + """List floating IP addresses""" log = logging.getLogger(__name__ + '.ListFloatingIP') @@ -127,7 +127,7 @@ class ListFloatingIP(lister.Lister): class RemoveFloatingIP(command.Command): - """Remove floating-ip from server""" + """Remove floating IP address from server""" log = logging.getLogger(__name__ + ".RemoveFloatingIP") @@ -136,7 +136,7 @@ class RemoveFloatingIP(command.Command): parser.add_argument( "ip_address", metavar="<ip-address>", - help="IP address to remove from server", + help="IP address to remove from server (name only)", ) parser.add_argument( "server", diff --git a/openstackclient/compute/v2/floatingippool.py b/openstackclient/compute/v2/floatingippool.py index cc48555..39a2d8f 100644 --- a/openstackclient/compute/v2/floatingippool.py +++ b/openstackclient/compute/v2/floatingippool.py @@ -23,7 +23,7 @@ from openstackclient.common import utils class ListFloatingIPPool(lister.Lister): - """List floating-ip-pools""" + """List pools of floating IP addresses""" log = logging.getLogger(__name__ + '.ListFloatingIPPool') diff --git a/openstackclient/compute/v2/security_group.py b/openstackclient/compute/v2/security_group.py index 3dd0c49..6d38195 100644 --- a/openstackclient/compute/v2/security_group.py +++ b/openstackclient/compute/v2/security_group.py @@ -23,7 +23,7 @@ from cliff import command from cliff import lister from cliff import show -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc try: from novaclient.v2 import security_group_rules @@ -93,6 +93,64 @@ class CreateSecurityGroup(show.ShowOne): return zip(*sorted(six.iteritems(info))) +class CreateSecurityGroupRule(show.ShowOne): + """Create a new security group rule""" + + log = logging.getLogger(__name__ + ".CreateSecurityGroupRule") + + def get_parser(self, prog_name): + parser = super(CreateSecurityGroupRule, self).get_parser(prog_name) + parser.add_argument( + 'group', + metavar='<group>', + help='Create rule in this security group (name or ID)', + ) + parser.add_argument( + "--proto", + metavar="<proto>", + default="tcp", + help="IP protocol (icmp, tcp, udp; default: tcp)", + ) + parser.add_argument( + "--src-ip", + metavar="<ip-address>", + default="0.0.0.0/0", + help="Source IP (may use CIDR notation; default: 0.0.0.0/0)", + ) + parser.add_argument( + "--dst-port", + metavar="<port-range>", + default=(0, 0), + action=parseractions.RangeAction, + help="Destination port, may be a range: 137:139 (default: 0; " + "only required for proto tcp and udp)", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + compute_client = self.app.client_manager.compute + group = utils.find_resource( + compute_client.security_groups, + parsed_args.group, + ) + if parsed_args.proto.lower() == 'icmp': + from_port, to_port = -1, -1 + else: + from_port, to_port = parsed_args.dst_port + data = compute_client.security_group_rules.create( + group.id, + parsed_args.proto, + from_port, + to_port, + parsed_args.src_ip, + ) + + info = _xform_security_group_rule(data._info) + return zip(*sorted(six.iteritems(info))) + + class DeleteSecurityGroup(command.Command): """Delete a security group""" @@ -103,7 +161,7 @@ class DeleteSecurityGroup(command.Command): parser.add_argument( 'group', metavar='<group>', - help='Name or ID of security group to delete', + help='Security group to delete (name or ID)', ) return parser @@ -119,8 +177,30 @@ class DeleteSecurityGroup(command.Command): return +class DeleteSecurityGroupRule(command.Command): + """Delete a security group rule""" + + log = logging.getLogger(__name__ + '.DeleteSecurityGroupRule') + + def get_parser(self, prog_name): + parser = super(DeleteSecurityGroupRule, self).get_parser(prog_name) + parser.add_argument( + 'rule', + metavar='<rule>', + help='Security group rule to delete (ID only)', + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + + compute_client = self.app.client_manager.compute + compute_client.security_group_rules.delete(parsed_args.rule) + return + + class ListSecurityGroup(lister.Lister): - """List all security groups""" + """List security groups""" log = logging.getLogger(__name__ + ".ListSecurityGroup") @@ -161,7 +241,7 @@ class ListSecurityGroup(lister.Lister): project_hash = {} try: projects = self.app.client_manager.identity.projects.list() - except ksc_exc.ClientException: + except ks_exc.ClientException: # This fails when the user is not an admin, just move along pass else: @@ -175,6 +255,49 @@ class ListSecurityGroup(lister.Lister): ) for s in data)) +class ListSecurityGroupRule(lister.Lister): + """List security group rules""" + + log = logging.getLogger(__name__ + ".ListSecurityGroupRule") + + def get_parser(self, prog_name): + parser = super(ListSecurityGroupRule, self).get_parser(prog_name) + parser.add_argument( + 'group', + metavar='<group>', + help='List all rules in this security group (name or ID)', + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + compute_client = self.app.client_manager.compute + group = utils.find_resource( + compute_client.security_groups, + parsed_args.group, + ) + + # Argh, the rules are not Resources... + rules = [] + for rule in group.rules: + rules.append(security_group_rules.SecurityGroupRule( + compute_client.security_group_rules, + _xform_security_group_rule(rule), + )) + + columns = column_headers = ( + "ID", + "IP Protocol", + "IP Range", + "Port Range", + ) + return (column_headers, + (utils.get_item_properties( + s, columns, + ) for s in rules)) + + class SetSecurityGroup(show.ShowOne): """Set security group properties""" @@ -185,7 +308,7 @@ class SetSecurityGroup(show.ShowOne): parser.add_argument( 'group', metavar='<group>', - help='Name or ID of security group to change', + help='Security group to modify (name or ID)', ) parser.add_argument( '--name', @@ -195,7 +318,7 @@ class SetSecurityGroup(show.ShowOne): parser.add_argument( "--description", metavar="<description>", - help="New security group name", + help="New security group description", ) return parser @@ -227,7 +350,7 @@ class SetSecurityGroup(show.ShowOne): class ShowSecurityGroup(show.ShowOne): - """Show a specific security group""" + """Display security group details""" log = logging.getLogger(__name__ + '.ShowSecurityGroup') @@ -236,7 +359,7 @@ class ShowSecurityGroup(show.ShowOne): parser.add_argument( 'group', metavar='<group>', - help='Name or ID of security group to change', + help='Security group to display (name or ID)', ) return parser @@ -263,126 +386,3 @@ class ShowSecurityGroup(show.ShowOne): ) return zip(*sorted(six.iteritems(info))) - - -class CreateSecurityGroupRule(show.ShowOne): - """Create a new security group rule""" - - log = logging.getLogger(__name__ + ".CreateSecurityGroupRule") - - def get_parser(self, prog_name): - parser = super(CreateSecurityGroupRule, self).get_parser(prog_name) - parser.add_argument( - 'group', - metavar='<group>', - help='Create rule in this security group', - ) - parser.add_argument( - "--proto", - metavar="<proto>", - default="tcp", - help="IP protocol (icmp, tcp, udp; default: tcp)", - ) - parser.add_argument( - "--src-ip", - metavar="<ip-address>", - default="0.0.0.0/0", - help="Source IP (may use CIDR notation; default: 0.0.0.0/0)", - ) - parser.add_argument( - "--dst-port", - metavar="<port-range>", - default=(0, 0), - action=parseractions.RangeAction, - help="Destination port, may be a range: 137:139 (default: 0; " - "only required for proto tcp and udp)", - ) - return parser - - def take_action(self, parsed_args): - self.log.debug("take_action(%s)", parsed_args) - - compute_client = self.app.client_manager.compute - group = utils.find_resource( - compute_client.security_groups, - parsed_args.group, - ) - if parsed_args.proto.lower() == 'icmp': - from_port, to_port = -1, -1 - else: - from_port, to_port = parsed_args.dst_port - data = compute_client.security_group_rules.create( - group.id, - parsed_args.proto, - from_port, - to_port, - parsed_args.src_ip, - ) - - info = _xform_security_group_rule(data._info) - return zip(*sorted(six.iteritems(info))) - - -class DeleteSecurityGroupRule(command.Command): - """Delete a security group rule""" - - log = logging.getLogger(__name__ + '.DeleteSecurityGroupRule') - - def get_parser(self, prog_name): - parser = super(DeleteSecurityGroupRule, self).get_parser(prog_name) - parser.add_argument( - 'rule', - metavar='<rule>', - help='Security group rule ID to delete', - ) - return parser - - @utils.log_method(log) - def take_action(self, parsed_args): - - compute_client = self.app.client_manager.compute - compute_client.security_group_rules.delete(parsed_args.rule) - return - - -class ListSecurityGroupRule(lister.Lister): - """List all security group rules""" - - log = logging.getLogger(__name__ + ".ListSecurityGroupRule") - - def get_parser(self, prog_name): - parser = super(ListSecurityGroupRule, self).get_parser(prog_name) - parser.add_argument( - 'group', - metavar='<group>', - help='List all rules in this security group', - ) - return parser - - def take_action(self, parsed_args): - self.log.debug("take_action(%s)", parsed_args) - - compute_client = self.app.client_manager.compute - group = utils.find_resource( - compute_client.security_groups, - parsed_args.group, - ) - - # Argh, the rules are not Resources... - rules = [] - for rule in group.rules: - rules.append(security_group_rules.SecurityGroupRule( - compute_client.security_group_rules, - _xform_security_group_rule(rule), - )) - - columns = column_headers = ( - "ID", - "IP Protocol", - "IP Range", - "Port Range", - ) - return (column_headers, - (utils.get_item_properties( - s, columns, - ) for s in rules)) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 661ce84..9dca784 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -43,7 +43,7 @@ from openstackclient.network import common def _format_servers_list_networks(networks): """Return a formatted string of a server's networks - :param server: a Server.networks field + :param networks: a Server.networks field :rtype: a string of formatted network addresses """ output = [] @@ -56,6 +56,29 @@ def _format_servers_list_networks(networks): return '; '.join(output) +def _format_servers_list_power_state(state): + """Return a formatted string of a server's power state + + :param state: the power state number of a server + :rtype: a string mapped to the power state number + """ + power_states = [ + 'NOSTATE', # 0x00 + 'Running', # 0x01 + '', # 0x02 + 'Paused', # 0x03 + 'Shutdown', # 0x04 + '', # 0x05 + 'Crashed', # 0x06 + 'Suspended' # 0x07 + ] + + try: + return power_states[state] + except Exception: + return 'N/A' + + def _get_ip_address(addresses, address_type, ip_address_family): # Old style addresses if address_type in addresses: @@ -425,10 +448,17 @@ class CreateServer(show.ShowOne): dev_key, dev_vol = dev_map.split('=', 1) block_volume = None if dev_vol: - block_volume = utils.find_resource( - volume_client.volumes, - dev_vol, - ).id + vol = dev_vol.split(':', 1)[0] + if vol: + vol_id = utils.find_resource( + volume_client.volumes, + vol, + ).id + block_volume = dev_vol.replace(vol, vol_id) + else: + msg = _("Volume name or ID must be specified if " + "--block-device-mapping is specified") + raise exceptions.CommandError(msg) block_device_mapping.update({dev_key: block_volume}) nics = [] @@ -607,7 +637,7 @@ class DeleteServer(command.Command): def get_parser(self, prog_name): parser = super(DeleteServer, self).get_parser(prog_name) parser.add_argument( - 'servers', + 'server', metavar='<server>', nargs="+", help=_('Server(s) to delete (name or ID)'), @@ -622,7 +652,7 @@ class DeleteServer(command.Command): @utils.log_method(log) def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - for server in parsed_args.servers: + for server in parsed_args.server: server_obj = utils.find_resource( compute_client.servers, server) compute_client.servers.delete(server_obj.id) @@ -717,6 +747,24 @@ class ListServer(lister.Lister): default=False, help=_('List additional fields in output'), ) + parser.add_argument( + '--marker', + metavar='<marker>', + default=None, + help=('The last server (name or ID) of the previous page. Display' + ' list of servers after marker. Display all servers if not' + ' specified.') + ) + parser.add_argument( + '--limit', + metavar='<limit>', + type=int, + default=None, + help=("Maximum number of servers to display. If limit equals -1," + " all servers will be displayed. If limit is greater than" + " 'osapi_max_limit' option of Nova API," + " 'osapi_max_limit' will be used instead."), + ) return parser @utils.log_method(log) @@ -735,7 +783,7 @@ class ListServer(lister.Lister): user_id = None if parsed_args.user: - user_id = identity_common.find_project( + user_id = identity_common.find_user( identity_client, parsed_args.user, parsed_args.user_domain, @@ -762,6 +810,8 @@ class ListServer(lister.Lister): 'ID', 'Name', 'Status', + 'OS-EXT-STS:task_state', + 'OS-EXT-STS:power_state', 'Networks', 'OS-EXT-AZ:availability_zone', 'OS-EXT-SRV-ATTR:host', @@ -771,25 +821,49 @@ class ListServer(lister.Lister): 'ID', 'Name', 'Status', + 'Task State', + 'Power State', 'Networks', 'Availability Zone', 'Host', 'Properties', ) mixed_case_fields = [ + 'OS-EXT-STS:task_state', + 'OS-EXT-STS:power_state', 'OS-EXT-AZ:availability_zone', 'OS-EXT-SRV-ATTR:host', ] else: - columns = ('ID', 'Name', 'Status', 'Networks') - column_headers = columns + columns = ( + 'ID', + 'Name', + 'Status', + 'Networks', + ) + column_headers = ( + 'ID', + 'Name', + 'Status', + 'Networks', + ) mixed_case_fields = [] - data = compute_client.servers.list(search_opts=search_opts) + + marker_id = None + if parsed_args.marker: + marker_id = utils.find_resource(compute_client.servers, + parsed_args.marker).id + + data = compute_client.servers.list(search_opts=search_opts, + marker=marker_id, + limit=parsed_args.limit) return (column_headers, (utils.get_item_properties( s, columns, mixed_case_fields=mixed_case_fields, formatters={ + 'OS-EXT-STS:power_state': + _format_servers_list_power_state, 'Networks': _format_servers_list_networks, 'Metadata': utils.format_dict, }, @@ -798,7 +872,7 @@ class ListServer(lister.Lister): class LockServer(command.Command): - """Lock a server. A non-admin user will not be able to execute actions""" + """Lock server(s). A non-admin user will not be able to execute actions""" log = logging.getLogger(__name__ + '.LockServer') @@ -807,7 +881,8 @@ class LockServer(command.Command): parser.add_argument( 'server', metavar='<server>', - help=_('Server (name or ID)'), + nargs='+', + help=_('Server(s) to lock (name or ID)'), ) return parser @@ -815,10 +890,11 @@ class LockServer(command.Command): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - utils.find_resource( - compute_client.servers, - parsed_args.server, - ).lock() + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).lock() # FIXME(dtroyer): Here is what I want, how with argparse/cliff? @@ -915,7 +991,7 @@ class MigrateServer(command.Command): class PauseServer(command.Command): - """Pause server""" + """Pause server(s)""" log = logging.getLogger(__name__ + '.PauseServer') @@ -924,18 +1000,19 @@ class PauseServer(command.Command): parser.add_argument( 'server', metavar='<server>', - help=_('Server (name or ID)'), + nargs='+', + help=_('Server(s) to pause (name or ID)'), ) return parser @utils.log_method(log) def take_action(self, parsed_args): - compute_client = self.app.client_manager.compute - utils.find_resource( - compute_client.servers, - parsed_args.server, - ).pause() + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server + ).pause() class RebootServer(command.Command): @@ -1217,7 +1294,7 @@ class ResizeServer(command.Command): class ResumeServer(command.Command): - """Resume server""" + """Resume server(s)""" log = logging.getLogger(__name__ + '.ResumeServer') @@ -1226,7 +1303,8 @@ class ResumeServer(command.Command): parser.add_argument( 'server', metavar='<server>', - help=_('Server (name or ID)'), + nargs='+', + help=_('Server(s) to resume (name or ID)'), ) return parser @@ -1234,10 +1312,11 @@ class ResumeServer(command.Command): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - utils.find_resource( - compute_client.servers, - parsed_args.server, - ) .resume() + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).resume() class SetServer(command.Command): @@ -1299,6 +1378,31 @@ class SetServer(command.Command): raise exceptions.CommandError(msg) +class ShelveServer(command.Command): + """Shelve server(s)""" + + log = logging.getLogger(__name__ + '.ShelveServer') + + def get_parser(self, prog_name): + parser = super(ShelveServer, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='<server>', + nargs='+', + help=_('Server(s) to shelve (name or ID)'), + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).shelve() + + class ShowServer(show.ShowOne): """Show server details""" @@ -1483,8 +1587,58 @@ class SshServer(command.Command): os.system(cmd % (login, ip_address)) +class StartServer(command.Command): + """Start server(s).""" + + log = logging.getLogger(__name__ + '.StartServer') + + def get_parser(self, prog_name): + parser = super(StartServer, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='<server>', + nargs="+", + help=_('Server(s) to start (name or ID)'), + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).start() + + +class StopServer(command.Command): + """Stop server(s).""" + + log = logging.getLogger(__name__ + '.StopServer') + + def get_parser(self, prog_name): + parser = super(StopServer, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='<server>', + nargs="+", + help=_('Server(s) to stop (name or ID)'), + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).stop() + + class SuspendServer(command.Command): - """Suspend server""" + """Suspend server(s)""" log = logging.getLogger(__name__ + '.SuspendServer') @@ -1493,7 +1647,8 @@ class SuspendServer(command.Command): parser.add_argument( 'server', metavar='<server>', - help=_('Server (name or ID)'), + nargs='+', + help=_('Server(s) to suspend (name or ID)'), ) return parser @@ -1501,14 +1656,15 @@ class SuspendServer(command.Command): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - utils.find_resource( - compute_client.servers, - parsed_args.server, - ).suspend() + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).suspend() class UnlockServer(command.Command): - """Unlock server""" + """Unlock server(s)""" log = logging.getLogger(__name__ + '.UnlockServer') @@ -1517,7 +1673,8 @@ class UnlockServer(command.Command): parser.add_argument( 'server', metavar='<server>', - help=_('Server (name or ID)'), + nargs='+', + help=_('Server(s) to unlock (name or ID)'), ) return parser @@ -1525,14 +1682,15 @@ class UnlockServer(command.Command): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - utils.find_resource( - compute_client.servers, - parsed_args.server, - ).unlock() + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).unlock() class UnpauseServer(command.Command): - """Unpause server""" + """Unpause server(s)""" log = logging.getLogger(__name__ + '.UnpauseServer') @@ -1541,7 +1699,8 @@ class UnpauseServer(command.Command): parser.add_argument( 'server', metavar='<server>', - help=_('Server (name or ID)'), + nargs='+', + help=_('Server(s) to unpause (name or ID)'), ) return parser @@ -1549,10 +1708,11 @@ class UnpauseServer(command.Command): def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - utils.find_resource( - compute_client.servers, - parsed_args.server, - ).unpause() + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).unpause() class UnrescueServer(command.Command): @@ -1614,3 +1774,28 @@ class UnsetServer(command.Command): server, parsed_args.property, ) + + +class UnshelveServer(command.Command): + """Unshelve server(s)""" + + log = logging.getLogger(__name__ + '.UnshelveServer') + + def get_parser(self, prog_name): + parser = super(UnshelveServer, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='<server>', + nargs='+', + help=_('Server(s) to unshelve (name or ID)'), + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + for server in parsed_args.server: + utils.find_resource( + compute_client.servers, + server, + ).unshelve() diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 4b2ebac..c2d51c2 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -17,11 +17,33 @@ import logging +from cliff import command from cliff import lister from openstackclient.common import utils +class DeleteService(command.Command): + """Delete service command""" + + log = logging.getLogger(__name__ + ".DeleteService") + + def get_parser(self, prog_name): + parser = super(DeleteService, self).get_parser(prog_name) + parser.add_argument( + "service", + metavar="<service>", + help="Compute service to delete (ID only)") + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + compute_client = self.app.client_manager.compute + + compute_client.services.delete(parsed_args.service) + return + + class ListService(lister.Lister): """List service command""" @@ -43,6 +65,7 @@ class ListService(lister.Lister): self.log.debug("take_action(%s)", parsed_args) compute_client = self.app.client_manager.compute columns = ( + "Id", "Binary", "Host", "Zone", diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index 31b90c2..4e7cf10 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -183,25 +183,16 @@ class ShowUsage(show.ShowOne): )) info = {} - info['Servers'] = len(usage.server_usages) - info['RAM MB-Hours'] = float("%.2f" % usage.total_memory_mb_usage) - info['CPU Hours'] = float("%.2f" % usage.total_vcpus_usage) - info['Disk GB-Hours'] = float("%.2f" % usage.total_local_gb_usage) + info['Servers'] = ( + len(usage.server_usages) + if hasattr(usage, "server_usages") else None) + info['RAM MB-Hours'] = ( + float("%.2f" % usage.total_memory_mb_usage) + if hasattr(usage, "total_memory_mb_usage") else None) + info['CPU Hours'] = ( + float("%.2f" % usage.total_vcpus_usage) + if hasattr(usage, "total_vcpus_usage") else None) + info['Disk GB-Hours'] = ( + float("%.2f" % usage.total_local_gb_usage) + if hasattr(usage, "total_local_gb_usage") else None) return zip(*sorted(six.iteritems(info))) - - -# This is out of order due to the subclass, will eventually be removed - -class ListProjectUsage(ListUsage): - """List resource usage per project""" - - deprecated = True - - log = logging.getLogger('DEPRECATED:') - - def take_action(self, parsed_args): - self.log.warning( - "%s is deprecated, use 'usage list'", - getattr(self, 'cmd_name', 'this command'), - ) - return super(ListProjectUsage, self).take_action(parsed_args) diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index b8bb33f..bd882ce 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -21,7 +21,7 @@ from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_API_VERSION = '2' +DEFAULT_API_VERSION = '3' API_VERSION_OPTION = 'os_identity_api_version' API_NAME = 'identity' API_VERSIONS = { diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index 97a95f2..065f0ad 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -21,7 +21,7 @@ import six from cliff import command from cliff import lister from cliff import show -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc from openstackclient.common import parseractions from openstackclient.common import utils @@ -88,7 +88,7 @@ class CreateProject(show.ShowOne): enabled=enabled, **kwargs ) - except ksc_exc.Conflict as e: + except ks_exc.Conflict as e: if parsed_args.or_show: project = utils.find_resource( identity_client.tenants, @@ -264,7 +264,7 @@ class ShowProject(show.ShowOne): parsed_args.project, ) info.update(project._info) - except ksc_exc.Forbidden as e: + except ks_exc.Forbidden as e: auth_ref = self.app.client_manager.auth_ref if ( parsed_args.project == auth_ref.project_id or diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index e98f8cb..cab6b4a 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -21,7 +21,7 @@ import six from cliff import command from cliff import lister from cliff import show -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc from openstackclient.common import exceptions from openstackclient.common import utils @@ -98,7 +98,7 @@ class CreateRole(show.ShowOne): identity_client = self.app.client_manager.identity try: role = identity_client.roles.create(parsed_args.role_name) - except ksc_exc.Conflict as e: + except ks_exc.Conflict as e: if parsed_args.or_show: role = utils.find_resource( identity_client.roles, diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index 76902e6..e2b285b 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -21,7 +21,7 @@ import six from cliff import command from cliff import lister from cliff import show -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa @@ -104,7 +104,7 @@ class CreateUser(show.ShowOne): tenant_id=project_id, enabled=enabled, ) - except ksc_exc.Conflict as e: + except ks_exc.Conflict as e: if parsed_args.or_show: user = utils.find_resource( identity_client.users, @@ -373,7 +373,7 @@ class ShowUser(show.ShowOne): parsed_args.user, ) info.update(user._info) - except ksc_exc.Forbidden as e: + except ks_exc.Forbidden as e: auth_ref = self.app.client_manager.auth_ref if ( parsed_args.user == auth_ref.user_id or diff --git a/openstackclient/identity/v3/credential.py b/openstackclient/identity/v3/credential.py index dbd73e2..f22092d 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -17,7 +17,6 @@ import logging import six -import sys from cliff import command from cliff import lister @@ -130,17 +129,20 @@ class SetCredential(command.Command): parser.add_argument( '--user', metavar='<user>', + required=True, help='Name or ID of user that owns the credential', ) parser.add_argument( '--type', metavar='<type>', choices=['ec2', 'cert'], + required=True, help='New credential type', ) parser.add_argument( '--data', metavar='<data>', + required=True, help='New credential data', ) parser.add_argument( @@ -153,25 +155,22 @@ class SetCredential(command.Command): @utils.log_method(log) def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - kwargs = {} - if parsed_args.user: - user_id = utils.find_resource(identity_client.users, - parsed_args.user).id - if user_id: - kwargs['user'] = user_id - if parsed_args.type: - kwargs['type'] = parsed_args.type - if parsed_args.data: - kwargs['data'] = parsed_args.data + + user_id = utils.find_resource(identity_client.users, + parsed_args.user).id + if parsed_args.project: project = utils.find_resource(identity_client.projects, parsed_args.project).id - kwargs['project'] = project + else: + project = None + + identity_client.credentials.update(parsed_args.credential, + user=user_id, + type=parsed_args.type, + blob=parsed_args.data, + project=project) - if not kwargs: - sys.stdout.write("Credential not updated, no arguments present") - return - identity_client.credentials.update(parsed_args.credential, **kwargs) return diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index aec530a..8278a30 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -22,7 +22,7 @@ import sys from cliff import command from cliff import lister from cliff import show -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa @@ -77,7 +77,7 @@ class CreateDomain(show.ShowOne): description=parsed_args.description, enabled=enabled, ) - except ksc_exc.Conflict as e: + except ks_exc.Conflict as e: if parsed_args.or_show: domain = utils.find_resource(identity_client.domains, parsed_args.name) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index 8297506..d503a6b 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -22,7 +22,7 @@ import sys from cliff import command from cliff import lister from cliff import show -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa @@ -156,7 +156,7 @@ class CreateGroup(show.ShowOne): name=parsed_args.name, domain=domain, description=parsed_args.description) - except ksc_exc.Conflict as e: + except ks_exc.Conflict as e: if parsed_args.or_show: group = utils.find_resource(identity_client.groups, parsed_args.name, diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index 27982a9..4e086b6 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -88,6 +88,8 @@ class CreateIdentityProvider(show.ShowOne): enabled=parsed_args.enabled) idp._info.pop('links', None) + remote_ids = utils.format_list(idp._info.pop('remote_ids', [])) + idp._info['remote_ids'] = remote_ids return zip(*sorted(six.iteritems(idp._info))) @@ -142,6 +144,11 @@ class SetIdentityProvider(command.Command): metavar='<identity-provider>', help='Identity provider to modify', ) + parser.add_argument( + '--description', + metavar='<description>', + help='Set identity provider description', + ) identity_remote_id_provider = parser.add_mutually_exclusive_group() identity_remote_id_provider.add_argument( '--remote-id', @@ -174,8 +181,10 @@ class SetIdentityProvider(command.Command): federation_client = self.app.client_manager.identity.federation # Basic argument checking - if (not parsed_args.enable and not parsed_args.disable and not - parsed_args.remote_id and not parsed_args.remote_id_file): + if (not parsed_args.enable and not parsed_args.disable and + not parsed_args.remote_id and + not parsed_args.remote_id_file and + not parsed_args.description): self.log.error('No changes requested') return (None, None) @@ -190,6 +199,8 @@ class SetIdentityProvider(command.Command): # Setup keyword args for the client kwargs = {} + if parsed_args.description: + kwargs['description'] = parsed_args.description if parsed_args.enable: kwargs['enabled'] = True if parsed_args.disable: @@ -221,9 +232,11 @@ class ShowIdentityProvider(show.ShowOne): @utils.log_method(log) def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - identity_provider = utils.find_resource( + idp = utils.find_resource( identity_client.federation.identity_providers, parsed_args.identity_provider) - identity_provider._info.pop('links', None) - return zip(*sorted(six.iteritems(identity_provider._info))) + idp._info.pop('links', None) + remote_ids = utils.format_list(idp._info.pop('remote_ids', [])) + idp._info['remote_ids'] = remote_ids + return zip(*sorted(six.iteritems(idp._info))) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index 96d7f97..f87105d 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -21,7 +21,7 @@ import six from cliff import command from cliff import lister from cliff import show -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc from openstackclient.common import parseractions from openstackclient.common import utils @@ -113,7 +113,7 @@ class CreateProject(show.ShowOne): enabled=enabled, **kwargs ) - except ksc_exc.Conflict as e: + except ks_exc.Conflict as e: if parsed_args.or_show: project = utils.find_resource(identity_client.projects, parsed_args.name, diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py index eb4c084..1ff0b8c 100644 --- a/openstackclient/identity/v3/region.py +++ b/openstackclient/identity/v3/region.py @@ -48,12 +48,6 @@ class CreateRegion(show.ShowOne): metavar='<description>', help=_('New region description'), ) - parser.add_argument( - '--url', - metavar='<url>', - help=_('New region url'), - ) - return parser @utils.log_method(log) @@ -62,7 +56,6 @@ class CreateRegion(show.ShowOne): region = identity_client.regions.create( id=parsed_args.region, - url=parsed_args.url, parent_region=parsed_args.parent_region, description=parsed_args.description, ) @@ -117,8 +110,8 @@ class ListRegion(lister.Lister): if parsed_args.parent_region: kwargs['parent_region_id'] = parsed_args.parent_region - columns_headers = ('Region', 'Parent Region', 'Description', 'URL') - columns = ('ID', 'Parent Region Id', 'Description', 'URL') + columns_headers = ('Region', 'Parent Region', 'Description') + columns = ('ID', 'Parent Region Id', 'Description') data = identity_client.regions.list(**kwargs) return (columns_headers, @@ -150,25 +143,16 @@ class SetRegion(command.Command): metavar='<description>', help=_('New region description'), ) - parser.add_argument( - '--url', - metavar='<url>', - help=_('New region url'), - ) return parser @utils.log_method(log) def take_action(self, parsed_args): identity_client = self.app.client_manager.identity - if (not parsed_args.url - and not parsed_args.parent_region - and not parsed_args.description): + if not parsed_args.parent_region and not parsed_args.description: return kwargs = {} - if parsed_args.url: - kwargs['url'] = parsed_args.url if parsed_args.description: kwargs['description'] = parsed_args.description if parsed_args.parent_region: diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index c72de47..66f1896 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -22,7 +22,7 @@ import sys from cliff import command from cliff import lister from cliff import show -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa @@ -172,7 +172,7 @@ class CreateRole(show.ShowOne): try: role = identity_client.roles.create(name=parsed_args.name) - except ksc_exc.Conflict as e: + except ks_exc.Conflict as e: if parsed_args.or_show: role = utils.find_resource(identity_client.roles, parsed_args.name) @@ -260,6 +260,7 @@ class ListRole(lister.Lister): data = identity_client.roles.list( user=user, domain=domain, + os_inherit_extension_inherited=parsed_args.inherited ) for user_role in data: user_role.user = user.name @@ -269,6 +270,7 @@ class ListRole(lister.Lister): data = identity_client.roles.list( user=user, project=project, + os_inherit_extension_inherited=parsed_args.inherited ) for user_role in data: user_role.user = user.name @@ -278,12 +280,14 @@ class ListRole(lister.Lister): data = identity_client.roles.list( user=user, domain='default', + os_inherit_extension_inherited=parsed_args.inherited ) elif parsed_args.group and parsed_args.domain: columns = ('ID', 'Name', 'Domain', 'Group') data = identity_client.roles.list( group=group, domain=domain, + os_inherit_extension_inherited=parsed_args.inherited ) for group_role in data: group_role.group = group.name @@ -293,6 +297,7 @@ class ListRole(lister.Lister): data = identity_client.roles.list( group=group, project=project, + os_inherit_extension_inherited=parsed_args.inherited ) for group_role in data: group_role.group = group.name diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 0e89454..eaef8f0 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -22,7 +22,7 @@ import six from cliff import command from cliff import lister from cliff import show -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc from openstackclient.common import utils from openstackclient.i18n import _ # noqa @@ -122,7 +122,7 @@ class CreateUser(show.ShowOne): description=parsed_args.description, enabled=enabled ) - except ksc_exc.Conflict as e: + except ks_exc.Conflict as e: if parsed_args.or_show: user = utils.find_resource(identity_client.users, parsed_args.name, diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 68c81cd..4ebc8f9 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -213,6 +213,8 @@ class CreateImage(show.ShowOne): if parsed_args.private: kwargs['is_public'] = False + info = {} + if not parsed_args.location and not parsed_args.copy_from: if parsed_args.volume: volume_client = self.app.client_manager.volume @@ -241,18 +243,18 @@ class CreateImage(show.ShowOne): # do a chunked transfer kwargs["data"] = sys.stdin - # Wrap the call to catch exceptions in order to close files - try: - image = image_client.images.create(**kwargs) - finally: - # Clean up open files - make sure data isn't a string - if ('data' in kwargs and hasattr(kwargs['data'], 'close') and - kwargs['data'] != sys.stdin): - kwargs['data'].close() - - info = {} - info.update(image._info) - info['properties'] = utils.format_dict(info.get('properties', {})) + if not parsed_args.volume: + # Wrap the call to catch exceptions in order to close files + try: + image = image_client.images.create(**kwargs) + finally: + # Clean up open files - make sure data isn't a string + if ('data' in kwargs and hasattr(kwargs['data'], 'close') and + kwargs['data'] != sys.stdin): + kwargs['data'].close() + + info.update(image._info) + info['properties'] = utils.format_dict(info.get('properties', {})) return zip(*sorted(six.iteritems(info))) @@ -352,7 +354,12 @@ class ListImage(lister.Lister): kwargs['public'] = True if parsed_args.private: kwargs['private'] = True - kwargs['detailed'] = bool(parsed_args.property or parsed_args.long) + # Note: We specifically need to do that below to get the 'status' + # column. + # + # Always set kwargs['detailed'] to True, and then filter the columns + # according to whether the --long option is specified or not. + kwargs['detailed'] = True if parsed_args.long: columns = ( @@ -380,7 +387,7 @@ class ListImage(lister.Lister): 'Properties', ) else: - columns = ("ID", "Name") + columns = ("ID", "Name", "Status") column_headers = columns # List of image data received @@ -452,7 +459,7 @@ class SaveImage(command.Command): gc_utils.save_image(data, parsed_args.file) -class SetImage(show.ShowOne): +class SetImage(command.Command): """Set image properties""" log = logging.getLogger(__name__ + ".SetImage") @@ -629,7 +636,7 @@ class SetImage(show.ShowOne): volume_client.volumes, parsed_args.volume, ) - response, body = volume_client.volumes.upload_to_image( + volume_client.volumes.upload_to_image( source_volume.id, parsed_args.force, parsed_args.image, @@ -640,7 +647,6 @@ class SetImage(show.ShowOne): if parsed_args.disk_format else image.disk_format), ) - info = body['os-volume_upload_image'] elif parsed_args.file: # Send an open file handle to glanceclient so it will # do a chunked transfer @@ -673,10 +679,7 @@ class SetImage(show.ShowOne): kwargs['data'] != sys.stdin): kwargs['data'].close() - info = {} - info.update(image._info) - info['properties'] = utils.format_dict(info.get('properties', {})) - return zip(*sorted(six.iteritems(info))) + return class ShowImage(show.ShowOne): diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index fff26c0..c0fb5b5 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -57,8 +57,7 @@ def _format_image(image): properties[key] = image.get(key) # format the tags if they are there - if image.get('tags'): - info['tags'] = utils.format_list(image.get('tags')) + info['tags'] = utils.format_list(image.get('tags')) # add properties back into the dictionary as a top-level key if properties: @@ -106,14 +105,14 @@ class AddProjectToImage(show.ShowOne): project_id, ) - return zip(*sorted(six.iteritems(image_member._info))) + return zip(*sorted(six.iteritems(image_member))) class CreateImage(show.ShowOne): """Create/upload an image""" log = logging.getLogger(__name__ + ".CreateImage") - deadopts = ('owner', 'size', 'location', 'copy-from', 'checksum', 'store') + deadopts = ('size', 'location', 'copy-from', 'checksum', 'store') def get_parser(self, prog_name): parser = super(CreateImage, self).get_parser(prog_name) @@ -121,7 +120,6 @@ class CreateImage(show.ShowOne): # TODO(bunting): There are additional arguments that v1 supported # that v2 either doesn't support or supports weirdly. # --checksum - could be faked clientside perhaps? - # --owner - could be set as an update after the put? # --location - maybe location add? # --size - passing image size is actually broken in python-glanceclient # --copy-from - does not exist in v2 @@ -151,6 +149,11 @@ class CreateImage(show.ShowOne): "(default: %s)" % DEFAULT_DISK_FORMAT, ) parser.add_argument( + "--owner", + metavar="<owner>", + help="Image owner project name or ID", + ) + parser.add_argument( "--min-disk", metavar="<disk-gb>", type=int, @@ -210,7 +213,7 @@ class CreateImage(show.ShowOne): "--%s" % deadopt, metavar="<%s>" % deadopt, dest=deadopt.replace('-', '_'), - help=argparse.SUPPRESS + help=argparse.SUPPRESS, ) return parser @@ -230,7 +233,7 @@ class CreateImage(show.ShowOne): copy_attrs = ('name', 'id', 'container_format', 'disk_format', 'min_disk', 'min_ram', - 'tags') + 'tags', 'owner') for attr in copy_attrs: if attr in parsed_args: val = getattr(parsed_args, attr, None) @@ -414,7 +417,7 @@ class ListImage(lister.Lister): 'Tags', ) else: - columns = ("ID", "Name") + columns = ("ID", "Name", "Status") column_headers = columns # List of image data received @@ -522,38 +525,11 @@ class SaveImage(command.Command): gc_utils.save_image(data, parsed_args.file) -class ShowImage(show.ShowOne): - """Display image details""" - - log = logging.getLogger(__name__ + ".ShowImage") - - def get_parser(self, prog_name): - parser = super(ShowImage, self).get_parser(prog_name) - parser.add_argument( - "image", - metavar="<image>", - help="Image to display (name or ID)", - ) - return parser - - def take_action(self, parsed_args): - self.log.debug("take_action(%s)", parsed_args) - - image_client = self.app.client_manager.image - image = utils.find_resource( - image_client.images, - parsed_args.image, - ) - - info = _format_image(image) - return zip(*sorted(six.iteritems(info))) - - -class SetImage(show.ShowOne): +class SetImage(command.Command): """Set image properties""" log = logging.getLogger(__name__ + ".SetImage") - deadopts = ('size', 'store', 'location', 'copy-from', 'checksum') + deadopts = ('visibility',) def get_parser(self, prog_name): parser = super(SetImage, self).get_parser(prog_name) @@ -567,8 +543,6 @@ class SetImage(show.ShowOne): # --force - needs adding # --checksum - maybe could be done client side # --stdin - could be implemented - # --property - needs adding - # --tags - needs adding parser.add_argument( "image", metavar="<image>", @@ -580,20 +554,44 @@ class SetImage(show.ShowOne): help="New image name" ) parser.add_argument( - "--architecture", - metavar="<architecture>", - help="Operating system Architecture" + "--owner", + metavar="<project>", + help="New image owner project (name or ID)", + ) + parser.add_argument( + "--min-disk", + type=int, + metavar="<disk-gb>", + help="Minimum disk size needed to boot image, in gigabytes" + ) + parser.add_argument( + "--min-ram", + type=int, + metavar="<ram-mb>", + help="Minimum RAM size needed to boot image, in megabytes", + ) + parser.add_argument( + "--container-format", + metavar="<container-format>", + help="Image container format " + "(default: %s)" % DEFAULT_CONTAINER_FORMAT, + ) + parser.add_argument( + "--disk-format", + metavar="<disk-format>", + help="Image disk format " + "(default: %s)" % DEFAULT_DISK_FORMAT, ) protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( "--protected", action="store_true", - help="Prevent image from being deleted" + help="Prevent image from being deleted", ) protected_group.add_argument( "--unprotected", action="store_true", - help="Allow image to be deleted (default)" + help="Allow image to be deleted (default)", ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( @@ -607,82 +605,64 @@ class SetImage(show.ShowOne): help="Image is inaccessible to the public (default)", ) parser.add_argument( - "--instance-uuid", - metavar="<instance_uuid>", - help="ID of instance used to create this image" + "--property", + dest="properties", + metavar="<key=value>", + action=parseractions.KeyValueAction, + help="Set a property on this image " + "(repeat option to set multiple properties)", ) parser.add_argument( - "--min-disk", - type=int, - metavar="<disk-gb>", - help="Minimum disk size needed to boot image, in gigabytes" + "--tag", + dest="tags", + metavar="<tag>", + default=[], + action='append', + help="Set a tag on this image " + "(repeat option to set multiple tags)", ) - visibility_choices = ["public", "private"] - public_group.add_argument( - "--visibility", - metavar="<visibility>", - choices=visibility_choices, - help=argparse.SUPPRESS + parser.add_argument( + "--architecture", + metavar="<architecture>", + help="Operating system architecture", ) - help_msg = ("ID of image in Glance that should be used as the kernel" - " when booting an AMI-style image") parser.add_argument( - "--kernel-id", - metavar="<kernel-id>", - help=help_msg + "--instance-id", + metavar="<instance-id>", + help="ID of server instance used to create this image", ) parser.add_argument( - "--os-version", - metavar="<os-version>", - help="Operating system version as specified by the distributor" + "--instance-uuid", + metavar="<instance-id>", + dest="instance_id", + help=argparse.SUPPRESS, ) - disk_choices = ["None", "ami", "ari", "aki", "vhd", "vmdk", "raw", - "qcow2", "vdi", "iso"] - help_msg = ("Format of the disk. Valid values: %s" % disk_choices) parser.add_argument( - "--disk-format", - metavar="<disk-format>", - choices=disk_choices, - help=help_msg + "--kernel-id", + metavar="<kernel-id>", + help="ID of kernel image used to boot this disk image", ) parser.add_argument( "--os-distro", metavar="<os-distro>", - help="Common name of operating system distribution" + help="Operating system distribution name", ) parser.add_argument( - "--owner", - metavar="<owner>", - help="New Owner of the image" + "--os-version", + metavar="<os-version>", + help="Operating system distribution version", ) - msg = ("ID of image stored in Glance that should be used as the " - "ramdisk when booting an AMI-style image") parser.add_argument( "--ramdisk-id", metavar="<ramdisk-id>", - help=msg - ) - parser.add_argument( - "--min-ram", - type=int, - metavar="<ram-mb>", - help="Amount of RAM (in MB) required to boot image" - ) - container_choices = ["None", "ami", "ari", "aki", "bare", "ovf", "ova"] - help_msg = ("Format of the container. Valid values: %s" - % container_choices) - parser.add_argument( - "--container-format", - metavar="<container-format>", - choices=container_choices, - help=help_msg + help="ID of ramdisk image used to boot this disk image", ) for deadopt in self.deadopts: parser.add_argument( "--%s" % deadopt, metavar="<%s>" % deadopt, dest=deadopt.replace('-', '_'), - help=argparse.SUPPRESS + help=argparse.SUPPRESS, ) return parser @@ -698,10 +678,9 @@ class SetImage(show.ShowOne): kwargs = {} copy_attrs = ('architecture', 'container_format', 'disk_format', - 'file', 'kernel_id', 'locations', 'name', + 'file', 'instance_id', 'kernel_id', 'locations', 'min_disk', 'min_ram', 'name', 'os_distro', 'os_version', - 'owner', 'prefix', 'progress', 'ramdisk_id', - 'visibility') + 'owner', 'prefix', 'progress', 'ramdisk_id', 'tags') for attr in copy_attrs: if attr in parsed_args: val = getattr(parsed_args, attr, None) @@ -710,6 +689,11 @@ class SetImage(show.ShowOne): # actually present on the command line kwargs[attr] = val + # Properties should get flattened into the general kwargs + if getattr(parsed_args, 'properties', None): + for k, v in six.iteritems(parsed_args.properties): + kwargs[k] = str(v) + # Handle exclusive booleans with care # Avoid including attributes in kwargs if an option is not # present on the command line. These exclusive booleans are not @@ -732,7 +716,35 @@ class SetImage(show.ShowOne): image = utils.find_resource( image_client.images, parsed_args.image) + if parsed_args.tags: + # Tags should be extended, but duplicates removed + kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags))) + image = image_client.images.update(image.id, **kwargs) - info = {} - info.update(image) + + +class ShowImage(show.ShowOne): + """Display image details""" + + log = logging.getLogger(__name__ + ".ShowImage") + + def get_parser(self, prog_name): + parser = super(ShowImage, self).get_parser(prog_name) + parser.add_argument( + "image", + metavar="<image>", + help="Image to display (name or ID)", + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + image_client = self.app.client_manager.image + image = utils.find_resource( + image_client.images, + parsed_args.image, + ) + + info = _format_image(image) return zip(*sorted(six.iteritems(info))) diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index 5f72782..69ed11f 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -18,10 +18,11 @@ from openstackclient.common import utils LOG = logging.getLogger(__name__) -DEFAULT_API_VERSION = '2' +DEFAULT_API_VERSION = '2.0' API_VERSION_OPTION = 'os_network_api_version' API_NAME = "network" API_VERSIONS = { + "2.0": "neutronclient.v2_0.client.Client", "2": "neutronclient.v2_0.client.Client", } # Translate our API version to auth plugin version prefix @@ -32,6 +33,7 @@ API_VERSION_MAP = { NETWORK_API_TYPE = 'network' NETWORK_API_VERSIONS = { + '2.0': 'openstackclient.api.network_v2.APIv2', '2': 'openstackclient.api.network_v2.APIv2', } diff --git a/openstackclient/network/common.py b/openstackclient/network/common.py index bd6203b..31faef2 100644 --- a/openstackclient/network/common.py +++ b/openstackclient/network/common.py @@ -27,7 +27,8 @@ def find(client, resource, resources, name_or_id, name_attr='name'): n = find(netclient, 'network', 'networks', 'matrix') """ list_method = getattr(client, "list_%s" % resources) - # Search for by name + + # Search by name kwargs = {name_attr: name_or_id, 'fields': 'id'} data = list_method(**kwargs) info = data[resources] @@ -36,7 +37,8 @@ def find(client, resource, resources, name_or_id, name_attr='name'): if len(info) > 1: msg = "More than one %s exists with the name '%s'." raise exceptions.CommandError(msg % (resource, name_or_id)) - # Search for by id + + # Search by id data = list_method(id=name_or_id, fields='id') info = data[resources] if len(info) == 1: diff --git a/openstackclient/object/v1/account.py b/openstackclient/object/v1/account.py new file mode 100644 index 0000000..aa94ff5 --- /dev/null +++ b/openstackclient/object/v1/account.py @@ -0,0 +1,85 @@ +# 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. +# + +"""Account v1 action implementations""" + +import logging + +from cliff import command +from cliff import show +import six + +from openstackclient.common import parseractions +from openstackclient.common import utils + + +class SetAccount(command.Command): + """Set account properties""" + + log = logging.getLogger(__name__ + '.SetAccount') + + def get_parser(self, prog_name): + parser = super(SetAccount, self).get_parser(prog_name) + parser.add_argument( + "--property", + metavar="<key=value>", + required=True, + action=parseractions.KeyValueAction, + help="Set a property on this account " + "(repeat option to set multiple properties)" + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.account_set( + properties=parsed_args.property, + ) + + +class ShowAccount(show.ShowOne): + """Display account details""" + + log = logging.getLogger(__name__ + '.ShowAccount') + + @utils.log_method(log) + def take_action(self, parsed_args): + data = self.app.client_manager.object_store.account_show() + if 'properties' in data: + data['properties'] = utils.format_dict(data.pop('properties')) + return zip(*sorted(six.iteritems(data))) + + +class UnsetAccount(command.Command): + """Unset account properties""" + + log = logging.getLogger(__name__ + '.UnsetAccount') + + def get_parser(self, prog_name): + parser = super(UnsetAccount, self).get_parser(prog_name) + parser.add_argument( + '--property', + metavar='<key>', + required=True, + action='append', + default=[], + help='Property to remove from account ' + '(repeat option to remove multiple properties)', + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.account_unset( + properties=parsed_args.property, + ) diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index 49173de..8c8844e 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -23,6 +23,7 @@ from cliff import command from cliff import lister from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -178,6 +179,36 @@ class SaveContainer(command.Command): ) +class SetContainer(command.Command): + """Set container properties""" + + log = logging.getLogger(__name__ + '.SetContainer') + + def get_parser(self, prog_name): + parser = super(SetContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Container to modify', + ) + parser.add_argument( + "--property", + metavar="<key=value>", + required=True, + action=parseractions.KeyValueAction, + help="Set a property on this container " + "(repeat option to set multiple properties)" + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.container_set( + parsed_args.container, + properties=parsed_args.property, + ) + + class ShowContainer(show.ShowOne): """Display container details""" @@ -198,5 +229,38 @@ class ShowContainer(show.ShowOne): data = self.app.client_manager.object_store.container_show( container=parsed_args.container, ) + if 'properties' in data: + data['properties'] = utils.format_dict(data.pop('properties')) return zip(*sorted(six.iteritems(data))) + + +class UnsetContainer(command.Command): + """Unset container properties""" + + log = logging.getLogger(__name__ + '.UnsetContainer') + + def get_parser(self, prog_name): + parser = super(UnsetContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Container to modify', + ) + parser.add_argument( + '--property', + metavar='<key>', + required=True, + action='append', + default=[], + help='Property to remove from container ' + '(repeat option to remove multiple properties)', + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.container_unset( + parsed_args.container, + properties=parsed_args.property, + ) diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index c90f031..4bd0612 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -23,6 +23,7 @@ from cliff import command from cliff import lister from cliff import show +from openstackclient.common import parseractions from openstackclient.common import utils @@ -221,6 +222,42 @@ class SaveObject(command.Command): ) +class SetObject(command.Command): + """Set object properties""" + + log = logging.getLogger(__name__ + '.SetObject') + + def get_parser(self, prog_name): + parser = super(SetObject, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Modify <object> from <container>', + ) + parser.add_argument( + 'object', + metavar='<object>', + help='Object to modify', + ) + parser.add_argument( + "--property", + metavar="<key=value>", + required=True, + action=parseractions.KeyValueAction, + help="Set a property on this object " + "(repeat option to set multiple properties)" + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.object_set( + parsed_args.container, + parsed_args.object, + properties=parsed_args.property, + ) + + class ShowObject(show.ShowOne): """Display object details""" @@ -247,5 +284,44 @@ class ShowObject(show.ShowOne): container=parsed_args.container, object=parsed_args.object, ) + if 'properties' in data: + data['properties'] = utils.format_dict(data.pop('properties')) return zip(*sorted(six.iteritems(data))) + + +class UnsetObject(command.Command): + """Unset object properties""" + + log = logging.getLogger(__name__ + '.UnsetObject') + + def get_parser(self, prog_name): + parser = super(UnsetObject, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='<container>', + help='Modify <object> from <container>', + ) + parser.add_argument( + 'object', + metavar='<object>', + help='Object to modify', + ) + parser.add_argument( + '--property', + metavar='<key>', + required=True, + action='append', + default=[], + help='Property to remove from object ' + '(repeat option to remove multiple properties)', + ) + return parser + + @utils.log_method(log) + def take_action(self, parsed_args): + self.app.client_manager.object_store.object_unset( + parsed_args.container, + parsed_args.object, + properties=parsed_args.property, + ) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 5b36b8b..5b4939a 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -25,12 +25,13 @@ from cliff import app from cliff import command from cliff import complete from cliff import help +from oslo_utils import strutils import openstackclient from openstackclient.common import clientmanager from openstackclient.common import commandmanager -from openstackclient.common import context from openstackclient.common import exceptions as exc +from openstackclient.common import logs from openstackclient.common import timing from openstackclient.common import utils @@ -102,7 +103,7 @@ class OpenStackShell(app.App): def configure_logging(self): """Configure logging for the app.""" - self.log_configurator = context.LogConfigurator(self.options) + self.log_configurator = logs.LogConfigurator(self.options) self.dump_stack_trace = self.log_configurator.dump_trace def run(self, argv): @@ -201,8 +202,10 @@ class OpenStackShell(app.App): # Parent __init__ parses argv into self.options super(OpenStackShell, self).initialize_app(argv) - self.log.info("START with options: %s", self.command_options) - self.log.debug("options: %s", self.options) + self.log.info("START with options: %s", + strutils.mask_password(self.command_options)) + self.log.debug("options: %s", + strutils.mask_password(self.options)) # Set the default plugin to token_endpoint if url and token are given if (self.options.url and self.options.token): @@ -238,6 +241,10 @@ class OpenStackShell(app.App): }, ) + # TODO(thowe): Change cliff so the default value for debug + # can be set to None. + if not self.options.debug: + self.options.debug = None self.cloud = cc.get_one_cloud( cloud=self.options.cloud, argparse=self.options, @@ -246,7 +253,8 @@ class OpenStackShell(app.App): self.log_configurator.configure(self.cloud) self.dump_stack_trace = self.log_configurator.dump_trace self.log.debug("defaults: %s", cc.defaults) - self.log.debug("cloud cfg: %s", self.cloud.config) + self.log.debug("cloud cfg: %s", + strutils.mask_password(self.cloud.config)) # Set up client TLS # NOTE(dtroyer): --insecure is the non-default condition that @@ -272,7 +280,7 @@ class OpenStackShell(app.App): for mod in clientmanager.PLUGIN_MODULES: default_version = getattr(mod, 'DEFAULT_API_VERSION', None) option = mod.API_VERSION_OPTION.replace('os_', '') - version_opt = self.cloud.config.get(option, default_version) + version_opt = str(self.cloud.config.get(option, default_version)) if version_opt: api = mod.API_NAME self.api_version[api] = version_opt diff --git a/openstackclient/tests/api/fakes.py b/openstackclient/tests/api/fakes.py index 85617ab..e285a61 100644 --- a/openstackclient/tests/api/fakes.py +++ b/openstackclient/tests/api/fakes.py @@ -15,7 +15,7 @@ from requests_mock.contrib import fixture -from keystoneclient import session +from keystoneauth1 import session from openstackclient.tests import utils diff --git a/openstackclient/tests/api/test_image_v1.py b/openstackclient/tests/api/test_image_v1.py index 34fcfca..f347975 100644 --- a/openstackclient/tests/api/test_image_v1.py +++ b/openstackclient/tests/api/test_image_v1.py @@ -15,7 +15,7 @@ from requests_mock.contrib import fixture -from keystoneclient import session +from keystoneauth1 import session from openstackclient.api import image_v1 from openstackclient.tests import utils diff --git a/openstackclient/tests/api/test_image_v2.py b/openstackclient/tests/api/test_image_v2.py index ddb160e..7706399 100644 --- a/openstackclient/tests/api/test_image_v2.py +++ b/openstackclient/tests/api/test_image_v2.py @@ -15,7 +15,7 @@ from requests_mock.contrib import fixture -from keystoneclient import session +from keystoneauth1 import session from openstackclient.api import image_v2 from openstackclient.tests import utils diff --git a/openstackclient/tests/api/test_network_v2.py b/openstackclient/tests/api/test_network_v2.py index 13c5d6e..80f1d9d 100644 --- a/openstackclient/tests/api/test_network_v2.py +++ b/openstackclient/tests/api/test_network_v2.py @@ -15,7 +15,7 @@ from requests_mock.contrib import fixture -from keystoneclient import session +from keystoneauth1 import session from openstackclient.api import network_v2 as network from openstackclient.tests import utils diff --git a/openstackclient/tests/api/test_object_store_v1.py b/openstackclient/tests/api/test_object_store_v1.py index b18a003..992bf26 100644 --- a/openstackclient/tests/api/test_object_store_v1.py +++ b/openstackclient/tests/api/test_object_store_v1.py @@ -17,7 +17,7 @@ import mock from requests_mock.contrib import fixture -from keystoneclient import session +from keystoneauth1 import session from openstackclient.api import object_store_v1 as object_store from openstackclient.tests import utils @@ -157,10 +157,6 @@ class TestContainer(TestObjectAPIv1): 'container': 'qaz', 'object_count': '1', 'bytes_used': '577', - 'read_acl': None, - 'write_acl': None, - 'sync_to': None, - 'sync_key': None, } self.requests_mock.register_uri( 'HEAD', @@ -323,8 +319,7 @@ class TestObject(TestObjectAPIv1): 'content-length': '577', 'last-modified': '20130101', 'etag': 'qaz', - 'wife': 'Wilma', - 'x-tra-header': 'yabba-dabba-do', + 'properties': {'wife': 'Wilma'}, } self.requests_mock.register_uri( 'HEAD', diff --git a/openstackclient/tests/common/test_context.py b/openstackclient/tests/common/test_logs.py index 55e4285..fe054a3 100644 --- a/openstackclient/tests/common/test_context.py +++ b/openstackclient/tests/common/test_logs.py @@ -14,7 +14,7 @@ import logging import mock -from openstackclient.common import context +from openstackclient.common import logs from openstackclient.tests import utils @@ -23,51 +23,51 @@ class TestContext(utils.TestCase): def test_log_level_from_options(self): opts = mock.Mock() opts.verbose_level = 0 - self.assertEqual(logging.ERROR, context.log_level_from_options(opts)) + self.assertEqual(logging.ERROR, logs.log_level_from_options(opts)) opts.verbose_level = 1 - self.assertEqual(logging.WARNING, context.log_level_from_options(opts)) + self.assertEqual(logging.WARNING, logs.log_level_from_options(opts)) opts.verbose_level = 2 - self.assertEqual(logging.INFO, context.log_level_from_options(opts)) + self.assertEqual(logging.INFO, logs.log_level_from_options(opts)) opts.verbose_level = 3 - self.assertEqual(logging.DEBUG, context.log_level_from_options(opts)) + self.assertEqual(logging.DEBUG, logs.log_level_from_options(opts)) def test_log_level_from_config(self): cfg = {'verbose_level': 0} - self.assertEqual(logging.ERROR, context.log_level_from_config(cfg)) + self.assertEqual(logging.ERROR, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1} - self.assertEqual(logging.WARNING, context.log_level_from_config(cfg)) + self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 2} - self.assertEqual(logging.INFO, context.log_level_from_config(cfg)) + self.assertEqual(logging.INFO, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 3} - self.assertEqual(logging.DEBUG, context.log_level_from_config(cfg)) + self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'critical'} - self.assertEqual(logging.CRITICAL, context.log_level_from_config(cfg)) + self.assertEqual(logging.CRITICAL, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'error'} - self.assertEqual(logging.ERROR, context.log_level_from_config(cfg)) + self.assertEqual(logging.ERROR, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'warning'} - self.assertEqual(logging.WARNING, context.log_level_from_config(cfg)) + self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'info'} - self.assertEqual(logging.INFO, context.log_level_from_config(cfg)) + self.assertEqual(logging.INFO, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'debug'} - self.assertEqual(logging.DEBUG, context.log_level_from_config(cfg)) + self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'bogus'} - self.assertEqual(logging.WARNING, context.log_level_from_config(cfg)) + self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) cfg = {'verbose_level': 1, 'log_level': 'info', 'debug': True} - self.assertEqual(logging.DEBUG, context.log_level_from_config(cfg)) + self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) @mock.patch('warnings.simplefilter') def test_set_warning_filter(self, simplefilter): - context.set_warning_filter(logging.ERROR) + logs.set_warning_filter(logging.ERROR) simplefilter.assert_called_with("ignore") - context.set_warning_filter(logging.WARNING) + logs.set_warning_filter(logging.WARNING) simplefilter.assert_called_with("ignore") - context.set_warning_filter(logging.INFO) + logs.set_warning_filter(logging.INFO) simplefilter.assert_called_with("once") class TestFileFormatter(utils.TestCase): def test_nothing(self): - formatter = context._FileFormatter() + formatter = logs._FileFormatter() self.assertEqual(('%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' '%(name)s %(message)s'), formatter.fmt) @@ -77,7 +77,7 @@ class TestFileFormatter(utils.TestCase): os_project_name = 'projecty' username = 'usernamey' options = Opts() - formatter = context._FileFormatter(options=options) + formatter = logs._FileFormatter(options=options) self.assertEqual(('%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' '%(name)s [cloudy usernamey projecty] %(message)s'), formatter.fmt) @@ -86,7 +86,7 @@ class TestFileFormatter(utils.TestCase): config = mock.Mock() config.config = {'cloud': 'cloudy'} config.auth = {'project_name': 'projecty', 'username': 'usernamey'} - formatter = context._FileFormatter(config=config) + formatter = logs._FileFormatter(config=config) self.assertEqual(('%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' '%(name)s [cloudy usernamey projecty] %(message)s'), formatter.fmt) @@ -119,7 +119,7 @@ class TestLogConfigurator(utils.TestCase): @mock.patch('logging.StreamHandler') @mock.patch('logging.getLogger') - @mock.patch('openstackclient.common.context.set_warning_filter') + @mock.patch('openstackclient.common.logs.set_warning_filter') def test_init(self, warning_filter, getLogger, handle): getLogger.side_effect = self.loggers console_logger = mock.Mock() @@ -127,7 +127,7 @@ class TestLogConfigurator(utils.TestCase): console_logger.setLevel = mock.Mock() handle.return_value = console_logger - configurator = context.LogConfigurator(self.options) + configurator = logs.LogConfigurator(self.options) getLogger.assert_called_with('iso8601') # last call warning_filter.assert_called_with(logging.WARNING) @@ -140,12 +140,12 @@ class TestLogConfigurator(utils.TestCase): self.assertEqual(False, configurator.dump_trace) @mock.patch('logging.getLogger') - @mock.patch('openstackclient.common.context.set_warning_filter') + @mock.patch('openstackclient.common.logs.set_warning_filter') def test_init_no_debug(self, warning_filter, getLogger): getLogger.side_effect = self.loggers self.options.debug = True - configurator = context.LogConfigurator(self.options) + configurator = logs.LogConfigurator(self.options) warning_filter.assert_called_with(logging.DEBUG) self.requests_log.setLevel.assert_called_with(logging.DEBUG) @@ -153,8 +153,8 @@ class TestLogConfigurator(utils.TestCase): @mock.patch('logging.FileHandler') @mock.patch('logging.getLogger') - @mock.patch('openstackclient.common.context.set_warning_filter') - @mock.patch('openstackclient.common.context._FileFormatter') + @mock.patch('openstackclient.common.logs.set_warning_filter') + @mock.patch('openstackclient.common.logs._FileFormatter') def test_init_log_file(self, formatter, warning_filter, getLogger, handle): getLogger.side_effect = self.loggers self.options.log_file = '/tmp/log_file' @@ -165,7 +165,7 @@ class TestLogConfigurator(utils.TestCase): mock_formatter = mock.Mock() formatter.return_value = mock_formatter - context.LogConfigurator(self.options) + logs.LogConfigurator(self.options) handle.assert_called_with(filename=self.options.log_file) self.root_logger.addHandler.assert_called_with(file_logger) @@ -174,11 +174,11 @@ class TestLogConfigurator(utils.TestCase): @mock.patch('logging.FileHandler') @mock.patch('logging.getLogger') - @mock.patch('openstackclient.common.context.set_warning_filter') - @mock.patch('openstackclient.common.context._FileFormatter') + @mock.patch('openstackclient.common.logs.set_warning_filter') + @mock.patch('openstackclient.common.logs._FileFormatter') def test_configure(self, formatter, warning_filter, getLogger, handle): getLogger.side_effect = self.loggers - configurator = context.LogConfigurator(self.options) + configurator = logs.LogConfigurator(self.options) cloud_config = mock.Mock() config_log = '/tmp/config_log' cloud_config.config = { diff --git a/openstackclient/tests/common/test_parseractions.py b/openstackclient/tests/common/test_parseractions.py index 8afcb63..b75c481 100644 --- a/openstackclient/tests/common/test_parseractions.py +++ b/openstackclient/tests/common/test_parseractions.py @@ -102,3 +102,58 @@ class TestKeyValueAction(utils.TestCase): expect = {'green': '100%'} self.assertDictEqual(expect, actual) self.assertEqual(None, failhere) + + +class TestNonNegativeAction(utils.TestCase): + def test_negative_values(self): + parser = argparse.ArgumentParser() + + # Set up our typical usage + parser.add_argument( + '--foo', + metavar='<foo>', + type=int, + action=parseractions.NonNegativeAction, + ) + + self.assertRaises( + argparse.ArgumentTypeError, + parser.parse_args, + "--foo -1".split() + ) + + def test_zero_values(self): + parser = argparse.ArgumentParser() + + # Set up our typical usage + parser.add_argument( + '--foo', + metavar='<foo>', + type=int, + action=parseractions.NonNegativeAction, + ) + + results = parser.parse_args( + '--foo 0'.split() + ) + + actual = getattr(results, 'foo', None) + self.assertEqual(actual, 0) + + def test_positive_values(self): + parser = argparse.ArgumentParser() + + # Set up our typical usage + parser.add_argument( + '--foo', + metavar='<foo>', + type=int, + action=parseractions.NonNegativeAction, + ) + + results = parser.parse_args( + '--foo 1'.split() + ) + + actual = getattr(results, 'foo', None) + self.assertEqual(actual, 1) diff --git a/openstackclient/tests/common/test_quota.py b/openstackclient/tests/common/test_quota.py index b6ad156..485b8a8 100644 --- a/openstackclient/tests/common/test_quota.py +++ b/openstackclient/tests/common/test_quota.py @@ -17,6 +17,8 @@ import mock from openstackclient.common import quota from openstackclient.tests.compute.v2 import fakes as compute_fakes from openstackclient.tests import fakes +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.network.v2 import fakes as network_fakes class FakeQuotaResource(fakes.FakeResource): @@ -40,11 +42,23 @@ class TestQuota(compute_fakes.TestComputev2): super(TestQuota, self).setUp() self.quotas_mock = self.app.client_manager.compute.quotas self.quotas_mock.reset_mock() + self.quotas_class_mock = self.app.client_manager.compute.quota_classes + self.quotas_class_mock.reset_mock() volume_mock = mock.Mock() volume_mock.quotas = mock.Mock() self.app.client_manager.volume = volume_mock self.volume_quotas_mock = volume_mock.quotas self.volume_quotas_mock.reset_mock() + self.volume_quotas_class_mock = \ + self.app.client_manager.volume.quota_classes + self.volume_quotas_class_mock.reset_mock() + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + self.app.client_manager.auth_ref = mock.Mock() + self.app.client_manager.auth_ref.service_catalog = mock.Mock() + self.service_catalog_mock = \ + self.app.client_manager.auth_ref.service_catalog + self.service_catalog_mock.reset_mock() class TestQuotaSet(TestQuota): @@ -76,6 +90,12 @@ class TestQuotaSet(TestQuota): loaded=True, ) + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + self.cmd = quota.SetQuota(self.app, None) def test_quota_set(self): @@ -83,15 +103,32 @@ class TestQuotaSet(TestQuota): '--floating-ips', str(compute_fakes.floating_ip_num), '--fixed-ips', str(compute_fakes.fix_ip_num), '--injected-files', str(compute_fakes.injected_file_num), + '--injected-file-size', str(compute_fakes.injected_file_size_num), + '--injected-path-size', str(compute_fakes.injected_path_size_num), '--key-pairs', str(compute_fakes.key_pair_num), - compute_fakes.project_name, + '--cores', str(compute_fakes.core_num), + '--ram', str(compute_fakes.ram_num), + '--instances', str(compute_fakes.instance_num), + '--properties', str(compute_fakes.property_num), + '--secgroup-rules', str(compute_fakes.secgroup_rule_num), + '--secgroups', str(compute_fakes.secgroup_num), + identity_fakes.project_name, ] verifylist = [ ('floating_ips', compute_fakes.floating_ip_num), ('fixed_ips', compute_fakes.fix_ip_num), ('injected_files', compute_fakes.injected_file_num), + ('injected_file_content_bytes', + compute_fakes.injected_file_size_num), + ('injected_file_path_bytes', compute_fakes.injected_path_size_num), ('key_pairs', compute_fakes.key_pair_num), - ('project', compute_fakes.project_name), + ('cores', compute_fakes.core_num), + ('ram', compute_fakes.ram_num), + ('instances', compute_fakes.instance_num), + ('metadata_items', compute_fakes.property_num), + ('security_group_rules', compute_fakes.secgroup_rule_num), + ('security_groups', compute_fakes.secgroup_num), + ('project', identity_fakes.project_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -102,17 +139,29 @@ class TestQuotaSet(TestQuota): 'floating_ips': compute_fakes.floating_ip_num, 'fixed_ips': compute_fakes.fix_ip_num, 'injected_files': compute_fakes.injected_file_num, + 'injected_file_content_bytes': + compute_fakes.injected_file_size_num, + 'injected_file_path_bytes': compute_fakes.injected_path_size_num, 'key_pairs': compute_fakes.key_pair_num, + 'cores': compute_fakes.core_num, + 'ram': compute_fakes.ram_num, + 'instances': compute_fakes.instance_num, + 'metadata_items': compute_fakes.property_num, + 'security_group_rules': compute_fakes.secgroup_rule_num, + 'security_groups': compute_fakes.secgroup_num, } - self.quotas_mock.update.assert_called_with('project_test', **kwargs) + self.quotas_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) def test_quota_set_volume(self): arglist = [ '--gigabytes', str(compute_fakes.floating_ip_num), '--snapshots', str(compute_fakes.fix_ip_num), '--volumes', str(compute_fakes.injected_file_num), - compute_fakes.project_name, + identity_fakes.project_name, ] verifylist = [ ('gigabytes', compute_fakes.floating_ip_num), @@ -130,5 +179,117 @@ class TestQuotaSet(TestQuota): 'volumes': compute_fakes.injected_file_num, } - self.volume_quotas_mock.update.assert_called_with('project_test', - **kwargs) + self.volume_quotas_mock.update.assert_called_with( + identity_fakes.project_id, + **kwargs + ) + + +class TestQuotaShow(TestQuota): + + def setUp(self): + super(TestQuotaShow, self).setUp() + + self.quotas_mock.get.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + + self.quotas_mock.defaults.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + + self.volume_quotas_mock.get.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + + self.volume_quotas_mock.defaults.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + + self.service_catalog_mock.get_endpoints.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ENDPOINT), + loaded=True, + ) + ] + + self.quotas_class_mock.get.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + + self.volume_quotas_class_mock.get.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + self.app.client_manager.network = network_fakes.FakeNetworkV2Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + self.cmd = quota.ShowQuota(self.app, None) + + def test_quota_show(self): + arglist = [ + identity_fakes.project_name, + ] + verifylist = [ + ('project', identity_fakes.project_name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.quotas_mock.get.assert_called_with(identity_fakes.project_id) + + def test_quota_show_with_default(self): + arglist = [ + '--default', + identity_fakes.project_name, + ] + verifylist = [ + ('default', True), + ('project', identity_fakes.project_name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.quotas_mock.defaults.assert_called_with(identity_fakes.project_id) + + def test_quota_show_with_class(self): + arglist = [ + '--class', + identity_fakes.project_name, + ] + verifylist = [ + ('quota_class', True), + ('project', identity_fakes.project_name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.quotas_class_mock.get.assert_called_with( + identity_fakes.project_id) diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py index a25a5ba..b564ffa 100644 --- a/openstackclient/tests/common/test_utils.py +++ b/openstackclient/tests/common/test_utils.py @@ -20,6 +20,7 @@ import mock from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.tests import fakes from openstackclient.tests import utils as test_utils PASSWORD = "Pa$$w0rd" @@ -27,6 +28,18 @@ WASSPORD = "Wa$$p0rd" DROWSSAP = "dr0w$$aP" +class FakeOddballResource(fakes.FakeResource): + + def get(self, attr): + """get() is needed for utils.find_resource()""" + if attr == 'id': + return self.id + elif attr == 'name': + return self.name + else: + return None + + class TestUtils(test_utils.TestCase): def test_get_password_good(self): @@ -124,6 +137,46 @@ class TestUtils(test_utils.TestCase): items, sort_str) @mock.patch.object(time, 'sleep') + def test_wait_for_status_ok(self, mock_sleep): + # Tests the normal flow that the resource is status=active + resource = mock.MagicMock(status='ACTIVE') + status_f = mock.Mock(return_value=resource) + res_id = str(uuid.uuid4()) + self.assertTrue(utils.wait_for_status(status_f, res_id,)) + self.assertFalse(mock_sleep.called) + + @mock.patch.object(time, 'sleep') + def test_wait_for_status_ok__with_overrides(self, mock_sleep): + # Tests the normal flow that the resource is status=complete + resource = mock.MagicMock(my_status='COMPLETE') + status_f = mock.Mock(return_value=resource) + res_id = str(uuid.uuid4()) + self.assertTrue(utils.wait_for_status(status_f, res_id, + status_field='my_status', + success_status=['complete'])) + self.assertFalse(mock_sleep.called) + + @mock.patch.object(time, 'sleep') + def test_wait_for_status_error(self, mock_sleep): + # Tests that we fail if the resource is status=error + resource = mock.MagicMock(status='ERROR') + status_f = mock.Mock(return_value=resource) + res_id = str(uuid.uuid4()) + self.assertFalse(utils.wait_for_status(status_f, res_id)) + self.assertFalse(mock_sleep.called) + + @mock.patch.object(time, 'sleep') + def test_wait_for_status_error_with_overrides(self, mock_sleep): + # Tests that we fail if the resource is my_status=failed + resource = mock.MagicMock(my_status='FAILED') + status_f = mock.Mock(return_value=resource) + res_id = str(uuid.uuid4()) + self.assertFalse(utils.wait_for_status(status_f, res_id, + status_field='my_status', + error_status=['failed'])) + self.assertFalse(mock_sleep.called) + + @mock.patch.object(time, 'sleep') def test_wait_for_delete_ok(self, mock_sleep): # Tests the normal flow that the resource is deleted with a 404 coming # back on the 2nd iteration of the wait loop. @@ -242,6 +295,47 @@ class TestFindResource(test_utils.TestCase): self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) + def test_find_resource_silly_resource(self): + # We need a resource with no resource_class for this test, start fresh + self.manager = mock.Mock() + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock( + side_effect=AttributeError( + "'Controller' object has no attribute 'find'", + ) + ) + silly_resource = FakeOddballResource( + None, + {'id': '12345', 'name': self.name}, + loaded=True, + ) + self.manager.list = mock.Mock( + return_value=[silly_resource, ], + ) + result = utils.find_resource(self.manager, self.name) + self.assertEqual(silly_resource, result) + self.manager.get.assert_called_with(self.name) + self.manager.find.assert_called_with(name=self.name) + + def test_find_resource_silly_resource_not_found(self): + # We need a resource with no resource_class for this test, start fresh + self.manager = mock.Mock() + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock( + side_effect=AttributeError( + "'Controller' object has no attribute 'find'", + ) + ) + self.manager.list = mock.Mock(return_value=[]) + result = self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + self.name) + self.assertEqual("Could not find resource legos", + str(result)) + self.manager.get.assert_called_with(self.name) + self.manager.find.assert_called_with(name=self.name) + def test_format_dict(self): expected = "a='b', c='d', e='f'" self.assertEqual(expected, diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py index e798bd4..91cc2bd 100644 --- a/openstackclient/tests/compute/v2/fakes.py +++ b/openstackclient/tests/compute/v2/fakes.py @@ -13,24 +13,18 @@ # under the License. # +import copy import mock +import uuid from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes from openstackclient.tests.image.v2 import fakes as image_fakes from openstackclient.tests.network.v2 import fakes as network_fakes from openstackclient.tests import utils +from openstackclient.tests.volume.v2 import fakes as volume_fakes -server_id = 'serv1' -server_name = 'waiter' - -SERVER = { - 'id': server_id, - 'name': server_name, - 'metadata': {}, -} - extension_name = 'Multinic' extension_namespace = 'http://docs.openstack.org/compute/ext/'\ 'multinic/api/v1.1' @@ -50,34 +44,49 @@ EXTENSION = { 'links': extension_links, } -flavor_id = 'm1.large' -flavor_name = 'Large' -flavor_ram = 8192 -flavor_vcpus = 4 - -FLAVOR = { - 'id': flavor_id, - 'name': flavor_name, - 'ram': flavor_ram, - 'vcpus': flavor_vcpus, -} - floating_ip_num = 100 fix_ip_num = 100 injected_file_num = 100 +injected_file_size_num = 10240 +injected_path_size_num = 255 key_pair_num = 100 +core_num = 20 +ram_num = 51200 +instance_num = 10 +property_num = 128 +secgroup_rule_num = 20 +secgroup_num = 10 project_name = 'project_test' QUOTA = { 'project': project_name, 'floating-ips': floating_ip_num, 'fix-ips': fix_ip_num, 'injected-files': injected_file_num, + 'injected-file-size': injected_file_size_num, + 'injected-path-size': injected_path_size_num, 'key-pairs': key_pair_num, + 'cores': core_num, + 'ram': ram_num, + 'instances': instance_num, + 'properties': property_num, + 'secgroup_rules': secgroup_rule_num, + 'secgroups': secgroup_num, } QUOTA_columns = tuple(sorted(QUOTA)) QUOTA_data = tuple(QUOTA[x] for x in sorted(QUOTA)) +block_device_mapping = 'vda=' + volume_fakes.volume_name + ':::0' + +service_host = 'host_test' +service_binary = 'compute_test' +service_status = 'enabled' +SERVICE = { + 'host': service_host, + 'binary': service_binary, + 'status': service_status, +} + class FakeComputev2Client(object): def __init__(self, **kwargs): @@ -85,12 +94,18 @@ class FakeComputev2Client(object): self.images.resource_class = fakes.FakeResource(None, {}) self.servers = mock.Mock() self.servers.resource_class = fakes.FakeResource(None, {}) + self.services = mock.Mock() + self.services.resource_class = fakes.FakeResource(None, {}) self.extensions = mock.Mock() self.extensions.resource_class = fakes.FakeResource(None, {}) self.flavors = mock.Mock() self.flavors.resource_class = fakes.FakeResource(None, {}) self.quotas = mock.Mock() self.quotas.resource_class = fakes.FakeResource(None, {}) + self.quota_classes = mock.Mock() + self.quota_classes.resource_class = fakes.FakeResource(None, {}) + self.volumes = mock.Mock() + self.volumes.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -118,3 +133,161 @@ class TestComputev2(utils.TestCommand): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) + + self.app.client_manager.volume = volume_fakes.FakeVolumeClient( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + + +class FakeServer(object): + """Fake one or more compute servers.""" + + @staticmethod + def create_one_server(attrs={}, methods={}): + """Create a fake server. + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :return: + A FakeResource object, with id, name, metadata + """ + # Set default attributes. + server_info = { + 'id': 'server-id-' + uuid.uuid4().hex, + 'name': 'server-name-' + uuid.uuid4().hex, + 'metadata': {}, + } + + # Overwrite default attributes. + server_info.update(attrs) + + server = fakes.FakeResource(info=copy.deepcopy(server_info), + methods=methods, + loaded=True) + return server + + @staticmethod + def create_servers(attrs={}, methods={}, count=2): + """Create multiple fake servers. + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :param int count: + The number of servers to fake + :return: + A list of FakeResource objects faking the servers + """ + servers = [] + for i in range(0, count): + servers.append(FakeServer.create_one_server(attrs, methods)) + + return servers + + @staticmethod + def get_servers(servers=None, count=2): + """Get an iterable MagicMock object with a list of faked servers. + + If servers list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List servers: + A list of FakeResource objects faking servers + :param int count: + The number of servers to fake + :return: + An iterable Mock object with side_effect set to a list of faked + servers + """ + if servers is None: + servers = FakeServer.create_servers(count) + return mock.MagicMock(side_effect=servers) + + +class FakeFlavorResource(fakes.FakeResource): + """Fake flavor object's methods to help test. + + The flavor object has three methods to get, set, unset its properties. + Need to fake them, otherwise the functions to be tested won't run properly. + """ + + # Fake properties. + _keys = {'property': 'value'} + + def set_keys(self, args): + self._keys.update(args) + + def unset_keys(self, keys): + for key in keys: + self._keys.pop(key, None) + + def get_keys(self): + return self._keys + + +class FakeFlavor(object): + """Fake one or more flavors.""" + + @staticmethod + def create_one_flavor(attrs={}): + """Create a fake flavor. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeFlavorResource object, with id, name, ram, vcpus, properties + """ + # Set default attributes. + flavor_info = { + 'id': 'flavor-id-' + uuid.uuid4().hex, + 'name': 'flavor-name-' + uuid.uuid4().hex, + 'ram': 8192, + 'vcpus': 4, + } + + # Overwrite default attributes. + flavor_info.update(attrs) + + flavor = FakeFlavorResource(info=copy.deepcopy(flavor_info), + loaded=True) + return flavor + + @staticmethod + def create_flavors(attrs={}, count=2): + """Create multiple fake flavors. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of flavors to fake + :return: + A list of FakeFlavorResource objects faking the flavors + """ + flavors = [] + for i in range(0, count): + flavors.append(FakeFlavor.create_one_flavor(attrs)) + + return flavors + + @staticmethod + def get_flavors(flavors=None, count=2): + """Get an iterable MagicMock object with a list of faked flavors. + + If flavors list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List flavors: + A list of FakeFlavorResource objects faking flavors + :param int count: + The number of flavors to fake + :return: + An iterable Mock object with side_effect set to a list of faked + flavors + """ + if flavors is None: + flavors = FakeServer.create_flavors(count) + return mock.MagicMock(side_effect=flavors) diff --git a/openstackclient/tests/compute/v2/test_flavor.py b/openstackclient/tests/compute/v2/test_flavor.py index 19be812..60356ef 100644 --- a/openstackclient/tests/compute/v2/test_flavor.py +++ b/openstackclient/tests/compute/v2/test_flavor.py @@ -13,26 +13,9 @@ # under the License. # -import copy - +from openstackclient.common import exceptions from openstackclient.compute.v2 import flavor from openstackclient.tests.compute.v2 import fakes as compute_fakes -from openstackclient.tests import fakes - - -class FakeFlavorResource(fakes.FakeResource): - - _keys = {'property': 'value'} - - def set_keys(self, args): - self._keys.update(args) - - def unset_keys(self, keys): - for key in keys: - self._keys.pop(key, None) - - def get_keys(self): - return self._keys class TestFlavor(compute_fakes.TestComputev2): @@ -45,18 +28,90 @@ class TestFlavor(compute_fakes.TestComputev2): self.flavors_mock.reset_mock() +class TestFlavorDelete(TestFlavor): + + flavor = compute_fakes.FakeFlavor.create_one_flavor() + + def setUp(self): + super(TestFlavorDelete, self).setUp() + + self.flavors_mock.get.return_value = self.flavor + self.flavors_mock.delete.return_value = None + + self.cmd = flavor.DeleteFlavor(self.app, None) + + def test_flavor_delete(self): + arglist = [ + self.flavor.id + ] + verifylist = [ + ('flavor', self.flavor.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.flavors_mock.delete.assert_called_with(self.flavor.id) + + def test_flavor_delete_with_unexist_flavor(self): + self.flavors_mock.get.side_effect = exceptions.NotFound(None) + self.flavors_mock.find.side_effect = exceptions.NotFound(None) + + arglist = [ + 'unexist_flavor' + ] + verifylist = [ + ('flavor', 'unexist_flavor'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + class TestFlavorList(TestFlavor): + # Return value of self.flavors_mock.list(). + flavors = compute_fakes.FakeFlavor.create_flavors(count=1) + + columns = ( + 'ID', + 'Name', + 'RAM', + 'Disk', + 'Ephemeral', + 'VCPUs', + 'Is Public', + ) + columns_long = columns + ( + 'Swap', + 'RXTX Factor', + 'Properties' + ) + + data = (( + flavors[0].id, + flavors[0].name, + flavors[0].ram, + '', + '', + flavors[0].vcpus, + '' + ), ) + data_long = (data[0] + ( + '', + '', + 'property=\'value\'' + ), ) + def setUp(self): super(TestFlavorList, self).setUp() - self.flavors_mock.list.return_value = [ - FakeFlavorResource( - None, - copy.deepcopy(compute_fakes.FLAVOR), - loaded=True, - ), - ] + self.flavors_mock.list.return_value = self.flavors # Get the command object to test self.cmd = flavor.ListFlavor(self.app, None) @@ -76,33 +131,17 @@ class TestFlavorList(TestFlavor): # Set expected values kwargs = { - 'is_public': True + 'is_public': True, + 'limit': None, + 'marker': None } self.flavors_mock.list.assert_called_with( **kwargs ) - collist = ( - 'ID', - 'Name', - 'RAM', - 'Disk', - 'Ephemeral', - 'VCPUs', - 'Is Public', - ) - self.assertEqual(collist, columns) - datalist = (( - compute_fakes.flavor_id, - compute_fakes.flavor_name, - compute_fakes.flavor_ram, - '', - '', - compute_fakes.flavor_vcpus, - '' - ), ) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) def test_flavor_list_all_flavors(self): arglist = [ @@ -119,33 +158,17 @@ class TestFlavorList(TestFlavor): # Set expected values kwargs = { - 'is_public': None + 'is_public': None, + 'limit': None, + 'marker': None } self.flavors_mock.list.assert_called_with( **kwargs ) - collist = ( - 'ID', - 'Name', - 'RAM', - 'Disk', - 'Ephemeral', - 'VCPUs', - 'Is Public', - ) - self.assertEqual(collist, columns) - datalist = (( - compute_fakes.flavor_id, - compute_fakes.flavor_name, - compute_fakes.flavor_ram, - '', - '', - compute_fakes.flavor_vcpus, - '' - ), ) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) def test_flavor_list_private_flavors(self): arglist = [ @@ -162,33 +185,17 @@ class TestFlavorList(TestFlavor): # Set expected values kwargs = { - 'is_public': False + 'is_public': False, + 'limit': None, + 'marker': None } self.flavors_mock.list.assert_called_with( **kwargs ) - collist = ( - 'ID', - 'Name', - 'RAM', - 'Disk', - 'Ephemeral', - 'VCPUs', - 'Is Public', - ) - self.assertEqual(collist, columns) - datalist = (( - compute_fakes.flavor_id, - compute_fakes.flavor_name, - compute_fakes.flavor_ram, - '', - '', - compute_fakes.flavor_vcpus, - '' - ), ) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) def test_flavor_list_public_flavors(self): arglist = [ @@ -205,33 +212,17 @@ class TestFlavorList(TestFlavor): # Set expected values kwargs = { - 'is_public': True + 'is_public': True, + 'limit': None, + 'marker': None } self.flavors_mock.list.assert_called_with( **kwargs ) - collist = ( - 'ID', - 'Name', - 'RAM', - 'Disk', - 'Ephemeral', - 'VCPUs', - 'Is Public', - ) - self.assertEqual(collist, columns) - datalist = (( - compute_fakes.flavor_id, - compute_fakes.flavor_name, - compute_fakes.flavor_ram, - '', - '', - compute_fakes.flavor_vcpus, - '' - ), ) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) def test_flavor_list_long(self): arglist = [ @@ -248,51 +239,28 @@ class TestFlavorList(TestFlavor): # Set expected values kwargs = { - 'is_public': True + 'is_public': True, + 'limit': None, + 'marker': None } self.flavors_mock.list.assert_called_with( **kwargs ) - collist = ( - 'ID', - 'Name', - 'RAM', - 'Disk', - 'Ephemeral', - 'VCPUs', - 'Is Public', - 'Swap', - 'RXTX Factor', - 'Properties' - ) - self.assertEqual(collist, columns) - datalist = (( - compute_fakes.flavor_id, - compute_fakes.flavor_name, - compute_fakes.flavor_ram, - '', - '', - compute_fakes.flavor_vcpus, - '', - '', - '', - 'property=\'value\'' - ), ) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns_long, columns) + self.assertEqual(tuple(self.data_long), tuple(data)) class TestFlavorSet(TestFlavor): + # Return value of self.flavors_mock.find(). + flavor = compute_fakes.FakeFlavor.create_one_flavor() + def setUp(self): super(TestFlavorSet, self).setUp() - self.flavors_mock.find.return_value = FakeFlavorResource( - None, - copy.deepcopy(compute_fakes.FLAVOR), - loaded=True, - ) + self.flavors_mock.find.return_value = self.flavor self.cmd = flavor.SetFlavor(self.app, None) @@ -318,14 +286,13 @@ class TestFlavorSet(TestFlavor): class TestFlavorUnset(TestFlavor): + # Return value of self.flavors_mock.find(). + flavor = compute_fakes.FakeFlavor.create_one_flavor() + def setUp(self): super(TestFlavorUnset, self).setUp() - self.flavors_mock.find.return_value = FakeFlavorResource( - None, - copy.deepcopy(compute_fakes.FLAVOR), - loaded=True, - ) + self.flavors_mock.find.return_value = self.flavor self.cmd = flavor.UnsetFlavor(self.app, None) diff --git a/openstackclient/tests/compute/v2/test_server.py b/openstackclient/tests/compute/v2/test_server.py index 4df18f0..359920f 100644 --- a/openstackclient/tests/compute/v2/test_server.py +++ b/openstackclient/tests/compute/v2/test_server.py @@ -17,6 +17,7 @@ import copy import mock import testtools +from mock import call from openstackclient.common import exceptions from openstackclient.common import utils as common_utils from openstackclient.compute.v2 import server @@ -24,6 +25,7 @@ from openstackclient.tests.compute.v2 import fakes as compute_fakes from openstackclient.tests import fakes from openstackclient.tests.image.v2 import fakes as image_fakes from openstackclient.tests import utils +from openstackclient.tests.volume.v2 import fakes as volume_fakes class TestServer(compute_fakes.TestComputev2): @@ -47,24 +49,63 @@ class TestServer(compute_fakes.TestComputev2): self.images_mock = self.app.client_manager.image.images self.images_mock.reset_mock() + # Get a shortcut to the VolumeManager Mock + self.volumes_mock = self.app.client_manager.volume.volumes + self.volumes_mock.reset_mock() + + # Set object attributes to be tested. Could be overwriten in subclass. + self.attrs = {} + + # Set object methods to be tested. Could be overwriten in subclass. + self.methods = {} + + def setup_servers_mock(self, count): + servers = compute_fakes.FakeServer.create_servers(attrs=self.attrs, + methods=self.methods, + count=count) + + # This is the return value for utils.find_resource() + self.servers_mock.get = compute_fakes.FakeServer.get_servers(servers, + 0) + return servers + + def run_method_with_servers(self, method_name, server_count): + servers = self.setup_servers_mock(server_count) + + arglist = [] + verifylist = [] + + for s in servers: + arglist.append(s.id) + verifylist = [ + ('server', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + for s in servers: + method = getattr(s, method_name) + method.assert_called_with() + class TestServerCreate(TestServer): def setUp(self): super(TestServerCreate, self).setUp() - self.servers_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(compute_fakes.SERVER), - loaded=True, - ) - new_server = fakes.FakeResource( - None, - copy.deepcopy(compute_fakes.SERVER), - loaded=True, - ) - new_server.__dict__['networks'] = {} - self.servers_mock.get.return_value = new_server + attrs = { + 'networks': {}, + } + self.new_server = compute_fakes.FakeServer.create_one_server( + attrs=attrs) + + # This is the return value for utils.find_resource(). + # This is for testing --wait option. + self.servers_mock.get.return_value = self.new_server + + self.servers_mock.create.return_value = self.new_server self.image = fakes.FakeResource( None, @@ -73,22 +114,25 @@ class TestServerCreate(TestServer): ) self.cimages_mock.get.return_value = self.image - self.flavor = fakes.FakeResource( + self.flavor = compute_fakes.FakeFlavor.create_one_flavor() + self.flavors_mock.get.return_value = self.flavor + + self.volume = fakes.FakeResource( None, - copy.deepcopy(compute_fakes.FLAVOR), + copy.deepcopy(volume_fakes.VOLUME), loaded=True, ) - self.flavors_mock.get.return_value = self.flavor + self.volumes_mock.get.return_value = self.volume # Get the command object to test self.cmd = server.CreateServer(self.app, None) def test_server_create_no_options(self): arglist = [ - compute_fakes.server_id, + self.new_server.name, ] verifylist = [ - ('server_name', compute_fakes.server_id), + ('server_name', self.new_server.name), ] try: # Missing required args should bail here @@ -100,13 +144,13 @@ class TestServerCreate(TestServer): arglist = [ '--image', 'image1', '--flavor', 'flavor1', - compute_fakes.server_id, + self.new_server.name, ] verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), ('config_drive', False), - ('server_name', compute_fakes.server_id), + ('server_name', self.new_server.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -131,19 +175,27 @@ class TestServerCreate(TestServer): ) # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( - compute_fakes.server_id, + self.new_server.name, self.image, self.flavor, **kwargs ) - collist = ('addresses', 'flavor', 'id', 'name', 'properties') + collist = ( + 'addresses', + 'flavor', + 'id', + 'name', + 'networks', + 'properties', + ) self.assertEqual(collist, columns) datalist = ( '', - 'Large ()', - compute_fakes.server_id, - compute_fakes.server_name, + self.flavor.name + ' ()', + self.new_server.id, + self.new_server.name, + self.new_server.networks, '', ) self.assertEqual(datalist, data) @@ -154,14 +206,14 @@ class TestServerCreate(TestServer): '--flavor', 'flavor1', '--nic', 'net-id=net1', '--nic', 'port-id=port1', - compute_fakes.server_id, + self.new_server.name, ] verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), ('nic', ['net-id=net1', 'port-id=port1']), ('config_drive', False), - ('server_name', compute_fakes.server_id), + ('server_name', self.new_server.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -207,19 +259,27 @@ class TestServerCreate(TestServer): ) # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( - compute_fakes.server_id, + self.new_server.name, self.image, self.flavor, **kwargs ) - collist = ('addresses', 'flavor', 'id', 'name', 'properties') + collist = ( + 'addresses', + 'flavor', + 'id', + 'name', + 'networks', + 'properties', + ) self.assertEqual(collist, columns) datalist = ( '', - 'Large ()', - compute_fakes.server_id, - compute_fakes.server_name, + self.flavor.name + ' ()', + self.new_server.id, + self.new_server.name, + self.new_server.networks, '', ) self.assertEqual(datalist, data) @@ -234,14 +294,14 @@ class TestServerCreate(TestServer): '--image', 'image1', '--flavor', 'flavor1', '--user-data', 'userdata.sh', - compute_fakes.server_id, + self.new_server.name, ] verifylist = [ ('image', 'image1'), ('flavor', 'flavor1'), ('user_data', 'userdata.sh'), ('config_drive', False), - ('server_name', compute_fakes.server_id), + ('server_name', self.new_server.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -272,19 +332,96 @@ class TestServerCreate(TestServer): ) # ServerManager.create(name, image, flavor, **kwargs) self.servers_mock.create.assert_called_with( - compute_fakes.server_id, + self.new_server.name, self.image, self.flavor, **kwargs ) - collist = ('addresses', 'flavor', 'id', 'name', 'properties') + collist = ( + 'addresses', + 'flavor', + 'id', + 'name', + 'networks', + 'properties', + ) self.assertEqual(collist, columns) datalist = ( '', - 'Large ()', - compute_fakes.server_id, - compute_fakes.server_name, + self.flavor.name + ' ()', + self.new_server.id, + self.new_server.name, + self.new_server.networks, + '', + ) + self.assertEqual(datalist, data) + + def test_server_create_with_block_device_mapping(self): + arglist = [ + '--image', 'image1', + '--flavor', self.flavor.id, + '--block-device-mapping', compute_fakes.block_device_mapping, + self.new_server.name, + ] + verifylist = [ + ('image', 'image1'), + ('flavor', self.flavor.id), + ('block_device_mapping', [compute_fakes.block_device_mapping]), + ('config_drive', False), + ('server_name', self.new_server.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # CreateServer.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + real_volume_mapping = ( + (compute_fakes.block_device_mapping.split('=', 1)[1]).replace( + volume_fakes.volume_name, + volume_fakes.volume_id)) + + # Set expected values + kwargs = dict( + meta=None, + files={}, + reservation_id=None, + min_count=1, + max_count=1, + security_groups=[], + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping={ + 'vda': real_volume_mapping + }, + nics=[], + scheduler_hints={}, + config_drive=None, + ) + # ServerManager.create(name, image, flavor, **kwargs) + self.servers_mock.create.assert_called_with( + self.new_server.name, + self.image, + self.flavor, + **kwargs + ) + + collist = ( + 'addresses', + 'flavor', + 'id', + 'name', + 'networks', + 'properties', + ) + self.assertEqual(collist, columns) + datalist = ( + '', + self.flavor.name + ' ()', + self.new_server.id, + self.new_server.name, + self.new_server.networks, '', ) self.assertEqual(datalist, data) @@ -295,23 +432,19 @@ class TestServerDelete(TestServer): def setUp(self): super(TestServerDelete, self).setUp() - # This is the return value for utils.find_resource() - self.servers_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(compute_fakes.SERVER), - loaded=True, - ) self.servers_mock.delete.return_value = None # Get the command object to test self.cmd = server.DeleteServer(self.app, None) def test_server_delete_no_options(self): + servers = self.setup_servers_mock(count=1) + arglist = [ - compute_fakes.server_id, + servers[0].id, ] verifylist = [ - ('servers', [compute_fakes.server_id]), + ('server', [servers[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -319,16 +452,39 @@ class TestServerDelete(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.delete.assert_called_with( - compute_fakes.server_id, + servers[0].id, ) + def test_server_delete_multi_servers(self): + servers = self.setup_servers_mock(count=3) + + arglist = [] + verifylist = [] + + for s in servers: + arglist.append(s.id) + verifylist = [ + ('server', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + calls = [] + for s in servers: + calls.append(call(s.id)) + self.servers_mock.delete.assert_has_calls(calls) + @mock.patch.object(common_utils, 'wait_for_delete', return_value=True) def test_server_delete_wait_ok(self, mock_wait_for_delete): + servers = self.setup_servers_mock(count=1) + arglist = [ - compute_fakes.server_id, '--wait' + servers[0].id, '--wait' ] verifylist = [ - ('servers', [compute_fakes.server_id]), + ('server', [servers[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -336,22 +492,24 @@ class TestServerDelete(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.delete.assert_called_with( - compute_fakes.server_id, + servers[0].id, ) mock_wait_for_delete.assert_called_once_with( self.servers_mock, - compute_fakes.server_id, + servers[0].id, callback=server._show_progress ) @mock.patch.object(common_utils, 'wait_for_delete', return_value=False) def test_server_delete_wait_fails(self, mock_wait_for_delete): + servers = self.setup_servers_mock(count=1) + arglist = [ - compute_fakes.server_id, '--wait' + servers[0].id, '--wait' ] verifylist = [ - ('servers', [compute_fakes.server_id]), + ('server', [servers[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -359,12 +517,12 @@ class TestServerDelete(TestServer): self.assertRaises(SystemExit, self.cmd.take_action, parsed_args) self.servers_mock.delete.assert_called_with( - compute_fakes.server_id, + servers[0].id, ) mock_wait_for_delete.assert_called_once_with( self.servers_mock, - compute_fakes.server_id, + servers[0].id, callback=server._show_progress ) @@ -374,12 +532,10 @@ class TestServerImageCreate(TestServer): def setUp(self): super(TestServerImageCreate, self).setUp() + self.server = compute_fakes.FakeServer.create_one_server() + # This is the return value for utils.find_resource() - self.servers_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(compute_fakes.SERVER), - loaded=True, - ) + self.servers_mock.get.return_value = self.server self.servers_mock.create_image.return_value = image_fakes.image_id @@ -394,10 +550,10 @@ class TestServerImageCreate(TestServer): def test_server_image_create_no_options(self): arglist = [ - compute_fakes.server_id, + self.server.id, ] verifylist = [ - ('server', compute_fakes.server_id), + ('server', self.server.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -407,16 +563,17 @@ class TestServerImageCreate(TestServer): # ServerManager.create_image(server, image_name, metadata=) self.servers_mock.create_image.assert_called_with( self.servers_mock.get.return_value, - compute_fakes.server_name, + self.server.name, ) - collist = ('id', 'name', 'owner', 'protected', 'visibility') + collist = ('id', 'name', 'owner', 'protected', 'tags', 'visibility') self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, image_fakes.image_name, image_fakes.image_owner, image_fakes.image_protected, + image_fakes.image_tags, image_fakes.image_visibility, ) self.assertEqual(datalist, data) @@ -424,11 +581,11 @@ class TestServerImageCreate(TestServer): def test_server_image_create_name(self): arglist = [ '--name', 'img-nam', - compute_fakes.server_id, + self.server.id, ] verifylist = [ ('name', 'img-nam'), - ('server', compute_fakes.server_id), + ('server', self.server.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -441,41 +598,157 @@ class TestServerImageCreate(TestServer): 'img-nam', ) - collist = ('id', 'name', 'owner', 'protected', 'visibility') + collist = ('id', 'name', 'owner', 'protected', 'tags', 'visibility') self.assertEqual(collist, columns) datalist = ( image_fakes.image_id, image_fakes.image_name, image_fakes.image_owner, image_fakes.image_protected, + image_fakes.image_tags, image_fakes.image_visibility, ) self.assertEqual(datalist, data) +class TestServerList(TestServer): + + # Columns to be listed up. + columns = ( + 'ID', + 'Name', + 'Status', + 'Networks', + ) + + # Data returned by corresponding Nova API. The elements in this list are + # tuples filled with server attributes. + data = [] + + # Default search options, in the case of no commandline option specified. + search_opts = { + 'reservation_id': None, + 'ip': None, + 'ip6': None, + 'name': None, + 'instance_name': None, + 'status': None, + 'flavor': None, + 'image': None, + 'host': None, + 'tenant_id': None, + 'all_tenants': False, + 'user_id': None, + } + + # Default params of the core function of the command in the case of no + # commandline option specified. + kwargs = { + 'search_opts': search_opts, + 'marker': None, + 'limit': None, + } + + def setUp(self): + super(TestServerList, self).setUp() + + # The fake servers' attributes. + self.attrs = { + 'status': 'ACTIVE', + 'networks': { + u'public': [u'10.20.30.40', u'2001:db8::5'] + }, + } + + # The servers to be listed. + self.servers = self.setup_servers_mock(3) + + self.servers_mock.list.return_value = self.servers + + # Get the command object to test + self.cmd = server.ListServer(self.app, None) + + # Prepare data returned by fake Nova API. + for s in self.servers: + self.data.append(( + s.id, + s.name, + s.status, + u'public=10.20.30.40, 2001:db8::5', + )) + + def test_server_list_no_option(self): + arglist = [] + verifylist = [ + ('all_projects', False), + ('long', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.servers_mock.list.assert_called_with(**self.kwargs) + self.assertEqual(self.columns, columns) + self.assertEqual(tuple(self.data), tuple(data)) + + +class TestServerLock(TestServer): + + def setUp(self): + super(TestServerLock, self).setUp() + + # Get the command object to test + self.cmd = server.LockServer(self.app, None) + + # Set methods to be tested. + self.methods = { + 'lock': None, + } + + def test_server_lock_one_server(self): + self.run_method_with_servers('lock', 1) + + def test_server_lock_multi_servers(self): + self.run_method_with_servers('lock', 3) + + +class TestServerPause(TestServer): + + def setUp(self): + super(TestServerPause, self).setUp() + + # Get the command object to test + self.cmd = server.PauseServer(self.app, None) + + # Set methods to be tested. + self.methods = { + 'pause': None, + } + + def test_server_pause_one_server(self): + self.run_method_with_servers('pause', 1) + + def test_server_pause_multi_servers(self): + self.run_method_with_servers('pause', 3) + + class TestServerResize(TestServer): def setUp(self): super(TestServerResize, self).setUp() + self.server = compute_fakes.FakeServer.create_one_server() + # This is the return value for utils.find_resource() - self.servers_get_return_value = fakes.FakeResource( - None, - copy.deepcopy(compute_fakes.SERVER), - loaded=True, - ) - self.servers_mock.get.return_value = self.servers_get_return_value + self.servers_mock.get.return_value = self.server self.servers_mock.resize.return_value = None self.servers_mock.confirm_resize.return_value = None self.servers_mock.revert_resize.return_value = None # This is the return value for utils.find_resource() - self.flavors_get_return_value = fakes.FakeResource( - None, - copy.deepcopy(compute_fakes.FLAVOR), - loaded=True, - ) + self.flavors_get_return_value = \ + compute_fakes.FakeFlavor.create_one_flavor() self.flavors_mock.get.return_value = self.flavors_get_return_value # Get the command object to test @@ -483,12 +756,12 @@ class TestServerResize(TestServer): def test_server_resize_no_options(self): arglist = [ - compute_fakes.server_id, + self.server.id, ] verifylist = [ ('confirm', False), ('revert', False), - ('server', compute_fakes.server_id), + ('server', self.server.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -496,7 +769,7 @@ class TestServerResize(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with( - compute_fakes.server_id, + self.server.id, ) self.assertNotCalled(self.servers_mock.resize) @@ -505,14 +778,14 @@ class TestServerResize(TestServer): def test_server_resize(self): arglist = [ - '--flavor', compute_fakes.flavor_id, - compute_fakes.server_id, + '--flavor', self.flavors_get_return_value.id, + self.server.id, ] verifylist = [ - ('flavor', compute_fakes.flavor_id), + ('flavor', self.flavors_get_return_value.id), ('confirm', False), ('revert', False), - ('server', compute_fakes.server_id), + ('server', self.server.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -520,14 +793,14 @@ class TestServerResize(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with( - compute_fakes.server_id, + self.server.id, ) self.flavors_mock.get.assert_called_with( - compute_fakes.flavor_id, + self.flavors_get_return_value.id, ) self.servers_mock.resize.assert_called_with( - self.servers_get_return_value, + self.server, self.flavors_get_return_value, ) self.assertNotCalled(self.servers_mock.confirm_resize) @@ -536,12 +809,12 @@ class TestServerResize(TestServer): def test_server_resize_confirm(self): arglist = [ '--confirm', - compute_fakes.server_id, + self.server.id, ] verifylist = [ ('confirm', True), ('revert', False), - ('server', compute_fakes.server_id), + ('server', self.server.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -549,24 +822,24 @@ class TestServerResize(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with( - compute_fakes.server_id, + self.server.id, ) self.assertNotCalled(self.servers_mock.resize) self.servers_mock.confirm_resize.assert_called_with( - self.servers_get_return_value, + self.server, ) self.assertNotCalled(self.servers_mock.revert_resize) def test_server_resize_revert(self): arglist = [ '--revert', - compute_fakes.server_id, + self.server.id, ] verifylist = [ ('confirm', False), ('revert', True), - ('server', compute_fakes.server_id), + ('server', self.server.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -574,16 +847,176 @@ class TestServerResize(TestServer): self.cmd.take_action(parsed_args) self.servers_mock.get.assert_called_with( - compute_fakes.server_id, + self.server.id, ) self.assertNotCalled(self.servers_mock.resize) self.assertNotCalled(self.servers_mock.confirm_resize) self.servers_mock.revert_resize.assert_called_with( - self.servers_get_return_value, + self.server, ) +class TestServerResume(TestServer): + + def setUp(self): + super(TestServerResume, self).setUp() + + # Get the command object to test + self.cmd = server.ResumeServer(self.app, None) + + # Set methods to be tested. + self.methods = { + 'resume': None, + } + + def test_server_resume_one_server(self): + self.run_method_with_servers('resume', 1) + + def test_server_resume_multi_servers(self): + self.run_method_with_servers('resume', 3) + + +class TestServerShelve(TestServer): + + def setUp(self): + super(TestServerShelve, self).setUp() + + # Get the command object to test + self.cmd = server.ShelveServer(self.app, None) + + # Set shelve method to be tested. + self.methods = { + 'shelve': None, + } + + def test_shelve_one_server(self): + self.run_method_with_servers('shelve', 1) + + def test_shelve_multi_servers(self): + self.run_method_with_servers('shelve', 3) + + +class TestServerStart(TestServer): + + def setUp(self): + super(TestServerStart, self).setUp() + + # Get the command object to test + self.cmd = server.StartServer(self.app, None) + + # Set methods to be tested. + self.methods = { + 'start': None, + } + + def test_server_start_one_server(self): + self.run_method_with_servers('start', 1) + + def test_server_start_multi_servers(self): + self.run_method_with_servers('start', 3) + + +class TestServerStop(TestServer): + + def setUp(self): + super(TestServerStop, self).setUp() + + # Get the command object to test + self.cmd = server.StopServer(self.app, None) + + # Set methods to be tested. + self.methods = { + 'stop': None, + } + + def test_server_stop_one_server(self): + self.run_method_with_servers('stop', 1) + + def test_server_stop_multi_servers(self): + self.run_method_with_servers('stop', 3) + + +class TestServerSuspend(TestServer): + + def setUp(self): + super(TestServerSuspend, self).setUp() + + # Get the command object to test + self.cmd = server.SuspendServer(self.app, None) + + # Set methods to be tested. + self.methods = { + 'suspend': None, + } + + def test_server_suspend_one_server(self): + self.run_method_with_servers('suspend', 1) + + def test_server_suspend_multi_servers(self): + self.run_method_with_servers('suspend', 3) + + +class TestServerUnlock(TestServer): + + def setUp(self): + super(TestServerUnlock, self).setUp() + + # Get the command object to test + self.cmd = server.UnlockServer(self.app, None) + + # Set methods to be tested. + self.methods = { + 'unlock': None, + } + + def test_server_unlock_one_server(self): + self.run_method_with_servers('unlock', 1) + + def test_server_unlock_multi_servers(self): + self.run_method_with_servers('unlock', 3) + + +class TestServerUnpause(TestServer): + + def setUp(self): + super(TestServerUnpause, self).setUp() + + # Get the command object to test + self.cmd = server.UnpauseServer(self.app, None) + + # Set methods to be tested. + self.methods = { + 'unpause': None, + } + + def test_server_unpause_one_server(self): + self.run_method_with_servers('unpause', 1) + + def test_server_unpause_multi_servers(self): + self.run_method_with_servers('unpause', 3) + + +class TestServerUnshelve(TestServer): + + def setUp(self): + super(TestServerUnshelve, self).setUp() + + # Get the command object to test + self.cmd = server.UnshelveServer(self.app, None) + + # Set unshelve method to be tested. + self.methods = { + 'unshelve': None, + } + + def test_unshelve_one_server(self): + self.run_method_with_servers('unshelve', 1) + + def test_unshelve_multi_servers(self): + self.run_method_with_servers('unshelve', 3) + + class TestServerGeneral(testtools.TestCase): OLD = { 'private': [ diff --git a/openstackclient/tests/compute/v2/test_service.py b/openstackclient/tests/compute/v2/test_service.py new file mode 100644 index 0000000..380fbc4 --- /dev/null +++ b/openstackclient/tests/compute/v2/test_service.py @@ -0,0 +1,151 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy + +from openstackclient.compute.v2 import service +from openstackclient.tests.compute.v2 import fakes as compute_fakes +from openstackclient.tests import fakes + + +class TestService(compute_fakes.TestComputev2): + + def setUp(self): + super(TestService, self).setUp() + + # Get a shortcut to the ServiceManager Mock + self.service_mock = self.app.client_manager.compute.services + self.service_mock.reset_mock() + + +class TestServiceDelete(TestService): + + def setUp(self): + super(TestServiceDelete, self).setUp() + + self.service_mock.delete.return_value = None + + # Get the command object to test + self.cmd = service.DeleteService(self.app, None) + + def test_service_delete_no_options(self): + arglist = [ + compute_fakes.service_binary, + ] + verifylist = [ + ('service', compute_fakes.service_binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.service_mock.delete.assert_called_with( + compute_fakes.service_binary, + ) + + +class TestServiceList(TestService): + + def setUp(self): + super(TestServiceList, self).setUp() + + self.service_mock.list.return_value = [fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.SERVICE), + loaded=True, + )] + + # Get the command object to test + self.cmd = service.ListService(self.app, None) + + def test_service_list(self): + arglist = [ + '--host', compute_fakes.service_host, + '--service', compute_fakes.service_binary, + ] + verifylist = [ + ('host', compute_fakes.service_host), + ('service', compute_fakes.service_binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + self.service_mock.list.assert_called_with( + compute_fakes.service_host, + compute_fakes.service_binary, + ) + + +class TestServiceSet(TestService): + + def setUp(self): + super(TestServiceSet, self).setUp() + + self.service_mock.enable.return_value = [fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.SERVICE), + loaded=True, + )] + + self.service_mock.disable.return_value = [fakes.FakeResource( + None, + copy.deepcopy(compute_fakes.SERVICE), + loaded=True, + )] + + self.cmd = service.SetService(self.app, None) + + def test_service_set_enable(self): + arglist = [ + compute_fakes.service_host, + compute_fakes.service_binary, + '--enable', + ] + verifylist = [ + ('host', compute_fakes.service_host), + ('service', compute_fakes.service_binary), + ('enabled', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.service_mock.enable.assert_called_with( + compute_fakes.service_host, + compute_fakes.service_binary, + ) + + def test_service_set_disable(self): + arglist = [ + compute_fakes.service_host, + compute_fakes.service_binary, + '--disable', + ] + verifylist = [ + ('host', compute_fakes.service_host), + ('service', compute_fakes.service_binary), + ('enabled', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.service_mock.disable.assert_called_with( + compute_fakes.service_host, + compute_fakes.service_binary, + ) diff --git a/openstackclient/tests/fakes.py b/openstackclient/tests/fakes.py index 979f948..9f4dcc5 100644 --- a/openstackclient/tests/fakes.py +++ b/openstackclient/tests/fakes.py @@ -14,10 +14,11 @@ # import json +import mock import six import sys -from keystoneclient import fixture +from keystoneauth1 import fixture import requests @@ -122,17 +123,41 @@ class FakeModule(object): class FakeResource(object): - def __init__(self, manager, info, loaded=False): + def __init__(self, manager=None, info={}, loaded=False, methods={}): + """Set attributes and methods for a resource. + + :param manager: + The resource manager + :param Dictionary info: + A dictionary with all attributes + :param bool loaded: + True if the resource is loaded in memory + :param Dictionary methods: + A dictionary with all methods + """ self.__name__ = type(self).__name__ self.manager = manager self._info = info self._add_details(info) + self._add_methods(methods) self._loaded = loaded def _add_details(self, info): for (k, v) in six.iteritems(info): setattr(self, k, v) + def _add_methods(self, methods): + """Fake methods with MagicMock objects. + + For each <@key, @value> pairs in methods, add an callable MagicMock + object named @key as an attribute, and set the mock's return_value to + @value. When users access the attribute with (), @value will be + returned, which looks like a function call. + """ + for (name, ret) in six.iteritems(methods): + method = mock.MagicMock(return_value=ret) + setattr(self, name, method) + def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') @@ -150,3 +175,11 @@ class FakeResponse(requests.Response): self._content = json.dumps(data) if not isinstance(self._content, six.binary_type): self._content = self._content.encode() + + +class FakeModel(dict): + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError(key) diff --git a/openstackclient/tests/identity/v2_0/test_project.py b/openstackclient/tests/identity/v2_0/test_project.py index 18e862e..16ab195 100644 --- a/openstackclient/tests/identity/v2_0/test_project.py +++ b/openstackclient/tests/identity/v2_0/test_project.py @@ -15,7 +15,7 @@ import copy -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc from openstackclient.identity.v2_0 import project from openstackclient.tests import fakes @@ -223,7 +223,7 @@ class TestProjectCreate(TestProject): def test_project_create_or_show_exists(self): def _raise_conflict(*args, **kwargs): - raise ksc_exc.Conflict(None) + raise ks_exc.Conflict(None) # need to make this throw an exception... self.projects_mock.create.side_effect = _raise_conflict diff --git a/openstackclient/tests/identity/v2_0/test_role.py b/openstackclient/tests/identity/v2_0/test_role.py index 478a4ff..ec48433 100644 --- a/openstackclient/tests/identity/v2_0/test_role.py +++ b/openstackclient/tests/identity/v2_0/test_role.py @@ -16,7 +16,7 @@ import copy import mock -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc from openstackclient.common import exceptions from openstackclient.identity.v2_0 import role @@ -146,7 +146,7 @@ class TestRoleCreate(TestRole): def test_role_create_or_show_exists(self): def _raise_conflict(*args, **kwargs): - raise ksc_exc.Conflict(None) + raise ks_exc.Conflict(None) # need to make this throw an exception... self.roles_mock.create.side_effect = _raise_conflict diff --git a/openstackclient/tests/identity/v2_0/test_user.py b/openstackclient/tests/identity/v2_0/test_user.py index 6fe82f9..bf25681 100644 --- a/openstackclient/tests/identity/v2_0/test_user.py +++ b/openstackclient/tests/identity/v2_0/test_user.py @@ -16,7 +16,8 @@ import copy import mock -from keystoneclient import exceptions as ksc_exc +from keystoneauth1 import exceptions as ks_exc + from openstackclient.identity.v2_0 import user from openstackclient.tests import fakes from openstackclient.tests.identity.v2_0 import fakes as identity_fakes @@ -345,7 +346,7 @@ class TestUserCreate(TestUser): def test_user_create_or_show_exists(self): def _raise_conflict(*args, **kwargs): - raise ksc_exc.Conflict(None) + raise ks_exc.Conflict(None) # need to make this throw an exception... self.users_mock.create.side_effect = _raise_conflict diff --git a/openstackclient/tests/identity/v3/fakes.py b/openstackclient/tests/identity/v3/fakes.py index 9c4de9c..9fe341e 100644 --- a/openstackclient/tests/identity/v3/fakes.py +++ b/openstackclient/tests/identity/v3/fakes.py @@ -123,13 +123,11 @@ PROJECT_2 = { } region_id = 'region_one' -region_url = 'http://localhost:1111' region_parent_region_id = 'region_two' region_description = 'region one' REGION = { 'id': region_id, - 'url': region_url, 'description': region_description, 'parent_region_id': region_parent_region_id, 'links': base_url + 'regions/' + region_id, @@ -195,6 +193,8 @@ SERVICE_WITHOUT_NAME = { 'links': base_url + 'services/' + service_id, } +credential_id = 'c-123' + endpoint_id = 'e-123' endpoint_url = 'http://127.0.0.1:35357' endpoint_region = 'RegionOne' @@ -261,6 +261,7 @@ TOKEN_WITH_DOMAIN_ID = { idp_id = 'test_idp' idp_description = 'super exciting IdP description' idp_remote_ids = ['entity1', 'entity2'] +formatted_idp_remote_ids = 'entity1, entity2' IDENTITY_PROVIDER = { 'id': idp_id, @@ -400,6 +401,8 @@ class FakeIdentityv3Client(object): def __init__(self, **kwargs): self.domains = mock.Mock() self.domains.resource_class = fakes.FakeResource(None, {}) + self.credentials = mock.Mock() + self.credentials.resource_class = fakes.FakeResource(None, {}) self.endpoints = mock.Mock() self.endpoints.resource_class = fakes.FakeResource(None, {}) self.groups = mock.Mock() diff --git a/openstackclient/tests/identity/v3/test_credential.py b/openstackclient/tests/identity/v3/test_credential.py new file mode 100644 index 0000000..e2e690c --- /dev/null +++ b/openstackclient/tests/identity/v3/test_credential.py @@ -0,0 +1,112 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +from openstackclient.identity.v3 import credential +from openstackclient.tests.identity.v3 import fakes as identity_fakes +from openstackclient.tests import utils + + +class TestCredential(identity_fakes.TestIdentityv3): + data = { + "access": "abc123", + "secret": "hidden-message", + "trust_id": None + } + + def __init__(self, *args): + super(TestCredential, self).__init__(*args) + + self.json_data = json.dumps(self.data) + + def setUp(self): + super(TestCredential, self).setUp() + + # Get a shortcut to the CredentialManager Mock + self.credentials_mock = self.app.client_manager.identity.credentials + self.credentials_mock.reset_mock() + + # Get a shortcut to the UserManager Mock + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + +class TestCredentialSet(TestCredential): + def setUp(self): + super(TestCredentialSet, self).setUp() + self.cmd = credential.SetCredential(self.app, None) + + def test_credential_set_no_options(self): + arglist = [ + identity_fakes.credential_id, + ] + + self.assertRaises(utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_credential_set_missing_user(self): + arglist = [ + '--type', 'ec2', + '--data', self.json_data, + identity_fakes.credential_id, + ] + + self.assertRaises(utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_credential_set_missing_type(self): + arglist = [ + '--user', identity_fakes.user_name, + '--data', self.json_data, + identity_fakes.credential_id, + ] + + self.assertRaises(utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_credential_set_missing_data(self): + arglist = [ + '--user', identity_fakes.user_name, + '--type', 'ec2', + identity_fakes.credential_id, + ] + + self.assertRaises(utils.ParserException, + self.check_parser, self.cmd, arglist, []) + + def test_credential_set_valid(self): + arglist = [ + '--user', identity_fakes.user_name, + '--type', 'ec2', + '--data', self.json_data, + identity_fakes.credential_id, + ] + + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) + + def test_credential_set_valid_with_project(self): + arglist = [ + '--user', identity_fakes.user_name, + '--type', 'ec2', + '--data', self.json_data, + '--project', identity_fakes.project_name, + identity_fakes.credential_id, + ] + + parsed_args = self.check_parser(self.cmd, arglist, []) + self.cmd.take_action(parsed_args) diff --git a/openstackclient/tests/identity/v3/test_identity_provider.py b/openstackclient/tests/identity/v3/test_identity_provider.py index cd328c1..db097d5 100644 --- a/openstackclient/tests/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/identity/v3/test_identity_provider.py @@ -69,7 +69,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -103,7 +103,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -137,7 +137,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -172,7 +172,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -211,7 +211,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -253,7 +253,7 @@ class TestIdentityProviderCreate(TestIdentityProvider): None, False, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -363,7 +363,7 @@ class TestIdentityProviderShow(TestIdentityProvider): identity_fakes.idp_description, True, identity_fakes.idp_id, - identity_fakes.idp_remote_ids + identity_fakes.formatted_idp_remote_ids ) self.assertEqual(datalist, data) @@ -374,6 +374,50 @@ class TestIdentityProviderSet(TestIdentityProvider): super(TestIdentityProviderSet, self).setUp() self.cmd = identity_provider.SetIdentityProvider(self.app, None) + def test_identity_provider_set_description(self): + """Set Identity Provider's description. """ + def prepare(self): + """Prepare fake return objects before the test is executed""" + updated_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) + updated_idp['enabled'] = False + resources = fakes.FakeResource( + None, + updated_idp, + loaded=True + ) + self.identity_providers_mock.update.return_value = resources + + prepare(self) + new_description = 'new desc' + arglist = [ + '--description', new_description, + identity_fakes.idp_id + ] + verifylist = [ + ('identity_provider', identity_fakes.idp_id), + ('description', new_description), + ('enable', False), + ('disable', False), + ('remote_id', None) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.identity_providers_mock.update.assert_called_with( + identity_fakes.idp_id, + description=new_description + ) + + collist = ('description', 'enabled', 'id', 'remote_ids') + self.assertEqual(collist, columns) + datalist = ( + identity_fakes.idp_description, + False, + identity_fakes.idp_id, + identity_fakes.idp_remote_ids + ) + self.assertEqual(datalist, data) + def test_identity_provider_disable(self): """Disable Identity Provider @@ -398,6 +442,7 @@ class TestIdentityProviderSet(TestIdentityProvider): ] verifylist = [ ('identity_provider', identity_fakes.idp_id), + ('description', None), ('enable', False), ('disable', True), ('remote_id', identity_fakes.idp_remote_ids) @@ -443,6 +488,7 @@ class TestIdentityProviderSet(TestIdentityProvider): ] verifylist = [ ('identity_provider', identity_fakes.idp_id), + ('description', None), ('enable', True), ('disable', False), ('remote_id', identity_fakes.idp_remote_ids) @@ -488,6 +534,7 @@ class TestIdentityProviderSet(TestIdentityProvider): ] verifylist = [ ('identity_provider', identity_fakes.idp_id), + ('description', None), ('enable', True), ('disable', False), ('remote_id', [self.new_remote_id]) @@ -533,6 +580,7 @@ class TestIdentityProviderSet(TestIdentityProvider): ] verifylist = [ ('identity_provider', identity_fakes.idp_id), + ('description', None), ('enable', True), ('disable', False), ('remote_id_file', self.new_remote_id), diff --git a/openstackclient/tests/identity/v3/test_region.py b/openstackclient/tests/identity/v3/test_region.py index 7f6ced9..0ebbbec 100644 --- a/openstackclient/tests/identity/v3/test_region.py +++ b/openstackclient/tests/identity/v3/test_region.py @@ -61,19 +61,17 @@ class TestRegionCreate(TestRegion): 'description': identity_fakes.region_description, 'id': identity_fakes.region_id, 'parent_region': None, - 'url': None, } self.regions_mock.create.assert_called_with( **kwargs ) - collist = ('description', 'parent_region', 'region', 'url') + collist = ('description', 'parent_region', 'region') self.assertEqual(collist, columns) datalist = ( identity_fakes.region_description, identity_fakes.region_parent_region_id, identity_fakes.region_id, - identity_fakes.region_url, ) self.assertEqual(datalist, data) @@ -94,19 +92,17 @@ class TestRegionCreate(TestRegion): 'description': None, 'id': identity_fakes.region_id, 'parent_region': None, - 'url': None, } self.regions_mock.create.assert_called_with( **kwargs ) - collist = ('description', 'parent_region', 'region', 'url') + collist = ('description', 'parent_region', 'region') self.assertEqual(collist, columns) datalist = ( identity_fakes.region_description, identity_fakes.region_parent_region_id, identity_fakes.region_id, - identity_fakes.region_url, ) self.assertEqual(datalist, data) @@ -129,54 +125,17 @@ class TestRegionCreate(TestRegion): 'description': None, 'id': identity_fakes.region_id, 'parent_region': identity_fakes.region_parent_region_id, - 'url': None, } self.regions_mock.create.assert_called_with( **kwargs ) - collist = ('description', 'parent_region', 'region', 'url') + collist = ('description', 'parent_region', 'region') self.assertEqual(collist, columns) datalist = ( identity_fakes.region_description, identity_fakes.region_parent_region_id, identity_fakes.region_id, - identity_fakes.region_url, - ) - self.assertEqual(datalist, data) - - def test_region_create_url(self): - arglist = [ - identity_fakes.region_id, - '--url', identity_fakes.region_url, - ] - verifylist = [ - ('region', identity_fakes.region_id), - ('url', identity_fakes.region_url), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - - # Set expected values - kwargs = { - 'description': None, - 'id': identity_fakes.region_id, - 'parent_region': None, - 'url': identity_fakes.region_url, - } - self.regions_mock.create.assert_called_with( - **kwargs - ) - - collist = ('description', 'parent_region', 'region', 'url') - self.assertEqual(collist, columns) - datalist = ( - identity_fakes.region_description, - identity_fakes.region_parent_region_id, - identity_fakes.region_id, - identity_fakes.region_url, ) self.assertEqual(datalist, data) @@ -233,13 +192,12 @@ class TestRegionList(TestRegion): columns, data = self.cmd.take_action(parsed_args) self.regions_mock.list.assert_called_with() - collist = ('Region', 'Parent Region', 'Description', 'URL') + collist = ('Region', 'Parent Region', 'Description') self.assertEqual(collist, columns) datalist = (( identity_fakes.region_id, identity_fakes.region_parent_region_id, identity_fakes.region_description, - identity_fakes.region_url, ), ) self.assertEqual(datalist, tuple(data)) @@ -257,13 +215,12 @@ class TestRegionList(TestRegion): self.regions_mock.list.assert_called_with( parent_region_id=identity_fakes.region_parent_region_id) - collist = ('Region', 'Parent Region', 'Description', 'URL') + collist = ('Region', 'Parent Region', 'Description') self.assertEqual(collist, columns) datalist = (( identity_fakes.region_id, identity_fakes.region_parent_region_id, identity_fakes.region_description, - identity_fakes.region_url, ), ) self.assertEqual(datalist, tuple(data)) @@ -319,29 +276,6 @@ class TestRegionSet(TestRegion): **kwargs ) - def test_region_set_url(self): - arglist = [ - '--url', 'new url', - identity_fakes.region_id, - ] - verifylist = [ - ('url', 'new url'), - ('region', identity_fakes.region_id), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.run(parsed_args) - self.assertEqual(0, result) - - # Set expected values - kwargs = { - 'url': 'new url', - } - self.regions_mock.update.assert_called_with( - identity_fakes.region_id, - **kwargs - ) - def test_region_set_parent_region_id(self): arglist = [ '--parent-region', 'new_parent', @@ -395,12 +329,11 @@ class TestRegionShow(TestRegion): identity_fakes.region_id, ) - collist = ('description', 'parent_region', 'region', 'url') + collist = ('description', 'parent_region', 'region') self.assertEqual(collist, columns) datalist = ( identity_fakes.region_description, identity_fakes.region_parent_region_id, identity_fakes.region_id, - identity_fakes.region_url, ) self.assertEqual(datalist, data) diff --git a/openstackclient/tests/identity/v3/test_role.py b/openstackclient/tests/identity/v3/test_role.py index 4a0ba06..8ad4b09 100644 --- a/openstackclient/tests/identity/v3/test_role.py +++ b/openstackclient/tests/identity/v3/test_role.py @@ -367,6 +367,39 @@ class TestRoleList(TestRole): ), ) self.assertEqual(datalist, tuple(data)) + def test_user_list_inherited(self): + arglist = [ + '--user', identity_fakes.user_id, + '--inherited', + ] + verifylist = [ + ('user', identity_fakes.user_id), + ('inherited', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'domain': 'default', + 'user': self.users_mock.get(), + 'os_inherit_extension_inherited': True, + } + # RoleManager.list(user=, group=, domain=, project=, **kwargs) + self.roles_mock.list.assert_called_with( + **kwargs + ) + + collist = ('ID', 'Name') + self.assertEqual(collist, columns) + datalist = (( + identity_fakes.role_id, + identity_fakes.role_name, + ), ) + self.assertEqual(datalist, tuple(data)) + def test_user_list_user(self): arglist = [ '--user', identity_fakes.user_id, @@ -383,6 +416,7 @@ class TestRoleList(TestRole): kwargs = { 'domain': 'default', 'user': self.users_mock.get(), + 'os_inherit_extension_inherited': False } # RoleManager.list(user=, group=, domain=, project=, **kwargs) self.roles_mock.list.assert_called_with( @@ -415,6 +449,7 @@ class TestRoleList(TestRole): kwargs = { 'domain': self.domains_mock.get(), 'user': self.users_mock.get(), + 'os_inherit_extension_inherited': False } # RoleManager.list(user=, group=, domain=, project=, **kwargs) self.roles_mock.list.assert_called_with( @@ -449,6 +484,7 @@ class TestRoleList(TestRole): kwargs = { 'domain': self.domains_mock.get(), 'group': self.groups_mock.get(), + 'os_inherit_extension_inherited': False } # RoleManager.list(user=, group=, domain=, project=, **kwargs) self.roles_mock.list.assert_called_with( @@ -483,6 +519,7 @@ class TestRoleList(TestRole): kwargs = { 'project': self.projects_mock.get(), 'user': self.users_mock.get(), + 'os_inherit_extension_inherited': False } # RoleManager.list(user=, group=, domain=, project=, **kwargs) self.roles_mock.list.assert_called_with( @@ -517,6 +554,7 @@ class TestRoleList(TestRole): kwargs = { 'project': self.projects_mock.get(), 'group': self.groups_mock.get(), + 'os_inherit_extension_inherited': False } # RoleManager.list(user=, group=, domain=, project=, **kwargs) self.roles_mock.list.assert_called_with( diff --git a/openstackclient/tests/image/v1/test_image.py b/openstackclient/tests/image/v1/test_image.py index a79df8b..4d964bd 100644 --- a/openstackclient/tests/image/v1/test_image.py +++ b/openstackclient/tests/image/v1/test_image.py @@ -261,16 +261,17 @@ class TestImageList(TestImage): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( - detailed=False, + detailed=True, marker=image_fakes.image_id, ) - collist = ('ID', 'Name') + collist = ('ID', 'Name', 'Status') self.assertEqual(collist, columns) datalist = (( image_fakes.image_id, image_fakes.image_name, + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -288,17 +289,18 @@ class TestImageList(TestImage): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( - detailed=False, + detailed=True, public=True, marker=image_fakes.image_id, ) - collist = ('ID', 'Name') + collist = ('ID', 'Name', 'Status') self.assertEqual(collist, columns) datalist = (( image_fakes.image_id, image_fakes.image_name, + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -316,17 +318,18 @@ class TestImageList(TestImage): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( - detailed=False, + detailed=True, private=True, marker=image_fakes.image_id, ) - collist = ('ID', 'Name') + collist = ('ID', 'Name', 'Status') self.assertEqual(collist, columns) datalist = (( image_fakes.image_id, image_fakes.image_name, + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -401,12 +404,13 @@ class TestImageList(TestImage): property_field='properties', ) - collist = ('ID', 'Name') + collist = ('ID', 'Name', 'Status') self.assertEqual(columns, collist) datalist = (( image_fakes.image_id, image_fakes.image_name, + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -423,7 +427,7 @@ class TestImageList(TestImage): # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with( - detailed=False, + detailed=True, marker=image_fakes.image_id, ) si_mock.assert_called_with( @@ -431,12 +435,13 @@ class TestImageList(TestImage): 'name:asc' ) - collist = ('ID', 'Name') + collist = ('ID', 'Name', 'Status') self.assertEqual(collist, columns) datalist = (( image_fakes.image_id, - image_fakes.image_name + image_fakes.image_name, + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -499,8 +504,7 @@ class TestImageSet(TestImage): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) kwargs = { 'name': 'new-name', @@ -517,9 +521,6 @@ class TestImageSet(TestImage): **kwargs ) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) - def test_image_set_bools1(self): arglist = [ '--protected', @@ -644,8 +645,7 @@ class TestImageSet(TestImage): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) # VolumeManager.upload_to_image(volume, force, image_name, # container_format, disk_format) @@ -664,9 +664,6 @@ class TestImageSet(TestImage): volume='volly', ) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) - class TestImageShow(TestImage): diff --git a/openstackclient/tests/image/v2/fakes.py b/openstackclient/tests/image/v2/fakes.py index 1a9e301..11ad455 100644 --- a/openstackclient/tests/image/v2/fakes.py +++ b/openstackclient/tests/image/v2/fakes.py @@ -13,6 +13,7 @@ # under the License. # +import copy import mock from openstackclient.tests import fakes @@ -25,6 +26,7 @@ image_name = 'graven' image_owner = 'baal' image_protected = False image_visibility = 'public' +image_tags = [] IMAGE = { 'id': image_id, @@ -32,11 +34,16 @@ IMAGE = { 'owner': image_owner, 'protected': image_protected, 'visibility': image_visibility, + 'tags': image_tags } IMAGE_columns = tuple(sorted(IMAGE)) IMAGE_data = tuple((IMAGE[x] for x in sorted(IMAGE))) +IMAGE_SHOW = copy.copy(IMAGE) +IMAGE_SHOW['tags'] = '' +IMAGE_SHOW_data = tuple((IMAGE_SHOW[x] for x in sorted(IMAGE_SHOW))) + member_status = 'pending' MEMBER = { 'member_id': identity_fakes.project_id, @@ -117,6 +124,14 @@ IMAGE_schema = { "type": "string", "description": "Status of the image (READ-ONLY)" }, + "tags": { + "items": { + "type": "string", + "maxLength": 255 + }, + "type": "array", + "description": "List of strings related to the image" + }, "visibility": { "enum": [ "public", diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py index 65d5e55..be73c4c 100644 --- a/openstackclient/tests/image/v2/test_image.py +++ b/openstackclient/tests/image/v2/test_image.py @@ -96,7 +96,7 @@ class TestImageCreate(TestImage): ) self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data) @mock.patch('glanceclient.common.utils.get_data_file', name='Open') def test_image_reserve_options(self, mock_open): @@ -112,6 +112,7 @@ class TestImageCreate(TestImage): '--disk-format', 'fs', '--min-disk', '10', '--min-ram', '4', + '--owner', '123456', '--protected', '--private', image_fakes.image_name, @@ -121,6 +122,7 @@ class TestImageCreate(TestImage): ('disk_format', 'fs'), ('min_disk', 10), ('min_ram', 4), + ('owner', '123456'), ('protected', True), ('unprotected', False), ('public', False), @@ -139,6 +141,7 @@ class TestImageCreate(TestImage): disk_format='fs', min_disk=10, min_ram=4, + owner='123456', protected=True, visibility='private', ) @@ -151,7 +154,7 @@ class TestImageCreate(TestImage): ) self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data) @mock.patch('glanceclient.common.utils.get_data_file', name='Open') def test_image_create_file(self, mock_open): @@ -208,16 +211,15 @@ class TestImageCreate(TestImage): ) self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data) - def test_image_dead_options(self): + def test_image_create_dead_options(self): arglist = [ - '--owner', 'nobody', + '--store', 'somewhere', image_fakes.image_name, ] verifylist = [ - ('owner', 'nobody'), ('name', image_fakes.image_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -238,10 +240,8 @@ class TestAddProjectToImage(TestImage): copy.deepcopy(image_fakes.IMAGE), loaded=True, ) - self.image_members_mock.create.return_value = fakes.FakeResource( - None, + self.image_members_mock.create.return_value = fakes.FakeModel( copy.deepcopy(image_fakes.MEMBER), - loaded=True, ) self.project_mock.get.return_value = fakes.FakeResource( None, @@ -374,12 +374,13 @@ class TestImageList(TestImage): marker=image_fakes.image_id, ) - collist = ('ID', 'Name') + collist = ('ID', 'Name', 'Status') self.assertEqual(collist, columns) datalist = (( image_fakes.image_id, image_fakes.image_name, + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -402,12 +403,13 @@ class TestImageList(TestImage): marker=image_fakes.image_id, ) - collist = ('ID', 'Name') + collist = ('ID', 'Name', 'Status') self.assertEqual(collist, columns) datalist = (( image_fakes.image_id, image_fakes.image_name, + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -430,12 +432,13 @@ class TestImageList(TestImage): marker=image_fakes.image_id, ) - collist = ('ID', 'Name') + collist = ('ID', 'Name', 'Status') self.assertEqual(collist, columns) datalist = (( image_fakes.image_id, image_fakes.image_name, + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -458,12 +461,13 @@ class TestImageList(TestImage): marker=image_fakes.image_id, ) - collist = ('ID', 'Name') + collist = ('ID', 'Name', 'Status') self.assertEqual(columns, collist) datalist = (( image_fakes.image_id, image_fakes.image_name, + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -536,12 +540,13 @@ class TestImageList(TestImage): property_field='properties', ) - collist = ('ID', 'Name') + collist = ('ID', 'Name', 'Status') self.assertEqual(columns, collist) datalist = (( image_fakes.image_id, image_fakes.image_name, + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -565,12 +570,13 @@ class TestImageList(TestImage): 'name:asc' ) - collist = ('ID', 'Name') + collist = ('ID', 'Name', 'Status') self.assertEqual(collist, columns) datalist = (( image_fakes.image_id, - image_fakes.image_name + image_fakes.image_name, + '', ), ) self.assertEqual(datalist, tuple(data)) @@ -639,11 +645,10 @@ class TestRemoveProjectImage(TestImage): ) -class TestImageShow(TestImage): +class TestImageSet(TestImage): def setUp(self): - super(TestImageShow, self).setUp() - + super(TestImageSet, self).setUp() # Set up the schema self.model = warlock.model_factory( image_fakes.IMAGE_schema, @@ -651,33 +656,262 @@ class TestImageShow(TestImage): ) self.images_mock.get.return_value = self.model(**image_fakes.IMAGE) - + self.images_mock.update.return_value = self.model(**image_fakes.IMAGE) # Get the command object to test - self.cmd = image.ShowImage(self.app, None) + self.cmd = image.SetImage(self.app, None) - def test_image_show(self): + def test_image_set_options(self): arglist = [ + '--name', 'new-name', + '--owner', 'new-owner', + '--min-disk', '2', + '--min-ram', '4', + '--container-format', 'ovf', + '--disk-format', 'vmdk', image_fakes.image_id, ] verifylist = [ + ('name', 'new-name'), + ('owner', 'new-owner'), + ('min_disk', 2), + ('min_ram', 4), + ('container_format', 'ovf'), + ('disk_format', 'vmdk'), ('image', image_fakes.image_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples - columns, data = self.cmd.take_action(parsed_args) - self.images_mock.get.assert_called_with( + self.cmd.take_action(parsed_args) + + kwargs = { + 'name': 'new-name', + 'owner': 'new-owner', + 'min_disk': 2, + 'min_ram': 4, + 'container_format': 'ovf', + 'disk_format': 'vmdk', + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, **kwargs) + + def test_image_set_bools1(self): + arglist = [ + '--protected', + '--private', + image_fakes.image_name, + ] + verifylist = [ + ('protected', True), + ('unprotected', False), + ('public', False), + ('private', True), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'protected': True, + 'visibility': 'private', + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( image_fakes.image_id, + **kwargs ) - self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + def test_image_set_bools2(self): + arglist = [ + '--unprotected', + '--public', + image_fakes.image_name, + ] + verifylist = [ + ('protected', False), + ('unprotected', True), + ('public', True), + ('private', False), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) -class TestImageSet(TestImage): + kwargs = { + 'protected': False, + 'visibility': 'public', + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + def test_image_set_properties(self): + arglist = [ + '--property', 'Alpha=1', + '--property', 'Beta=2', + image_fakes.image_name, + ] + verifylist = [ + ('properties', {'Alpha': '1', 'Beta': '2'}), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'Alpha': '1', + 'Beta': '2', + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + def test_image_set_fake_properties(self): + arglist = [ + '--architecture', 'z80', + '--instance-id', '12345', + '--kernel-id', '67890', + '--os-distro', 'cpm', + '--os-version', '2.2H', + '--ramdisk-id', 'xyzpdq', + image_fakes.image_name, + ] + verifylist = [ + ('architecture', 'z80'), + ('instance_id', '12345'), + ('kernel_id', '67890'), + ('os_distro', 'cpm'), + ('os_version', '2.2H'), + ('ramdisk_id', 'xyzpdq'), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'architecture': 'z80', + 'instance_id': '12345', + 'kernel_id': '67890', + 'os_distro': 'cpm', + 'os_version': '2.2H', + 'ramdisk_id': 'xyzpdq', + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + def test_image_set_tag(self): + arglist = [ + '--tag', 'test-tag', + image_fakes.image_name, + ] + verifylist = [ + ('tags', ['test-tag']), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'tags': ['test-tag'], + } + # ImageManager.update(image, **kwargs) + self.images_mock.update.assert_called_with( + image_fakes.image_id, + **kwargs + ) + + def test_image_set_tag_merge(self): + old_image = copy.copy(image_fakes.IMAGE) + old_image['tags'] = ['old1', 'new2'] + self.images_mock.get.return_value = self.model(**old_image) + arglist = [ + '--tag', 'test-tag', + image_fakes.image_name, + ] + verifylist = [ + ('tags', ['test-tag']), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'tags': ['old1', 'new2', 'test-tag'], + } + # ImageManager.update(image, **kwargs) + a, k = self.images_mock.update.call_args + self.assertEqual(image_fakes.image_id, a[0]) + self.assertTrue('tags' in k) + self.assertEqual(set(kwargs['tags']), set(k['tags'])) + + def test_image_set_tag_merge_dupe(self): + old_image = copy.copy(image_fakes.IMAGE) + old_image['tags'] = ['old1', 'new2'] + self.images_mock.get.return_value = self.model(**old_image) + arglist = [ + '--tag', 'old1', + image_fakes.image_name, + ] + verifylist = [ + ('tags', ['old1']), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + self.cmd.take_action(parsed_args) + + kwargs = { + 'tags': ['new2', 'old1'], + } + # ImageManager.update(image, **kwargs) + a, k = self.images_mock.update.call_args + self.assertEqual(image_fakes.image_id, a[0]) + self.assertTrue('tags' in k) + self.assertEqual(set(kwargs['tags']), set(k['tags'])) + + def test_image_set_dead_options(self): + + arglist = [ + '--visibility', '1-mile', + image_fakes.image_name, + ] + verifylist = [ + ('visibility', '1-mile'), + ('image', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + + +class TestImageShow(TestImage): def setUp(self): - super(TestImageSet, self).setUp() + super(TestImageShow, self).setUp() + # Set up the schema self.model = warlock.model_factory( image_fakes.IMAGE_schema, @@ -685,39 +919,24 @@ class TestImageSet(TestImage): ) self.images_mock.get.return_value = self.model(**image_fakes.IMAGE) - self.images_mock.update.return_value = self.model(**image_fakes.IMAGE) + # Get the command object to test - self.cmd = image.SetImage(self.app, None) + self.cmd = image.ShowImage(self.app, None) - def test_image_set_options(self): + def test_image_show(self): arglist = [ - '--name', 'new-name', - '--owner', 'new-owner', - '--min-disk', '2', - '--min-ram', '4', image_fakes.image_id, ] verifylist = [ - ('name', 'new-name'), - ('owner', 'new-owner'), - ('min_disk', 2), - ('min_ram', 4), ('image', image_fakes.image_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # DisplayCommandBase.take_action() returns two tuples columns, data = self.cmd.take_action(parsed_args) - - kwargs = { - 'name': 'new-name', - 'owner': 'new-owner', - 'min_disk': 2, - 'min_ram': 4 - } - # ImageManager.update(image, **kwargs) - self.images_mock.update.assert_called_with( - image_fakes.image_id, **kwargs) + self.images_mock.get.assert_called_with( + image_fakes.image_id, + ) self.assertEqual(image_fakes.IMAGE_columns, columns) - self.assertEqual(image_fakes.IMAGE_data, data) + self.assertEqual(image_fakes.IMAGE_SHOW_data, data) diff --git a/openstackclient/tests/object/v1/fakes.py b/openstackclient/tests/object/v1/fakes.py index 6aef05b..986ab2f 100644 --- a/openstackclient/tests/object/v1/fakes.py +++ b/openstackclient/tests/object/v1/fakes.py @@ -13,7 +13,7 @@ # under the License. # -from keystoneclient import session +from keystoneauth1 import session from openstackclient.api import object_store_v1 as object_store from openstackclient.tests import utils diff --git a/openstackclient/tests/object/v1/test_container_all.py b/openstackclient/tests/object/v1/test_container_all.py index 8b200e0..69fc0f3 100644 --- a/openstackclient/tests/object/v1/test_container_all.py +++ b/openstackclient/tests/object/v1/test_container_all.py @@ -286,7 +286,6 @@ class TestContainerShow(TestContainerAll): def test_object_show_container(self): headers = { - 'x-container-meta-owner': object_fakes.ACCOUNT_ID, 'x-container-object-count': '42', 'x-container-bytes-used': '123', 'x-container-read': 'qaz', diff --git a/openstackclient/tests/volume/v1/test_volume.py b/openstackclient/tests/volume/v1/test_volume.py index f73260e..70ff50d 100644 --- a/openstackclient/tests/volume/v1/test_volume.py +++ b/openstackclient/tests/volume/v1/test_volume.py @@ -527,6 +527,188 @@ class TestVolumeCreate(TestVolume): self.assertEqual(datalist, data) +class TestVolumeList(TestVolume): + + def setUp(self): + super(TestVolumeList, self).setUp() + + self.volumes_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(volume_fakes.VOLUME), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = volume.ListVolume(self.app, None) + + def test_volume_list_no_options(self): + arglist = [] + verifylist = [ + ('long', False), + ('all_projects', False), + ('name', None), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, columns) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + '', + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_name(self): + arglist = [ + '--name', volume_fakes.volume_name, + ] + verifylist = [ + ('long', False), + ('all_projects', False), + ('name', volume_fakes.volume_name), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + '', + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_status(self): + arglist = [ + '--status', volume_fakes.volume_status, + ] + verifylist = [ + ('long', False), + ('all_projects', False), + ('name', None), + ('status', volume_fakes.volume_status), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + '', + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_all_projects(self): + arglist = [ + '--all-projects', + ] + verifylist = [ + ('long', False), + ('all_projects', True), + ('name', None), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, columns) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + '', + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ('all_projects', False), + ('name', None), + ('status', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Type', + 'Bootable', + 'Attached to', + 'Properties', + ) + self.assertEqual(collist, columns) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + volume_fakes.volume_type, + '', + '', + "Alpha='a', Beta='b', Gamma='g'", + ), ) + self.assertEqual(datalist, tuple(data)) + + class TestVolumeSet(TestVolume): def setUp(self): diff --git a/openstackclient/tests/volume/v2/fakes.py b/openstackclient/tests/volume/v2/fakes.py index 7b7758a..60cec33 100644 --- a/openstackclient/tests/volume/v2/fakes.py +++ b/openstackclient/tests/volume/v2/fakes.py @@ -14,9 +14,11 @@ import copy import mock +import random +import uuid from openstackclient.tests import fakes -from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes from openstackclient.tests.image.v2 import fakes as image_fakes from openstackclient.tests import utils @@ -212,7 +214,7 @@ class TestVolume(utils.TestCommand): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) - self.app.client_manager.identity = identity_fakes.FakeIdentityv2Client( + self.app.client_manager.identity = identity_fakes.FakeIdentityv3Client( endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) @@ -220,3 +222,85 @@ class TestVolume(utils.TestCommand): endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN ) + + +class FakeVolume(object): + """Fake one or more volumes. + + TODO(xiexs): Currently, only volume API v2 is supported by this class. + """ + + @staticmethod + def create_one_volume(attrs={}): + """Create a fake volume. + + :param Dictionary attrs: + A dictionary with all attributes of volume + :retrun: + A FakeResource object with id, name, status, etc. + """ + # Set default attribute + volume_info = { + 'id': 'volume-id' + uuid.uuid4().hex, + 'name': 'volume-name' + uuid.uuid4().hex, + 'description': 'description' + uuid.uuid4().hex, + 'status': random.choice(['available', 'in_use']), + 'size': random.randint(1, 20), + 'volume_type': + random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), + 'metadata': { + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, + 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex}, + 'snapshot_id': random.randint(1, 5), + 'availability_zone': 'zone' + uuid.uuid4().hex, + 'attachments': { + 'device': '/dev/' + uuid.uuid4().hex, + 'server_id': uuid.uuid4().hex}, + } + + # Overwrite default attributes if there are some attributes set + volume_info.update(attrs) + + volume = fakes.FakeResource( + None, + volume_info, + loaded=True) + return volume + + @staticmethod + def create_volumes(attrs={}, count=2): + """Create multiple fake volumes. + + :param Dictionary attrs: + A dictionary with all attributes of volume + :param Integer count: + The number of volumes to be faked + :return: + A list of FakeResource objects + """ + volumes = [] + for n in range(0, count): + volumes.append(FakeVolume.create_one_volume(attrs)) + + return volumes + + @staticmethod + def get_volumes(volumes=None, count=2): + """Get an iterable MagicMock object with a list of faked volumes. + + If volumes list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List volumes: + A list of FakeResource objects faking volumes + :param Integer count: + The number of volumes to be faked + :return + An iterable Mock object with side_effect set to a list of faked + volumes + """ + if volumes is None: + volumes = FakeVolume.create_volumes(count) + + return mock.MagicMock(side_effect=volumes) diff --git a/openstackclient/tests/volume/v2/test_snapshot.py b/openstackclient/tests/volume/v2/test_snapshot.py index 3ceb57f..3b30d4e 100644 --- a/openstackclient/tests/volume/v2/test_snapshot.py +++ b/openstackclient/tests/volume/v2/test_snapshot.py @@ -225,6 +225,7 @@ class TestSnapshotList(TestSnapshot): def test_snapshot_list_without_options(self): arglist = [] verifylist = [ + ('all_projects', False), ("long", False) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -243,7 +244,7 @@ class TestSnapshotList(TestSnapshot): def test_snapshot_list_with_options(self): arglist = ["--long"] - verifylist = [("long", True)] + verifylist = [("long", True), ('all_projects', False)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -263,3 +264,33 @@ class TestSnapshotList(TestSnapshot): volume_fakes.EXPECTED_SNAPSHOT.get("properties") ),) self.assertEqual(datalist, tuple(data)) + + def test_snapshot_list_all_projects(self): + arglist = [ + '--all-projects', + ] + verifylist = [ + ('long', False), + ('all_projects', True) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = [ + "ID", + "Name", + "Description", + "Status", + "Size" + ] + self.assertEqual(collist, columns) + + datalist = (( + volume_fakes.snapshot_id, + volume_fakes.snapshot_name, + volume_fakes.snapshot_description, + "available", + volume_fakes.snapshot_size + ), ) + self.assertEqual(datalist, tuple(data)) diff --git a/openstackclient/tests/volume/v2/test_type.py b/openstackclient/tests/volume/v2/test_type.py index 9a07263..c63cd1f 100644 --- a/openstackclient/tests/volume/v2/test_type.py +++ b/openstackclient/tests/volume/v2/test_type.py @@ -73,7 +73,7 @@ class TestTypeCreate(TestType): self.types_mock.create.assert_called_with( volume_fakes.type_name, description=volume_fakes.type_description, - public=True, + is_public=True, ) collist = ( @@ -93,7 +93,7 @@ class TestTypeCreate(TestType): arglist = [ volume_fakes.type_name, "--description", volume_fakes.type_description, - "--private" + "--private", ] verifylist = [ ("name", volume_fakes.type_name), @@ -107,7 +107,7 @@ class TestTypeCreate(TestType): self.types_mock.create.assert_called_with( volume_fakes.type_name, description=volume_fakes.type_description, - private=True, + is_public=False, ) collist = ( diff --git a/openstackclient/tests/volume/v2/test_volume.py b/openstackclient/tests/volume/v2/test_volume.py index b15fd02..70324b6 100644 --- a/openstackclient/tests/volume/v2/test_volume.py +++ b/openstackclient/tests/volume/v2/test_volume.py @@ -15,7 +15,7 @@ import copy from openstackclient.tests import fakes -from openstackclient.tests.identity.v2_0 import fakes as identity_fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes from openstackclient.tests.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume @@ -27,7 +27,7 @@ class TestVolume(volume_fakes.TestVolume): self.volumes_mock = self.app.client_manager.volume.volumes self.volumes_mock.reset_mock() - self.projects_mock = self.app.client_manager.identity.tenants + self.projects_mock = self.app.client_manager.identity.projects self.projects_mock.reset_mock() self.users_mock = self.app.client_manager.identity.users @@ -560,32 +560,145 @@ class TestVolumeList(TestVolume): ), ) self.assertEqual(datalist, tuple(data)) - def test_volume_list_all_projects_option(self): + def test_volume_list_project(self): arglist = [ - '--all-projects', + '--project', identity_fakes.project_name, ] verifylist = [ + ('project', identity_fakes.project_name), ('long', False), - ('all_projects', True), - ('name', None), + ('all_projects', False), ('status', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - collist = [ + collist = ( 'ID', 'Display Name', 'Status', 'Size', 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_project_domain(self): + arglist = [ + '--project', identity_fakes.project_name, + '--project-domain', identity_fakes.domain_name, ] - self.assertEqual(collist, columns) + verifylist = [ + ('project', identity_fakes.project_name), + ('project_domain', identity_fakes.domain_name), + ('long', False), + ('all_projects', False), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_user(self): + arglist = [ + '--user', identity_fakes.user_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('long', False), + ('all_projects', False), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) server = volume_fakes.volume_attachment_server['server_id'] device = volume_fakes.volume_attachment_server['device'] msg = 'Attached to %s on %s ' % (server, device) + + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + + def test_volume_list_user_domain(self): + arglist = [ + '--user', identity_fakes.user_name, + '--user-domain', identity_fakes.domain_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('user_domain', identity_fakes.domain_name), + ('long', False), + ('all_projects', False), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = ( + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ) + self.assertEqual(collist, tuple(columns)) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + datalist = (( volume_fakes.volume_id, volume_fakes.volume_name, @@ -666,6 +779,41 @@ class TestVolumeList(TestVolume): ), ) self.assertEqual(datalist, tuple(data)) + def test_volume_list_all_projects(self): + arglist = [ + '--all-projects', + ] + verifylist = [ + ('long', False), + ('all_projects', True), + ('name', None), + ('status', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + collist = [ + 'ID', + 'Display Name', + 'Status', + 'Size', + 'Attached to', + ] + self.assertEqual(collist, columns) + + server = volume_fakes.volume_attachment_server['server_id'] + device = volume_fakes.volume_attachment_server['device'] + msg = 'Attached to %s on %s ' % (server, device) + datalist = (( + volume_fakes.volume_id, + volume_fakes.volume_name, + volume_fakes.volume_status, + volume_fakes.volume_size, + msg, + ), ) + self.assertEqual(datalist, tuple(data)) + def test_volume_list_long(self): arglist = [ '--long', diff --git a/openstackclient/volume/v1/snapshot.py b/openstackclient/volume/v1/snapshot.py index de7bb5b..93e17eb 100644 --- a/openstackclient/volume/v1/snapshot.py +++ b/openstackclient/volume/v1/snapshot.py @@ -111,6 +111,12 @@ class ListSnapshot(lister.Lister): def get_parser(self, prog_name): parser = super(ListSnapshot, self).get_parser(prog_name) parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help='Include all projects (admin only)', + ) + parser.add_argument( '--long', action='store_true', default=False, @@ -157,7 +163,12 @@ class ListSnapshot(lister.Lister): # Just forget it if there's any trouble pass - data = self.app.client_manager.volume.volume_snapshots.list() + search_opts = { + 'all_tenants': parsed_args.all_projects, + } + + data = self.app.client_manager.volume.volume_snapshots.list( + search_opts=search_opts) return (column_headers, (utils.get_item_properties( s, columns, diff --git a/openstackclient/volume/v1/volume.py b/openstackclient/volume/v1/volume.py index 52b0eb2..0691d88 100644 --- a/openstackclient/volume/v1/volume.py +++ b/openstackclient/volume/v1/volume.py @@ -207,16 +207,16 @@ class ListVolume(lister.Lister): def get_parser(self, prog_name): parser = super(ListVolume, self).get_parser(prog_name) parser.add_argument( + '--name', + metavar='<name>', + help='Filter results by volume name', + ) + parser.add_argument( '--status', metavar='<status>', help='Filter results by status', ) parser.add_argument( - '--name', - metavar='<name>', - help='Filter results by name', - ) - parser.add_argument( '--all-projects', action='store_true', default=False, @@ -239,7 +239,7 @@ class ListVolume(lister.Lister): def _format_attach(attachments): """Return a formatted string of a volume's attached instances - :param volume: a volume.attachments field + :param attachments: a volume.attachments field :rtype: a string of formatted instances """ diff --git a/openstackclient/volume/v2/snapshot.py b/openstackclient/volume/v2/snapshot.py index bbc92c4..aa7630a 100644 --- a/openstackclient/volume/v2/snapshot.py +++ b/openstackclient/volume/v2/snapshot.py @@ -108,6 +108,12 @@ class ListSnapshot(lister.Lister): def get_parser(self, prog_name): parser = super(ListSnapshot, self).get_parser(prog_name) parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help='Include all projects (admin only)', + ) + parser.add_argument( '--long', action='store_true', default=False, @@ -149,7 +155,12 @@ class ListSnapshot(lister.Lister): # Just forget it if there's any trouble pass - data = self.app.client_manager.volume.volume_snapshots.list() + search_opts = { + 'all_tenants': parsed_args.all_projects, + } + + data = self.app.client_manager.volume.volume_snapshots.list( + search_opts=search_opts) return (column_headers, (utils.get_item_properties( s, columns, diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index ad6215e..925b01d 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -16,7 +16,6 @@ import copy import logging -import os from cliff import command from cliff import lister @@ -25,6 +24,7 @@ import six from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.identity import common as identity_common class CreateVolume(show.ShowOne): @@ -175,7 +175,7 @@ class DeleteVolume(command.Command): action="store_true", default=False, help="Attempt forced removal of volume(s), regardless of state " - "(defaults to False" + "(defaults to False)" ) return parser @@ -185,10 +185,10 @@ class DeleteVolume(command.Command): for volume in parsed_args.volumes: volume_obj = utils.find_resource( volume_client.volumes, volume) - if parsed_args.force: - volume_client.volumes.force_delete(volume_obj.id) - else: - volume_client.volumes.delete(volume_obj.id) + if parsed_args.force: + volume_client.volumes.force_delete(volume_obj.id) + else: + volume_client.volumes.delete(volume_obj.id) return @@ -200,27 +200,39 @@ class ListVolume(lister.Lister): def get_parser(self, prog_name): parser = super(ListVolume, self).get_parser(prog_name) parser.add_argument( - '--all-projects', - action='store_true', - default=bool(int(os.environ.get("ALL_PROJECTS", 0))), - help='Include all projects (admin only)', + '--project', + metavar='<project-id>', + help='Filter results by project (name or ID) (admin only)' ) + identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( - '--long', - action='store_true', - default=False, - help='List additional fields in output', + '--user', + metavar='<user-id>', + help='Filter results by user (name or ID) (admin only)' ) + identity_common.add_user_domain_option_to_parser(parser) parser.add_argument( '--name', metavar='<name>', - help='Filter results by name', + help='Filter results by volume name', ) parser.add_argument( '--status', metavar='<status>', help='Filter results by status', ) + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help='Include all projects (admin only)', + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help='List additional fields in output', + ) return parser @utils.log_method(log) @@ -228,11 +240,12 @@ class ListVolume(lister.Lister): volume_client = self.app.client_manager.volume compute_client = self.app.client_manager.compute + identity_client = self.app.client_manager.identity def _format_attach(attachments): """Return a formatted string of a volume's attached instances - :param volume: a volume.attachments field + :param attachments: a volume.attachments field :rtype: a string of formatted instances """ @@ -282,8 +295,23 @@ class ListVolume(lister.Lister): # Just forget it if there's any trouble pass + project_id = None + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain) + + user_id = None + if parsed_args.user: + user_id = identity_common.find_user(identity_client, + parsed_args.user, + parsed_args.user_domain) + search_opts = { - 'all_projects': parsed_args.all_projects, + 'all_tenants': parsed_args.all_projects, + 'project_id': project_id, + 'user_id': user_id, 'display_name': parsed_args.name, 'status': parsed_args.status, } diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index 8cca86f..583e6ed 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -73,9 +73,9 @@ class CreateVolumeType(show.ShowOne): kwargs = {} if parsed_args.public: - kwargs['public'] = True + kwargs['is_public'] = True if parsed_args.private: - kwargs['private'] = True + kwargs['is_public'] = False volume_type = volume_client.volume_types.create( parsed_args.name, diff --git a/post_test_hook.sh b/post_test_hook.sh index 4c35b52..7bb036f 100755 --- a/post_test_hook.sh +++ b/post_test_hook.sh @@ -12,7 +12,7 @@ OPENSTACKCLIENT_DIR=$(cd $(dirname "$0") && pwd) echo "Running openstackclient functional test suite" sudo -H -u stack -i <<! -source ~stack/devstack/accrc/admin/admin +source ~stack/devstack/openrc admin admin echo 'Running tests with:' env | grep OS_ cd ${OPENSTACKCLIENT_DIR} diff --git a/python-openstackclient/locale/de/LC_MESSAGES/python-openstackclient.po b/python-openstackclient/locale/de/LC_MESSAGES/python-openstackclient.po index 6758943..983abdb 100644 --- a/python-openstackclient/locale/de/LC_MESSAGES/python-openstackclient.po +++ b/python-openstackclient/locale/de/LC_MESSAGES/python-openstackclient.po @@ -9,18 +9,18 @@ # OpenStack Infra <zanata@openstack.org>, 2015. #zanata msgid "" msgstr "" -"Project-Id-Version: python-openstackclient 1.6.1.dev56\n" +"Project-Id-Version: python-openstackclient 1.8.1.dev15\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2015-09-21 06:09+0000\n" +"POT-Creation-Date: 2015-11-04 06:04+0000\n" "PO-Revision-Date: 2015-09-19 07:10+0000\n" "Last-Translator: Andreas Jaeger <jaegerandi@gmail.com>\n" -"Language-Team: German (http://www.transifex.com/openstack/python-" -"openstackclient/language/de/)\n" +"Language: de\n" +"Language-Team: German\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.0\n" +"Generated-By: Babel 2.1.1\n" msgid "" "\n" @@ -279,9 +279,6 @@ msgstr "Neue Regionskennung" msgid "New region description" msgstr "Beschreibung des neuen Bereichs" -msgid "New region url" -msgstr "URL des neuen Bereichs" - msgid "New role name" msgstr "Name der neuen Rolle" diff --git a/python-openstackclient/locale/python-openstackclient.pot b/python-openstackclient/locale/python-openstackclient.pot index f613a5a..cc4f1a1 100644 --- a/python-openstackclient/locale/python-openstackclient.pot +++ b/python-openstackclient/locale/python-openstackclient.pot @@ -7,16 +7,16 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: python-openstackclient 1.6.1.dev43\n" +"Project-Id-Version: python-openstackclient 1.8.1.dev15\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2015-09-11 06:10+0000\n" +"POT-Creation-Date: 2015-11-04 06:04+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.0\n" +"Generated-By: Babel 2.1.1\n" #: openstackclient/api/auth.py:144 msgid "Set a username with --os-username, OS_USERNAME, or auth.username\n" @@ -756,32 +756,27 @@ msgid "Parent region ID" msgstr "" #: openstackclient/identity/v3/region.py:49 -#: openstackclient/identity/v3/region.py:151 +#: openstackclient/identity/v3/region.py:144 msgid "New region description" msgstr "" -#: openstackclient/identity/v3/region.py:54 -#: openstackclient/identity/v3/region.py:156 -msgid "New region url" -msgstr "" - -#: openstackclient/identity/v3/region.py:86 +#: openstackclient/identity/v3/region.py:79 msgid "Region ID to delete" msgstr "" -#: openstackclient/identity/v3/region.py:108 +#: openstackclient/identity/v3/region.py:101 msgid "Filter by parent region ID" msgstr "" -#: openstackclient/identity/v3/region.py:141 +#: openstackclient/identity/v3/region.py:134 msgid "Region to modify" msgstr "" -#: openstackclient/identity/v3/region.py:146 +#: openstackclient/identity/v3/region.py:139 msgid "New parent region ID" msgstr "" -#: openstackclient/identity/v3/region.py:191 +#: openstackclient/identity/v3/region.py:175 msgid "Region to display" msgstr "" diff --git a/python-openstackclient/locale/zh_TW/LC_MESSAGES/python-openstackclient.po b/python-openstackclient/locale/zh_TW/LC_MESSAGES/python-openstackclient.po index 0680a23..301912c 100644 --- a/python-openstackclient/locale/zh_TW/LC_MESSAGES/python-openstackclient.po +++ b/python-openstackclient/locale/zh_TW/LC_MESSAGES/python-openstackclient.po @@ -4,20 +4,21 @@ # python-openstackclient project. # # Translators: +# OpenStack Infra <zanata@openstack.org>, 2015. #zanata msgid "" msgstr "" -"Project-Id-Version: python-openstackclient\n" +"Project-Id-Version: python-openstackclient 1.8.1.dev15\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2015-08-04 06:26+0000\n" -"PO-Revision-Date: 2015-06-14 18:41+0000\n" +"POT-Creation-Date: 2015-11-04 06:04+0000\n" +"PO-Revision-Date: 2015-06-14 06:41+0000\n" "Last-Translator: openstackjenkins <jenkins@openstack.org>\n" -"Language-Team: Chinese (Taiwan) (http://www.transifex.com/openstack/python-" -"openstackclient/language/zh_TW/)\n" +"Language: zh_Hant_TW\n" +"Language-Team: Chinese (Taiwan)\n" "Plural-Forms: nplurals=1; plural=0\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.0\n" +"Generated-By: Babel 2.1.1\n" msgid "" "\n" @@ -227,9 +228,6 @@ msgstr "新地區識別號" msgid "New region description" msgstr "新地區描述" -msgid "New region url" -msgstr "新地區網址" - msgid "New role name" msgstr "新角色名稱" diff --git a/releasenotes/notes/.placeholder b/releasenotes/notes/.placeholder new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/releasenotes/notes/.placeholder diff --git a/releasenotes/notes/add-ksa-7f0795157d93a898.yaml b/releasenotes/notes/add-ksa-7f0795157d93a898.yaml new file mode 100644 index 0000000..c777469 --- /dev/null +++ b/releasenotes/notes/add-ksa-7f0795157d93a898.yaml @@ -0,0 +1,5 @@ +--- +other: + - | + Add dependency on keystoneauth1 module to perform authentication in place + of python-keystoneclient. diff --git a/releasenotes/notes/bug-1475831-3caafd724d4ed644.yaml b/releasenotes/notes/bug-1475831-3caafd724d4ed644.yaml new file mode 100644 index 0000000..dbe5285 --- /dev/null +++ b/releasenotes/notes/bug-1475831-3caafd724d4ed644.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Some compute quotas were not being set + [Bug `1475831 <https://bugs.launchpad.net/bugs/1475831>`_] diff --git a/releasenotes/notes/bug-1517386-1c0aad8e3203b02b.yaml b/releasenotes/notes/bug-1517386-1c0aad8e3203b02b.yaml new file mode 100644 index 0000000..5353a25 --- /dev/null +++ b/releasenotes/notes/bug-1517386-1c0aad8e3203b02b.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Add `--all` for `snapshot list` + [Bug `1517386 <https://bugs.launchpad.net/bugs/1517386>`_] diff --git a/releasenotes/notes/bug-1519181-932c1ff07ef16666.yaml b/releasenotes/notes/bug-1519181-932c1ff07ef16666.yaml new file mode 100644 index 0000000..919b0ec --- /dev/null +++ b/releasenotes/notes/bug-1519181-932c1ff07ef16666.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Add Status column to default `image list` output (v1 and v2) + [Bug `1519181 <https://bugs.launchpad.net/bugs/1519181>`_] + diff --git a/releasenotes/notes/bug-1519510-3cde4643b33ebb7a.yaml b/releasenotes/notes/bug-1519510-3cde4643b33ebb7a.yaml new file mode 100644 index 0000000..7476b4f --- /dev/null +++ b/releasenotes/notes/bug-1519510-3cde4643b33ebb7a.yaml @@ -0,0 +1,5 @@ +--- +other: + - | + Drop Python 2.6 support + [Bug `1519510 <https://bugs.launchpad.net/bugs/1519510>`_] diff --git a/releasenotes/notes/bug-1520115-0367e1c8917dc912.yaml b/releasenotes/notes/bug-1520115-0367e1c8917dc912.yaml new file mode 100644 index 0000000..98829c7 --- /dev/null +++ b/releasenotes/notes/bug-1520115-0367e1c8917dc912.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fix `volume type create`, passes incorrect privacy argument + [Bug `1520115 <https://bugs.launchpad.net/bugs/1520115>`_] diff --git a/releasenotes/notes/bug-1520541-44d45e4693089c03.yaml b/releasenotes/notes/bug-1520541-44d45e4693089c03.yaml new file mode 100644 index 0000000..d3e4455 --- /dev/null +++ b/releasenotes/notes/bug-1520541-44d45e4693089c03.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + `volume delete` only deletes last volume specified + [Bug `1520541 <https://bugs.launchpad.net/bugs/1520541>`_] diff --git a/releasenotes/notes/no-project-usage-list-e88eb49aa2e96cf7.yaml b/releasenotes/notes/no-project-usage-list-e88eb49aa2e96cf7.yaml new file mode 100644 index 0000000..67c5870 --- /dev/null +++ b/releasenotes/notes/no-project-usage-list-e88eb49aa2e96cf7.yaml @@ -0,0 +1,3 @@ +--- +upgrade: + - Removed the deprecated command `project usage list` in favor of `usage list` diff --git a/releasenotes/notes/server-changes-3962541b6ebdbbd8.yaml b/releasenotes/notes/server-changes-3962541b6ebdbbd8.yaml new file mode 100644 index 0000000..ae3739b --- /dev/null +++ b/releasenotes/notes/server-changes-3962541b6ebdbbd8.yaml @@ -0,0 +1,10 @@ +--- +other: + - | + `server` resource changes: + + * Added `--limit` and `--marker` to `server list` + * Added `server shelve` + * Added `server unshelve` + * `server resume` now takes multiple servers + * `server suspend` now takes multiple servers diff --git a/releasenotes/source/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/releasenotes/source/_static/.placeholder diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/releasenotes/source/_templates/.placeholder diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 0000000..1b6929e --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# OpenStackClient Release Notes documentation build configuration file, created +# by sphinx-quickstart on Tue Nov 3 17:40:50 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'oslosphinx', + 'reno.sphinxext', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'OpenStackClient Release Notes' +copyright = u'2015, OpenStackClient Developers' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +from openstackclient import version_info as openstackclient_version +# The full version, including alpha/beta/rc tags. +release = openstackclient_version.version_string_with_vcs() +# The short X.Y version. +version = openstackclient_version.canonical_version_string() + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'OpenStackClientReleaseNotesdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [( + 'index', + 'OpenStackClientReleaseNotes.tex', + u'OpenStackClient Release Notes Documentation', + u'OpenStackClient Developers', + 'manual', +)] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [( + 'index', + 'openstackclientreleasenotes', + u'OpenStackClient Release Notes Documentation', + [u'OpenStackClient Developers'], + 1, +)] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [( + 'index', + 'OpenStackClientReleaseNotes', + u'OpenStackclient Release Notes Documentation', + u'OpenStackclient Developers', + 'OpenStackClientReleaseNotes', + 'One line description of project.', + 'Miscellaneous', +)] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 0000000..c89ed74 --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,8 @@ +============================= +OpenStackClient Release Notes +============================= + +.. toctree:: + :maxdepth: 1 + + unreleased diff --git a/releasenotes/source/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 0000000..cb3cccf --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +===================== +Current Release Notes +===================== + +.. release-notes:: diff --git a/requirements.txt b/requirements.txt index c638ed1..fbe3b8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,16 +5,16 @@ pbr>=1.6 six>=1.9.0 Babel>=1.3 -cliff>=1.14.0 # Apache-2.0 -cliff-tablib>=1.0 +cliff>=1.15.0 # Apache-2.0 +keystoneauth1>=1.0.0 os-client-config!=1.6.2,>=1.4.0 -oslo.config>=2.3.0 # Apache-2.0 +oslo.config>=2.7.0 # Apache-2.0 oslo.i18n>=1.5.0 # Apache-2.0 -oslo.utils>=2.0.0 # Apache-2.0 +oslo.utils>=2.8.0 # Apache-2.0 python-glanceclient>=0.18.0 -python-keystoneclient>=1.6.0 -python-novaclient>=2.28.1 +python-keystoneclient!=1.8.0,>=1.6.0 +python-novaclient!=2.33.0,>=2.29.0 python-cinderclient>=1.3.1 python-neutronclient>=2.6.0 -requests>=2.5.2 +requests>=2.8.1 stevedore>=1.5.0 # Apache-2.0 @@ -5,7 +5,7 @@ description-file = README.rst author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = http://wiki.openstack.org/OpenStackClient +home-page = http://docs.openstack.org/developer/python-openstackclient classifier = Environment :: OpenStack Intended Audience :: Information Technology @@ -15,7 +15,6 @@ classifier = Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 - Programming Language :: Python :: 2.6 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 @@ -66,6 +65,7 @@ openstack.compute.v2 = aggregate_set = openstackclient.compute.v2.aggregate:SetAggregate aggregate_show = openstackclient.compute.v2.aggregate:ShowAggregate + compute_service_delete = openstackclient.compute.v2.service:DeleteService compute_service_list = openstackclient.compute.v2.service:ListService compute_service_set = openstackclient.compute.v2.service:SetService @@ -103,8 +103,6 @@ openstack.compute.v2 = keypair_list = openstackclient.compute.v2.keypair:ListKeypair keypair_show = openstackclient.compute.v2.keypair:ShowKeypair - project_usage_list = openstackclient.compute.v2.usage:ListProjectUsage - security_group_create = openstackclient.compute.v2.security_group:CreateSecurityGroup security_group_delete = openstackclient.compute.v2.security_group:DeleteSecurityGroup security_group_list = openstackclient.compute.v2.security_group:ListSecurityGroup @@ -131,13 +129,17 @@ openstack.compute.v2 = server_resize = openstackclient.compute.v2.server:ResizeServer server_resume = openstackclient.compute.v2.server:ResumeServer server_set = openstackclient.compute.v2.server:SetServer + server_shelve = openstackclient.compute.v2.server:ShelveServer server_show = openstackclient.compute.v2.server:ShowServer server_ssh = openstackclient.compute.v2.server:SshServer + server_start = openstackclient.compute.v2.server:StartServer + server_stop = openstackclient.compute.v2.server:StopServer server_suspend = openstackclient.compute.v2.server:SuspendServer server_unlock = openstackclient.compute.v2.server:UnlockServer server_unpause = openstackclient.compute.v2.server:UnpauseServer server_unrescue = openstackclient.compute.v2.server:UnrescueServer server_unset = openstackclient.compute.v2.server:UnsetServer + server_unshelve = openstackclient.compute.v2.server:UnshelveServer usage_list = openstackclient.compute.v2.usage:ListUsage usage_show = openstackclient.compute.v2.usage:ShowUsage @@ -332,16 +334,23 @@ openstack.network.v2 = network_show = openstackclient.network.v2.network:ShowNetwork openstack.object_store.v1 = + object_store_account_set = openstackclient.object.v1.account:SetAccount + object_store_account_show = openstackclient.object.v1.account:ShowAccount + object_store_account_unset = openstackclient.object.v1.account:UnsetAccount container_create = openstackclient.object.v1.container:CreateContainer container_delete = openstackclient.object.v1.container:DeleteContainer container_list = openstackclient.object.v1.container:ListContainer container_save = openstackclient.object.v1.container:SaveContainer + container_set = openstackclient.object.v1.container:SetContainer container_show = openstackclient.object.v1.container:ShowContainer + container_unset = openstackclient.object.v1.container:UnsetContainer object_create = openstackclient.object.v1.object:CreateObject object_delete = openstackclient.object.v1.object:DeleteObject object_list = openstackclient.object.v1.object:ListObject object_save = openstackclient.object.v1.object:SaveObject + object_set = openstackclient.object.v1.object:SetObject object_show = openstackclient.object.v1.object:ShowObject + object_unset = openstackclient.object.v1.object:UnsetObject openstack.volume.v1 = backup_create = openstackclient.volume.v1.backup:CreateBackup diff --git a/test-requirements.txt b/test-requirements.txt index 25d0628..7322c84 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -7,12 +7,24 @@ coverage>=3.6 discover fixtures>=1.3.1 mock>=1.2 -oslosphinx>=2.5.0 # Apache-2.0 +oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 +reno>=0.1.1 # Apache2 requests-mock>=0.6.0 # Apache-2.0 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 -os-testr>=0.1.0 +os-testr>=0.4.1 testrepository>=0.0.18 testtools>=1.4.0 WebOb>=1.2.3 -tempest-lib>=0.8.0 +tempest-lib>=0.10.0 + +# Install these to generate sphinx autodocs +python-barbicanclient>=3.3.0 +python-congressclient>=1.0.0 +python-designateclient>=1.5.0 +python-heatclient>=0.6.0 +python-ironicclient>=0.8.0 +python-mistralclient>=1.0.0 +python-saharaclient>=0.10.0 +python-tuskarclient>=0.1.17 +python-zaqarclient>=0.3.0 @@ -1,6 +1,6 @@ [tox] minversion = 1.6 -envlist = py34,py26,py27,pep8 +envlist = py34,py27,pep8 skipdist = True [testenv] @@ -33,6 +33,9 @@ downloadcache = ~/cache/pip [testenv:docs] commands = python setup.py build_sphinx +[testenv:releasenotes] +commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + [flake8] show-source = True exclude = .git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools |
