diff --git a/.coveragerc b/.coveragerc index 933b5e5ac..b474b73c7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ [run] branch = True source = novaclient -omit = novaclient/openstack/* +omit = novaclient/tests/* [report] ignore_errors = True diff --git a/.gitignore b/.gitignore index 82ede6ad7..611c10e37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .coverage .venv -.testrepository +.stestr/ subunit.log .tox *,cover diff --git a/.gitreview b/.gitreview index 033138304..9221a4f9d 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] -host=review.openstack.org +host=review.opendev.org port=29418 project=openstack/python-novaclient.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..894b69869 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: mixed-line-ending + args: ['--fix', 'lf'] + - id: check-byte-order-marker + - id: check-executables-have-shebangs + - id: check-merge-conflict + - id: debug-statements + - id: check-yaml + files: .*\.(yaml|yml)$ + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.5 + hooks: + - id: remove-tabs + exclude: '.*\.(svg)$' + - repo: https://github.com/PyCQA/bandit + rev: 1.8.5 + hooks: + - id: bandit + exclude: '^novaclient/tests/.*$' + - repo: https://opendev.org/openstack/hacking + rev: 7.0.0 + hooks: + - id: hacking + additional_dependencies: [] + exclude: '^(doc|releasenotes|tools)/.*$' diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 000000000..ac945e834 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=${OS_TEST_PATH:-./novaclient/tests/unit} +top_dir=./ diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index c8fae426b..000000000 --- a/.testr.conf +++ /dev/null @@ -1,7 +0,0 @@ -[DEFAULT] -test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ - OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ - OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-300} \ - ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./novaclient/tests/unit} $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 000000000..8abe5d285 --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,31 @@ +- job: + name: python-novaclient-functional + parent: devstack-tox-functional + timeout: 7200 + required-projects: + - openstack/nova + - openstack/python-novaclient + vars: + openrc_enable_export: true + devstack_localrc: + KEYSTONE_ADMIN_ENDPOINT: true + NEUTRON_ENFORCE_SCOPE: false + irrelevant-files: + - ^.*\.rst$ + - ^doc/.*$ + - ^releasenotes/.*$ + +- project: + templates: + - check-requirements + - lib-forward-testing-python3 + - openstack-cover-jobs + - openstack-python3-jobs + - publish-openstack-docs-pti + - release-notes-jobs-python3 + check: + jobs: + - python-novaclient-functional + gate: + jobs: + - python-novaclient-functional diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 20115405c..d4205aed3 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,16 +1,19 @@ -If you would like to contribute to the development of OpenStack, -you must follow the steps documented at: +The source repository for this project can be found at: - https://docs.openstack.org/infra/manual/developers.html#development-workflow + https://opendev.org/openstack/python-novaclient -Once those steps have been completed, changes to OpenStack -should be submitted for review via the Gerrit tool, following -the workflow documented at: +Pull requests submitted through GitHub are not monitored. - https://docs.openstack.org/infra/manual/developers.html#development-workflow +To start contributing to OpenStack, follow the steps in the contribution guide +to set up and use Gerrit: -Pull requests submitted through GitHub will be ignored. + https://docs.openstack.org/contributors/code-and-documentation/quick-start.html -Bugs should be filed on Launchpad, not GitHub: +Bugs should be filed on Launchpad: https://bugs.launchpad.net/python-novaclient + +For more specific information about contributing to this repository, see the +python-novaclient contributor guide: + + https://docs.openstack.org/python-novaclient/latest/contributor/contributing.html diff --git a/HACKING.rst b/HACKING.rst index 5a5d01d7f..b5653449b 100644 --- a/HACKING.rst +++ b/HACKING.rst @@ -5,30 +5,12 @@ Nova Client Style Commandments https://docs.openstack.org/hacking/latest - Step 2: Read on - Nova Client Specific Commandments --------------------------------- None so far Text encoding ------------- -- All text within python code should be of type 'unicode'. - - WRONG: - - >>> s = 'foo' - >>> s - 'foo' - >>> type(s) - - - RIGHT: - - >>> u = u'foo' - >>> u - u'foo' - >>> type(u) - - Transitions between internal unicode and external strings should always be immediately and explicitly encoded or decoded. @@ -36,13 +18,13 @@ Text encoding - All external text that is not explicitly encoded (database storage, commandline arguments, etc.) should be presumed to be encoded as utf-8. - WRONG: + Wrong:: mystring = infile.readline() myreturnstring = do_some_magic_with(mystring) outfile.write(myreturnstring) - RIGHT: + Right:: mystring = infile.readline() mytext = s.decode('utf-8') @@ -52,8 +34,8 @@ Text encoding Running Tests ------------- -The testing system is based on a combination of tox and testr. If you just -want to run the whole suite, run `tox` and all will be fine. However, if + +The testing system is based on a combination of tox and stestr. If you just +want to run the whole suite, run ``tox`` and all will be fine. However, if you'd like to dig in a bit more, you might want to learn some things about -testr itself. A basic walkthrough for OpenStack can be found at -http://wiki.openstack.org/testr +stestr itself. diff --git a/README.rst b/README.rst index acf6493af..cdaf86536 100644 --- a/README.rst +++ b/README.rst @@ -12,16 +12,12 @@ Python bindings to the OpenStack Compute API ============================================ .. image:: https://img.shields.io/pypi/v/python-novaclient.svg - :target: https://pypi.python.org/pypi/python-novaclient/ + :target: https://pypi.org/project/python-novaclient/ :alt: Latest Version -.. image:: https://img.shields.io/pypi/dm/python-novaclient.svg - :target: https://pypi.python.org/pypi/python-novaclient/ - :alt: Downloads - This is a client for the OpenStack Compute API. It provides a Python API (the -``novaclient`` module) and a command-line script (``nova``). Each implements -100% of the OpenStack Compute API. +``novaclient`` module) and a deprecated command-line script (``nova``). The +Python API implements 100% of the OpenStack Compute API. * License: Apache License, Version 2.0 * `PyPi`_ - package installation @@ -32,12 +28,14 @@ This is a client for the OpenStack Compute API. It provides a Python API (the * `Source`_ * `Specs`_ * `How to Contribute`_ +* `Release Notes`_ -.. _PyPi: https://pypi.python.org/pypi/python-novaclient +.. _PyPi: https://pypi.org/project/python-novaclient .. _Online Documentation: https://docs.openstack.org/python-novaclient/latest .. _Launchpad project: https://launchpad.net/python-novaclient .. _Blueprints: https://blueprints.launchpad.net/python-novaclient .. _Bugs: https://bugs.launchpad.net/python-novaclient -.. _Source: https://git.openstack.org/cgit/openstack/python-novaclient -.. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html -.. _Specs: http://specs.openstack.org/openstack/nova-specs/ +.. _Source: https://opendev.org/openstack/python-novaclient +.. _How to Contribute: https://docs.opendev.org/opendev/infra-manual/latest/developers.html +.. _Specs: https://specs.openstack.org/openstack/nova-specs/ +.. _Release Notes: https://docs.openstack.org/releasenotes/python-novaclient diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index efceab818..000000000 --- a/babel.cfg +++ /dev/null @@ -1 +0,0 @@ -[python: **.py] diff --git a/bindep.txt b/bindep.txt index 7aea44026..bf36b2d84 100644 --- a/bindep.txt +++ b/bindep.txt @@ -1,5 +1,5 @@ # This is a cross-platform list tracking distribution packages needed by tests; -# see https://docs.openstack.org/infra/bindep/ for additional information. +# see https://docs.opendev.org/opendev/bindep/latest/ for additional information. build-essential [platform:dpkg] dbus-devel [platform:rpm] @@ -10,15 +10,13 @@ libdbus-1-dev [platform:dpkg] libdbus-glib-1-dev [platform:dpkg] libffi-dev [platform:dpkg] libffi-devel [platform:rpm] -libssl-dev [platform:ubuntu-xenial] +libssl-dev [platform:ubuntu] libuuid-devel [platform:rpm] locales [platform:debian] -python-dev [platform:dpkg] -python-devel [platform:rpm] +openssl python3-all-dev [platform:ubuntu !platform:ubuntu-precise] python3-dev [platform:dpkg] python3-devel [platform:fedora] -python3.4 [platform:ubuntu-trusty] -python3.5 [platform:ubuntu-xenial] -python34-devel [platform:centos] uuid-dev [platform:dpkg] +libpcre2-dev [platform:dpkg doc] +pcre2-devel [platform:rpm doc] diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 000000000..e7a8ac75b --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,7 @@ +sphinx>=2.1.1 # BSD +openstackdocstheme>=2.2.0 # Apache-2.0 +reno>=3.1.0 # Apache-2.0 +sphinxcontrib-apidoc>=0.2.0 # BSD + +# redirect tests in docs +whereto>=0.5.0 # Apache-2.0 diff --git a/doc/source/_extra/.htaccess b/doc/source/_extra/.htaccess new file mode 100644 index 000000000..bdff533e2 --- /dev/null +++ b/doc/source/_extra/.htaccess @@ -0,0 +1,10 @@ +# The following is generated with: +# +# git log --follow --name-status --format='%H' ac25ae6fee.. -- doc/source/ | \ +# grep ^R | grep .rst | cut -f2- | \ +# sed -e 's|doc/source/|redirectmatch 301 ^/python-novaclient/([^/]+)/|' -e 's|doc/source/|/python-novaclient/$1/|' -e 's/.rst/.html$/' -e 's/.rst/.html/' | \ +# sort + +redirectmatch 301 ^/python-novaclient/([^/]+)/api.html$ /python-novaclient/$1/reference/api/index.html +redirectmatch 301 ^/python-novaclient/([^/]+)/man/nova.html$ /python-novaclient/$1/cli/nova.html +redirectmatch 301 ^/python-novaclient/([^/]+)/shell.html$ /python-novaclient/$1/user/shell.html diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index c3c6caf42..03145381a 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -2,84 +2,4009 @@ nova ====== +The nova client is the command-line interface (CLI) for +the Compute service (nova) API and its extensions. -SYNOPSIS -======== +For help on a specific :command:`nova` command, enter: - `nova` [options] [command-options] +.. code-block:: console - `nova help` + $ nova help COMMAND - `nova help` +.. deprecated:: 17.8.0 + The ``nova`` CLI has been deprecated in favour of the unified + ``openstack`` CLI. For information on using the ``openstack`` CLI, see + :python-openstackclient-doc:`OpenStackClient <>`. -DESCRIPTION -=========== +.. _nova_command_usage: -`nova` is a command line client for controlling OpenStack Nova, the cloud -computing fabric controller. It implements 100% of the Nova API, allowing -management of instances, images, quotas and much more. +nova usage +~~~~~~~~~~ -Before you can issue commands with `nova`, you must ensure that your -environment contains the necessary variables so that you can prove to the CLI -who you are and what credentials you have to issue the commands. See -`Getting Credentials for a CLI` section of `OpenStack CLI Guide` for more -info. +.. code-block:: console -See `OpenStack Nova CLI Guide` for a full-fledged guide. + usage: nova [--version] [--debug] [--os-cache] [--timings] + [--os-region-name ] [--service-type ] + [--service-name ] + [--os-endpoint-type ] + [--os-compute-api-version ] + [--os-endpoint-override ] [--profile HMAC_KEY] + [--insecure] [--os-cacert ] + [--os-cert ] [--os-key ] [--timeout ] + [--collect-timing] [--os-auth-type ] + [--os-auth-url OS_AUTH_URL] [--os-system-scope OS_SYSTEM_SCOPE] + [--os-domain-id OS_DOMAIN_ID] [--os-domain-name OS_DOMAIN_NAME] + [--os-project-id OS_PROJECT_ID] + [--os-project-name OS_PROJECT_NAME] + [--os-project-domain-id OS_PROJECT_DOMAIN_ID] + [--os-project-domain-name OS_PROJECT_DOMAIN_NAME] + [--os-trust-id OS_TRUST_ID] + [--os-default-domain-id OS_DEFAULT_DOMAIN_ID] + [--os-default-domain-name OS_DEFAULT_DOMAIN_NAME] + [--os-user-id OS_USER_ID] [--os-username OS_USERNAME] + [--os-user-domain-id OS_USER_DOMAIN_ID] + [--os-user-domain-name OS_USER_DOMAIN_NAME] + [--os-password OS_PASSWORD] + ... +**Subcommands:** -OPTIONS -======= +``add-fixed-ip`` + **DEPRECATED** Add new IP address on a network to + server. -To get a list of available commands and options run:: +``add-secgroup`` + Add a Security Group to a server. - nova help +``agent-create`` + Create new agent build. -To get usage and options of a command run:: +``agent-delete`` + Delete existing agent build. - nova help +``agent-list`` + List all builds. +``agent-modify`` + Modify existing agent build. -EXAMPLES -======== +``aggregate-add-host`` + Add the host to the specified aggregate. -Get information about boot command:: +``aggregate-cache-images`` + Request images be pre-cached on hosts within an aggregate. + (Supported by API versions '2.81' - '2.latest') - nova help boot +``aggregate-create`` + Create a new aggregate with the specified + details. -List available images:: +``aggregate-delete`` + Delete the aggregate. - nova image-list +``aggregate-list`` + Print a list of all aggregates. -List available flavors:: +``aggregate-remove-host`` + Remove the specified host from the specified + aggregate. - nova flavor-list +``aggregate-set-metadata`` + Update the metadata associated with the + aggregate. -Launch an instance:: +``aggregate-show`` + Show details of the specified aggregate. - nova boot myserver --image some-image --flavor 2 +``aggregate-update`` + Update the aggregate's name and optionally + availability zone. -View instance information:: +``availability-zone-list`` + List all the availability zones. - nova show myserver +``backup`` + Backup a server by creating a 'backup' type + snapshot. -List instances:: +``boot`` + Boot a new server. - nova list +``clear-password`` + Clear the admin password for a server from the + metadata server. This action does not actually + change the instance server password. -Terminate an instance:: +``cloudpipe-configure`` + **DEPRECATED** Update the VPN IP/port of a + cloudpipe instance. - nova delete myserver +``cloudpipe-create`` + **DEPRECATED** Create a cloudpipe instance for the + given project. +``cloudpipe-list`` + **DEPRECATED** Print a list of all cloudpipe + instances. -SEE ALSO -======== +``console-log`` + Get console log output of a server. -OpenStack Nova CLI Guide: http://docs.openstack.org/cli-reference/nova.html +``delete`` + Immediately shut down and delete specified + server(s). +``diagnostics`` + Retrieve server diagnostics. -BUGS -==== +``evacuate`` + Evacuate server from failed host. -Nova client is hosted in Launchpad so you can view current bugs at https://bugs.launchpad.net/python-novaclient/. +``flavor-access-add`` + Add flavor access for the given tenant. + +``flavor-access-list`` + Print access information about the given + flavor. + +``flavor-access-remove`` + Remove flavor access for the given tenant. + +``flavor-create`` + Create a new flavor. + +``flavor-delete`` + Delete a specific flavor + +``flavor-key`` + Set or unset extra_spec for a flavor. + +``flavor-list`` + Print a list of available 'flavors' (sizes of + servers). + +``flavor-show`` + Show details about the given flavor. + +``flavor-update`` + Update the description of an existing flavor. + (Supported by API versions '2.55' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] + +``floating-ip-associate`` + **DEPRECATED** Associate a floating IP address to + a server. + +``floating-ip-disassociate`` + **DEPRECATED** Disassociate a floating IP address + from a server. + +``force-delete`` + Force delete a server. + +``get-mks-console`` + Get an MKS console to a server. (Supported by + API versions '2.8' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``get-password`` + Get the admin password for a server. This + operation calls the metadata service to query + metadata information and does not read + password information from the server itself. + +``get-rdp-console`` + Get a rdp console to a server. + +``get-serial-console`` + Get a serial console to a server. + +``get-spice-console`` + Get a spice console to a server. + +``get-vnc-console`` + Get a vnc console to a server. + +``host-action`` + **DEPRECATED** Perform a power action on a host. + +``host-describe`` + **DEPRECATED** Describe a specific host. + +``host-evacuate`` + Evacuate all instances from failed host. + +``host-evacuate-live`` + Live migrate all instances off the specified + host to other available hosts. + +``host-list`` + **DEPRECATED** List all hosts by service. + +``host-meta`` + Set or Delete metadata on all instances of a + host. + +``host-servers-migrate`` + Cold migrate all instances off the specified + host to other available hosts. + +``host-update`` + **DEPRECATED** Update host settings. + +``hypervisor-list`` + List hypervisors. (Supported by API versions '2.0' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] + +``hypervisor-servers`` + List servers belonging to specific + hypervisors. + +``hypervisor-show`` + Display the details of the specified + hypervisor. + +``hypervisor-stats`` + Get hypervisor statistics over all compute + nodes. + +``hypervisor-uptime`` + Display the uptime of the specified + hypervisor. + +``image-create`` + Create a new image by taking a snapshot of a + running server. + +``instance-action`` + Show an action. + +``instance-action-list`` + List actions on a server. + +``instance-usage-audit-log`` + List/Get server usage audits. + +``interface-attach`` + Attach a network interface to a server. + +``interface-detach`` + Detach a network interface from a server. + +``interface-list`` + List interfaces attached to a server. + +``keypair-add`` + Create a new key pair for use with servers. + +``keypair-delete`` + Delete keypair given by its name. (Supported + by API versions '2.0' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``keypair-list`` + Print a list of keypairs for a user (Supported + by API versions '2.0' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``keypair-show`` + Show details about the given keypair. + (Supported by API versions '2.0' - '2.latest') + [hint: use '--os-compute-api-version' flag to + show help message for proper version] + +``limits`` + Print rate and absolute limits. + +``list`` + List servers. + +``list-secgroup`` + List Security Group(s) of a server. + +``live-migration`` + Migrate running server to a new machine. + +``live-migration-abort`` + Abort an on-going live migration. (Supported + by API versions '2.24' - '2.latest') [hint: + use '--os-compute-api-version' flag to show + help message for proper version] + +``live-migration-force-complete`` + Force on-going live migration to complete. + (Supported by API versions '2.22' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] + +``lock`` + Lock a server. A normal (non-admin) user will + not be able to execute actions on a locked + server. + +``meta`` + Set or delete metadata on a server. + +``migrate`` + Migrate a server. The new host will be + selected by the scheduler. + +``migration-list`` + Print a list of migrations. + +``pause`` + Pause a server. + +``quota-class-show`` + List the quotas for a quota class. + +``quota-class-update`` + Update the quotas for a quota class. + (Supported by API versions '2.0' - '2.latest') + [hint: use '--os-compute-api-version' flag to + show help message for proper version] + +``quota-defaults`` + List the default quotas for a tenant. + +``quota-delete`` + Delete quota for a tenant/user so their quota + will Revert back to default. + +``quota-show`` + List the quotas for a tenant/user. + +``quota-update`` + Update the quotas for a tenant/user. + (Supported by API versions '2.0' - '2.latest') + [hint: use '--os-compute-api-version' flag to + show help message for proper version] + +``reboot`` + Reboot a server. + +``rebuild`` + Shutdown, re-image, and re-boot a server. + +``refresh-network`` + Refresh server network information. + +``remove-fixed-ip`` + **DEPRECATED** Remove an IP address from a server. + +``remove-secgroup`` + Remove a Security Group from a server. + +``rescue`` + Reboots a server into rescue mode, which + starts the machine from either the initial + image or a specified image, attaching the + current boot disk as secondary. + +``reset-network`` + Reset network of a server. + +``reset-state`` + Reset the state of a server. + +``resize`` + Resize a server. + +``resize-confirm`` + Confirm a previous resize. + +``resize-revert`` + Revert a previous resize (and return to the + previous VM). + +``restore`` + Restore a soft-deleted server. + +``resume`` + Resume a server. + +``server-group-create`` + Create a new server group with the specified + details. + +``server-group-delete`` + Delete specific server group(s). + +``server-group-get`` + Get a specific server group. + +``server-group-list`` + Print a list of all server groups. + +``server-migration-list`` + Get the migrations list of specified server. + (Supported by API versions '2.23' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] + +``server-migration-show`` + Get the migration of specified server. + (Supported by API versions '2.23' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] + +``server-tag-add`` + Add one or more tags to a server. (Supported + by API versions '2.26' - '2.latest') [hint: + use '--os-compute-api-version' flag to show + help message for proper version] + +``server-tag-delete`` + Delete one or more tags from a server. + (Supported by API versions '2.26' - '2.latest') + [hint: use '--os-compute-api-version' flag to show help message + for proper version] + +``server-tag-delete-all`` + Delete all tags from a server. (Supported by + API versions '2.26' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``server-tag-list`` + Get list of tags from a server. (Supported by + API versions '2.26' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``server-tag-set`` + Set list of tags to a server. (Supported by + API versions '2.26' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``server-topology`` + Retrieve NUMA topology of the given server. + (Supported by API versions '2.78' - '2.latest') + +``service-delete`` + Delete the service. + +``service-disable`` + Disable the service. + +``service-enable`` + Enable the service. + +``service-force-down`` + Force service to down. (Supported by API + versions '2.11' - '2.latest') [hint: use + '--os-compute-api-version' flag to show help + message for proper version] + +``service-list`` + Show a list of all running services. Filter by + host & binary. + +``set-password`` + Change the admin password for a server. + +``shelve`` + Shelve a server. + +``shelve-offload`` + Remove a shelved server from the compute node. + +``show`` + Show details about the given server. + +``ssh`` + SSH into a server. + +``start`` + Start the server(s). + +``stop`` + Stop the server(s). + +``suspend`` + Suspend a server. + +``trigger-crash-dump`` + Trigger crash dump in an instance. (Supported + by API versions '2.17' - '2.latest') [hint: + use '--os-compute-api-version' flag to show + help message for proper version] + +``unlock`` + Unlock a server. + +``unpause`` + Unpause a server. + +``unrescue`` + Restart the server from normal boot disk + again. + +``unshelve`` + Unshelve a server. + +``update`` + Update the name or the description for a + server. + +``usage`` + Show usage data for a single tenant. + +``usage-list`` + List usage data for all tenants. + +``version-list`` + List all API versions. + +``virtual-interface-list`` + **DEPRECATED** Show virtual interface info about + the given server. + +``volume-attach`` + Attach a volume to a server. + +``volume-attachments`` + List all the volumes attached to a server. + +``volume-detach`` + Detach a volume from a server. + +``volume-update`` + Update the attachment on the server. Migrates the data from an + attached volume to the specified available volume and swaps out + the active attachment to the new volume. + Since microversion 2.85, support for updating the + ``delete_on_termination`` delete flag, which allows changing the + behavior of volume deletion on instance deletion. + +``x509-create-cert`` + **DEPRECATED** Create x509 cert for a user in + tenant. + +``x509-get-root-cert`` + **DEPRECATED** Fetch the x509 root cert. + +``bash-completion`` + Prints all of the commands and options to + stdout so that the nova.bash_completion script + doesn't have to hard code them. + +``help`` + Display help about this program or one of its + subcommands. + +.. _nova_command_options: + +nova optional arguments +~~~~~~~~~~~~~~~~~~~~~~~ + +``--version`` + show program's version number and exit + +``--debug`` + Print debugging output. + +``--os-cache`` + Use the auth token cache. Defaults to False if + ``env[OS_CACHE]`` is not set. + +``--timings`` + Print call timing info. + +``--os-region-name `` + Defaults to ``env[OS_REGION_NAME]``. + +``--service-type `` + Defaults to compute for most actions. + +``--service-name `` + Defaults to ``env[NOVA_SERVICE_NAME]``. + +``--os-endpoint-type `` + Defaults to ``env[NOVA_ENDPOINT_TYPE]``, + ``env[OS_ENDPOINT_TYPE]`` or publicURL. + +``--os-compute-api-version `` + Accepts X, X.Y (where X is major and Y is + minor part) or "X.latest", defaults to + ``env[OS_COMPUTE_API_VERSION]``. + +``--os-endpoint-override `` + Use this API endpoint instead of the Service + Catalog. Defaults to + ``env[OS_ENDPOINT_OVERRIDE]``. + +``--profile HMAC_KEY`` + HMAC key to use for encrypting context data + for performance profiling of operation. This + key should be the value of the HMAC key + configured for the OSprofiler middleware in + nova; it is specified in the Nova + configuration file at "/etc/nova/nova.conf". + Without the key, profiling will not be + triggered even if OSprofiler is enabled on the + server side. + +``--os-auth-type , --os-auth-plugin `` + Authentication type to use + +.. _nova_add-secgroup: + +nova add-secgroup +----------------- + +.. code-block:: console + + usage: nova add-secgroup + +Add a Security Group to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name or ID of Security Group. + +.. _nova_agent-create: + +nova agent-create +----------------- + +.. code-block:: console + + usage: nova agent-create + + +Create new agent build. + +**Positional arguments:** + +```` + Type of OS. + +```` + Type of architecture. + +```` + Version. + +```` + URL. + +```` + MD5 hash. + +```` + Type of hypervisor. + +.. _nova_agent-delete: + +nova agent-delete +----------------- + +.. code-block:: console + + usage: nova agent-delete + +Delete existing agent build. + +**Positional arguments:** + +```` + ID of the agent-build. + +.. _nova_agent-list: + +nova agent-list +--------------- + +.. code-block:: console + + usage: nova agent-list [--hypervisor ] + +List all builds. + +**Optional arguments:** + +``--hypervisor `` + Type of hypervisor. + +.. _nova_agent-modify: + +nova agent-modify +----------------- + +.. code-block:: console + + usage: nova agent-modify + +Modify existing agent build. + +**Positional arguments:** + +```` + ID of the agent-build. + +```` + Version. + +```` + URL + +```` + MD5 hash. + +.. _nova_aggregate-add-host: + +nova aggregate-add-host +----------------------- + +.. code-block:: console + + usage: nova aggregate-add-host + +Add the host to the specified aggregate. + +**Positional arguments:** + +```` + Name or ID of aggregate. + +```` + The host to add to the aggregate. + +.. _nova_aggregate-cache-images: + +nova aggregate-cache-images +--------------------------- + +.. code-block:: console + + usage: nova aggregate-cache-images [ ..] + +Request image(s) be pre-cached on hosts within the aggregate. +(Supported by API versions '2.81' - '2.latest') + +.. versionadded:: 16.0.0 + +**Positional arguments:** + +```` + Name or ID of aggregate. + +```` + Name or ID of image(s) to cache. + +.. _nova_aggregate-create: + +nova aggregate-create +--------------------- + +.. code-block:: console + + usage: nova aggregate-create [] + +Create a new aggregate with the specified details. + +**Positional arguments:** + +```` + Name of aggregate. + +```` + The availability zone of the aggregate (optional). + +.. _nova_aggregate-delete: + +nova aggregate-delete +--------------------- + +.. code-block:: console + + usage: nova aggregate-delete + +Delete the aggregate. + +**Positional arguments:** + +```` + Name or ID of aggregate to delete. + +.. _nova_aggregate-list: + +nova aggregate-list +------------------- + +.. code-block:: console + + usage: nova aggregate-list + +Print a list of all aggregates. + +.. _nova_aggregate-remove-host: + +nova aggregate-remove-host +-------------------------- + +.. code-block:: console + + usage: nova aggregate-remove-host + +Remove the specified host from the specified aggregate. + +**Positional arguments:** + +```` + Name or ID of aggregate. + +```` + The host to remove from the aggregate. + +.. _nova_aggregate-set-metadata: + +nova aggregate-set-metadata +--------------------------- + +.. code-block:: console + + usage: nova aggregate-set-metadata [ ...] + +Update the metadata associated with the aggregate. + +**Positional arguments:** + +```` + Name or ID of aggregate to update. + +```` + Metadata to add/update to aggregate. Specify only the key to + delete a metadata item. + +.. _nova_aggregate-show: + +nova aggregate-show +------------------- + +.. code-block:: console + + usage: nova aggregate-show + +Show details of the specified aggregate. + +**Positional arguments:** + +```` + Name or ID of aggregate. + +.. _nova_aggregate-update: + +nova aggregate-update +--------------------- + +.. code-block:: console + + usage: nova aggregate-update [--name NAME] + [--availability-zone ] + + +Update the aggregate's name and optionally availability zone. + +**Positional arguments:** + +```` + Name or ID of aggregate to update. + +**Optional arguments:** + +``--name NAME`` + New name for aggregate. + +``--availability-zone `` + New availability zone for aggregate. + +.. _nova_availability-zone-list: + +nova availability-zone-list +--------------------------- + +.. code-block:: console + + usage: nova availability-zone-list + +List all the availability zones. + +.. _nova_backup: + +nova backup +----------- + +.. code-block:: console + + usage: nova backup + +Backup a server by creating a 'backup' type snapshot. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name of the backup image. + +```` + The backup type, like "daily" or "weekly". + +```` + Int parameter representing how many backups to keep around. + +.. _nova_boot: + +nova boot +--------- + +.. code-block:: console + + usage: nova boot [--flavor ] [--image ] + [--image-with ] [--boot-volume ] + [--snapshot ] [--min-count ] + [--max-count ] [--meta ] + [--key-name ] [--user-data ] + [--availability-zone ] + [--security-groups ] + [--block-device-mapping ] + [--block-device key1=value1[,key2=value2...]] + [--swap ] + [--ephemeral size=[,format=]] + [--hint ] + [--nic ] + [--config-drive ] [--poll] [--admin-pass ] + [--access-ip-v4 ] [--access-ip-v6 ] + [--description ] [--tags ] + [--return-reservation-id] + [--trusted-image-certificate-id ] + [--host ] + [--hypervisor-hostname ] + [--hostname ] + + +Boot a new server. + +In order to create a server with pre-existing ports that contain a +``resource_request`` value, such as for guaranteed minimum bandwidth +quality of service support, microversion ``2.72`` is required. + +**Positional arguments:** + +```` + Name for the new server. + +**Optional arguments:** + +``--flavor `` + Name or ID of flavor (see 'nova flavor-list'). + +``--image `` + Name or ID of image (see 'glance image-list'). + +``--image-with `` + Image metadata property (see 'glance image-show'). + +``--boot-volume `` + Volume ID to boot from. + +``--snapshot `` + Snapshot ID to boot from (will create a + volume). + +``--min-count `` + Boot at least servers (limited by + quota). + +``--max-count `` + Boot up to servers (limited by + quota). + +``--meta `` + Record arbitrary key/value metadata to + /meta_data.json on the metadata server. Can be + specified multiple times. + +``--key-name `` + Key name of keypair that should be created + earlier with the command keypair-add. + +``--user-data `` + user data file to pass to be exposed by the + metadata server. + +``--availability-zone `` + The availability zone for server placement. + +``--security-groups `` + Comma separated list of security group names. + +``--block-device-mapping `` + Block device mapping in the format + =:::. + +``--block-device`` + key1=value1[,key2=value2...] + Block device mapping with the keys: id=UUID + (image_id, snapshot_id or volume_id only if + using source image, snapshot or volume) + source=source type (image, snapshot, volume or + blank), dest=destination type of the block + device (volume or local), bus=device's bus + (e.g. uml, lxc, virtio, ...; if omitted, + hypervisor driver chooses a suitable default, + honoured only if device type is supplied) + type=device type (e.g. disk, cdrom, ...; + defaults to 'disk') device=name of the device + (e.g. vda, xda, ...; if omitted, hypervisor + driver chooses suitable device depending on + selected bus; note the libvirt driver always + uses default device names), size=size of the + block device in MiB(for swap) and in GiB(for + other formats) (if omitted, hypervisor driver + calculates size), format=device will be + formatted (e.g. swap, ntfs, ...; optional), + bootindex=integer used for ordering the boot + disks (for image backed instances it is equal + to 0, for others need to be specified), + shutdown=shutdown behaviour (either preserve + or remove, for local destination set to + remove), tag=device metadata tag + (optional; supported by API versions '2.42' + - '2.latest'), and volume_type=type of volume + to create (either ID or name) when source is + `blank`, `image` or `snapshot` and dest is `volume` + (optional; supported by API versions '2.67' + - '2.latest'). + +``--swap `` + Create and attach a local swap block device of + MiB. + +``--ephemeral`` + size=[,format=] + Create and attach a local ephemeral block + device of GiB and format it to . + +``--hint `` + Send arbitrary key/value pairs to the + scheduler for custom use. + +``--nic `` + Create a NIC on the server. Specify option + multiple times to create multiple nics unless + using the special 'auto' or 'none' values. + auto: automatically allocate network resources + if none are available. This cannot be + specified with any other nic value and cannot + be specified multiple times. none: do not + attach a NIC at all. This cannot be specified + with any other nic value and cannot be + specified multiple times. net-id: attach NIC + to network with a specific UUID. net-name: + attach NIC to network with this name (either + port-id or net-id or net-name must be + provided), v4-fixed-ip: IPv4 fixed address for + NIC (optional), v6-fixed-ip: IPv6 fixed + address for NIC (optional), port-id: attach + NIC to port with this UUID tag: interface + metadata tag (optional) (either port-id or + net-id must be provided). (Supported by API + versions '2.42' - '2.latest') + +``--config-drive `` + Enable config drive. The value must be a + boolean value. + +``--poll`` + Report the new server boot progress until it + completes. + +``--admin-pass `` + Admin password for the instance. + +``--access-ip-v4 `` + Alternative access IPv4 of the instance. + +``--access-ip-v6 `` + Alternative access IPv6 of the instance. + +``--description `` + Description for the server. (Supported by API + versions '2.19' - '2.latest') + +``--tags `` + Tags for the server.Tags must be separated by commas: --tags + (Supported by API versions '2.52' - '2.latest') + +``--return-reservation-id`` + Return a reservation id bound to created servers. + +``--trusted-image-certificate-id `` + Trusted image certificate IDs used to validate certificates + during the image signature verification process. + Defaults to env[OS_TRUSTED_IMAGE_CERTIFICATE_IDS]. + May be specified multiple times to pass multiple trusted image + certificate IDs. (Supported by API versions '2.63' - '2.latest') + +``--host `` + Requested host to create servers. Admin only by default. + (Supported by API versions '2.74' - '2.latest') + +``--hypervisor-hostname `` + Requested hypervisor hostname to create servers. Admin only by default. + (Supported by API versions '2.74' - '2.latest') + +``--hostname `` + Hostname for the instance. This sets the hostname stored in the + metadata server: a utility such as cloud-init running on the guest + is required to propagate these changes to the guest. + (Supported by API versions '2.90' - '2.latest') + +.. _nova_clear-password: + +nova clear-password +------------------- + +.. code-block:: console + + usage: nova clear-password + +Clear the admin password for a server from the metadata server. This action +does not actually change the instance server password. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_console-log: + +nova console-log +---------------- + +.. code-block:: console + + usage: nova console-log [--length ] + +Get console log output of a server. + +**Locale encoding issues** + +If you encounter an error such as: + +.. code-block:: console + + UnicodeEncodeError: 'ascii' codec can't encode characters in position + +The solution to these problems is different depending on which locale your +computer is running in. + +For instance, if you have a German Linux machine, you can fix the problem by +exporting the locale to de_DE.utf-8: + +.. code-block:: console + + export LC_ALL=de_DE.utf-8 + export LANG=de_DE.utf-8 + +If you are on a US machine, en_US.utf-8 is the encoding of choice. On some +newer Linux systems, you could also try C.UTF-8 as the locale: + +.. code-block:: console + + export LC_ALL=C.UTF-8 + export LANG=C.UTF-8 + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--length `` + Length in lines to tail. + +.. _nova_delete: + +nova delete +----------- + +.. code-block:: console + + usage: nova delete [--all-tenants] [ ...] + +Immediately shut down and delete specified server(s). + +**Positional arguments:** + +```` + Name or ID of server(s). + +**Optional arguments:** + +``--all-tenants`` + Delete server(s) in another tenant by name (Admin only). + +.. _nova_diagnostics: + +nova diagnostics +---------------- + +.. code-block:: console + + usage: nova diagnostics + +Retrieve server diagnostics. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_evacuate: + +nova evacuate +------------- + +.. code-block:: console + + usage: nova evacuate [--password ] [--on-shared-storage] [--force] [] + +Evacuate server from failed host. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name or ID of the target host. If no host is + specified, the scheduler will choose one. + +**Optional arguments:** + +``--password `` + Set the provided admin password on the evacuated + server. Not applicable if the server is on shared + storage. + +``--on-shared-storage`` + Specifies whether server files are located on shared + storage. (Supported by API versions '2.0' - '2.13') + +``--force`` + Force an evacuation by not verifying the provided destination host by the + scheduler. (Supported by API versions '2.29' - '2.67') + + .. warning:: This could result in failures to actually evacuate the + server to the specified host. It is recommended to either not specify + a host so that the scheduler will pick one, or specify a host without + ``--force``. + +.. _nova_flavor-access-add: + +nova flavor-access-add +---------------------- + +.. code-block:: console + + usage: nova flavor-access-add + +Add flavor access for the given tenant. + +**Positional arguments:** + +```` + Flavor name or ID to add access for the given tenant. + +```` + Tenant ID to add flavor access for. + +.. _nova_flavor-access-list: + +nova flavor-access-list +----------------------- + +.. code-block:: console + + usage: nova flavor-access-list [--flavor ] + +Print access information about the given flavor. + +**Optional arguments:** + +``--flavor `` + Filter results by flavor name or ID. + +.. _nova_flavor-access-remove: + +nova flavor-access-remove +------------------------- + +.. code-block:: console + + usage: nova flavor-access-remove + +Remove flavor access for the given tenant. + +**Positional arguments:** + +```` + Flavor name or ID to remove access for the given tenant. + +```` + Tenant ID to remove flavor access for. + +.. _nova_flavor-create: + +nova flavor-create +------------------ + +.. code-block:: console + + usage: nova flavor-create [--ephemeral ] [--swap ] + [--rxtx-factor ] [--is-public ] + [--description ] + + +Create a new flavor. + +**Positional arguments:** + +```` + Unique name of the new flavor. + +```` + Unique ID of the new flavor. Specifying 'auto' will + generated a UUID for the ID. + +```` + Memory size in MiB. + +```` + Disk size in GiB. + +```` + Number of vcpus + +**Optional arguments:** + +``--ephemeral `` + Ephemeral space size in GiB (default 0). + +``--swap `` + Swap space size in MiB (default 0). + +``--rxtx-factor `` + RX/TX factor (default 1). + +``--is-public `` + Make flavor accessible to the public (default + true). + +``--description `` + A free form description of the flavor. Limited to 65535 characters + in length. Only printable characters are allowed. + (Supported by API versions '2.55' - '2.latest') + +.. _nova_flavor-delete: + +nova flavor-delete +------------------ + +.. code-block:: console + + usage: nova flavor-delete + +Delete a specific flavor + +**Positional arguments:** + +```` + Name or ID of the flavor to delete. + +.. _nova_flavor-key: + +nova flavor-key +--------------- + +.. code-block:: console + + usage: nova flavor-key [ ...] + +Set or unset extra_spec for a flavor. + +**Positional arguments:** + +```` + Name or ID of flavor. + +```` + Actions: 'set' or 'unset'. + +```` + Extra_specs to set/unset (only key is necessary on unset). + +.. _nova_flavor-list: + +nova flavor-list +---------------- + +.. code-block:: console + + usage: nova flavor-list [--extra-specs] [--all] [--marker ] + [--min-disk ] [--min-ram ] + [--limit ] [--sort-key ] + [--sort-dir ] + +Print a list of available 'flavors' (sizes of servers). + +**Optional arguments:** + +``--extra-specs`` + Get extra-specs of each flavor. + +``--all`` + Display all flavors (Admin only). + +``--marker `` + The last flavor ID of the previous page; displays + list of flavors after "marker". + +``--min-disk `` + Filters the flavors by a minimum disk space, in GiB. + +``--min-ram `` + Filters the flavors by a minimum RAM, in MiB. + +``--limit `` + Maximum number of flavors to display. If limit is + bigger than 'CONF.api.max_limit' option of Nova API, + limit 'CONF.api.max_limit' will be used instead. + +``--sort-key `` + Flavors list sort key. + +``--sort-dir `` + Flavors list sort direction. + +.. _nova_flavor-show: + +nova flavor-show +---------------- + +.. code-block:: console + + usage: nova flavor-show + +Show details about the given flavor. + +**Positional arguments:** + +```` + Name or ID of flavor. + +nova flavor-update +------------------ + +.. code-block:: console + + usage: nova flavor-update + +Update the description of an existing flavor. +(Supported by API versions '2.55' - '2.latest') +[hint: use '--os-compute-api-version' flag to show help message for proper +version] + +.. versionadded:: 10.0.0 + +**Positional arguments** + +```` + Name or ID of the flavor to update. + +```` + A free form description of the flavor. Limited to 65535 + characters in length. Only printable characters are allowed. + +.. _nova_force-delete: + +nova force-delete +----------------- + +.. code-block:: console + + usage: nova force-delete + +Force delete a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_get-mks-console: + +nova get-mks-console +-------------------- + +.. code-block:: console + + usage: nova get-mks-console + +Get an MKS console to a server. (Supported by API versions '2.8' - '2.latest') +[hint: use '--os-compute-api-version' flag to show help message for proper +version] + +.. versionadded:: 3.0.0 + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_get-password: + +nova get-password +----------------- + +.. code-block:: console + + usage: nova get-password [] + +Get the admin password for a server. This operation calls the metadata service +to query metadata information and does not read password information from the +server itself. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Private key (used locally to decrypt password) (Optional). + When specified, the command displays the clear (decrypted) VM + password. When not specified, the ciphered VM password is + displayed. + +.. _nova_get-rdp-console: + +nova get-rdp-console +-------------------- + +.. code-block:: console + + usage: nova get-rdp-console + +Get a rdp console to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Type of rdp console ("rdp-html5"). + +.. _nova_get-serial-console: + +nova get-serial-console +----------------------- + +.. code-block:: console + + usage: nova get-serial-console [--console-type CONSOLE_TYPE] + +Get a serial console to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--console-type CONSOLE_TYPE`` + Type of serial console, default="serial". + +.. _nova_get-spice-console: + +nova get-spice-console +---------------------- + +.. code-block:: console + + usage: nova get-spice-console + +Get a spice console to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Type of spice console ("spice-html5"). + +.. _nova_get-vnc-console: + +nova get-vnc-console +-------------------- + +.. code-block:: console + + usage: nova get-vnc-console + +Get a vnc console to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Type of vnc console ("novnc" or "xvpvnc"). + +.. _nova_host-evacuate: + +nova host-evacuate +------------------ + +.. code-block:: console + + usage: nova host-evacuate [--target_host ] [--force] [--strict] + + +Evacuate all instances from failed host. + +**Positional arguments:** + +```` + The hypervisor hostname (or pattern) to search for. + + .. warning:: + + Use a fully qualified domain name if you only want to evacuate from + a specific host. + +**Optional arguments:** + +``--target_host `` + Name of target host. If no host is specified + the scheduler will select a target. + +``--force`` + Force an evacuation by not verifying the provided destination host by the + scheduler. (Supported by API versions '2.29' - '2.67') + + .. warning:: This could result in failures to actually evacuate the + server to the specified host. It is recommended to either not specify + a host so that the scheduler will pick one, or specify a host without + ``--force``. + +``--strict`` + Evacuate host with exact hypervisor hostname match + +.. _nova_host-evacuate-live: + +nova host-evacuate-live +----------------------- + +.. code-block:: console + + usage: nova host-evacuate-live [--target-host ] [--block-migrate] + [--max-servers ] [--force] + [--strict] + + +Live migrate all instances off the specified host to other available hosts. + +**Positional arguments:** + +```` + Name of host. + The hypervisor hostname (or pattern) to search for. + + .. warning:: + + Use a fully qualified domain name if you only want to live migrate + from a specific host. + +**Optional arguments:** + +``--target-host `` + Name of target host. If no host is specified, the scheduler will choose + one. + +``--block-migrate`` + Enable block migration. (Default=auto) + (Supported by API versions '2.25' - '2.latest') + +``--max-servers `` + Maximum number of servers to live migrate + simultaneously + +``--force`` + Force a live-migration by not verifying the provided destination host by + the scheduler. (Supported by API versions '2.30' - '2.67') + + .. warning:: This could result in failures to actually live migrate the + servers to the specified host. It is recommended to either not specify + a host so that the scheduler will pick one, or specify a host without + ``--force``. + +``--strict`` + live Evacuate host with exact hypervisor hostname match + +.. _nova_host-meta: + +nova host-meta +-------------- + +.. code-block:: console + + usage: nova host-meta [--strict] [ ...] + +Set or Delete metadata on all instances of a host. + +**Positional arguments:** + +```` + The hypervisor hostname (or pattern) to search for. + + .. warning:: + + Use a fully qualified domain name if you only want to update + metadata for servers on a specific host. + +```` + Actions: 'set' or 'delete' + +```` + Metadata to set or delete (only key is necessary on delete) + +**Optional arguments:** + +``--strict`` + Set host-meta to the hypervisor with exact hostname match + +.. _nova_host-servers-migrate: + +nova host-servers-migrate +------------------------- + +.. code-block:: console + + usage: nova host-servers-migrate [--strict] + +Cold migrate all instances off the specified host to other available hosts. + +**Positional arguments:** + +```` + Name of host. + The hypervisor hostname (or pattern) to search for. + + .. warning:: + + Use a fully qualified domain name if you only want to cold migrate + from a specific host. + +**Optional arguments:** + +``--strict`` + Migrate host with exact hypervisor hostname match + +.. _nova_hypervisor-list: + +nova hypervisor-list +-------------------- + +.. code-block:: console + + usage: nova hypervisor-list [--matching ] [--marker ] + [--limit ] + +List hypervisors. (Supported by API versions '2.0' - '2.latest') [hint: use +'--os-compute-api-version' flag to show help message for proper version] + +**Optional arguments:** + +``--matching `` + List hypervisors matching the given . If + matching is used limit and marker options will be + ignored. + +``--marker `` + The last hypervisor of the previous page; displays + list of hypervisors after "marker". + (Supported by API versions '2.33' - '2.latest') + +``--limit `` + Maximum number of hypervisors to display. If limit is + bigger than 'CONF.api.max_limit' option of Nova API, + limit 'CONF.api.max_limit' will be used instead. + (Supported by API versions '2.33' - '2.latest') + +.. _nova_hypervisor-servers: + +nova hypervisor-servers +----------------------- + +.. code-block:: console + + usage: nova hypervisor-servers + +List servers belonging to specific hypervisors. + +**Positional arguments:** + +```` + The hypervisor hostname (or pattern) to search for. + +.. _nova_hypervisor-show: + +nova hypervisor-show +-------------------- + +.. code-block:: console + + usage: nova hypervisor-show [--wrap ] + +Display the details of the specified hypervisor. + +**Positional arguments:** + +```` + Name or ID of the hypervisor. + Starting with microversion 2.53 the ID must be a UUID. + +**Optional arguments:** + +``--wrap `` + Wrap the output to a specified length. Default is 40 or 0 + to disable + +.. _nova_hypervisor-stats: + +nova hypervisor-stats +--------------------- + +.. code-block:: console + + usage: nova hypervisor-stats + +Get hypervisor statistics over all compute nodes. + +.. _nova_hypervisor-uptime: + +nova hypervisor-uptime +---------------------- + +.. code-block:: console + + usage: nova hypervisor-uptime + +Display the uptime of the specified hypervisor. + +**Positional arguments:** + +```` + Name or ID of the hypervisor. + Starting with microversion 2.53 the ID must be a UUID. + +.. _nova_image-create: + +nova image-create +----------------- + +.. code-block:: console + + usage: nova image-create [--metadata ] [--show] [--poll] + + +Create a new image by taking a snapshot of a running server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name of snapshot. + +**Optional arguments:** + +``--metadata `` + Record arbitrary key/value metadata to + /meta_data.json on the metadata server. Can be + specified multiple times. + +``--show`` + Print image info. + +``--poll`` + Report the snapshot progress and poll until image + creation is complete. + +.. _nova_instance-action: + +nova instance-action +-------------------- + +.. code-block:: console + + usage: nova instance-action + +Show an action. + +**Positional arguments:** + +```` + Name or UUID of the server to show actions for. Only UUID can + be used to show actions for a deleted server. (Supported by + API versions '2.21' - '2.latest') + +```` + Request ID of the action to get. + +.. _nova_instance-action-list: + +nova instance-action-list +------------------------- + +.. code-block:: console + + usage: nova instance-action-list [--marker ] [--limit ] + [--changes-since ] + [--changes-before ] + + +List actions on a server. + +**Positional arguments:** + +```` + Name or UUID of the server to list actions for. Only UUID can be + used to list actions on a deleted server. (Supported by API + versions '2.21' - '2.latest') + +**Optional arguments:** + +``--marker `` + The last instance action of the previous page; displays list of actions + after "marker". (Supported by API versions '2.58' - '2.latest') + +``--limit `` + Maximum number of instance actions to display. Note that there is + a configurable max limit on the server, and the limit that is used will be + the minimum of what is requested here and what is configured + in the server. (Supported by API versions '2.58' - '2.latest') + +``--changes-since `` + List only instance actions changed later or equal to a certain + point of time. The provided time should be an ISO 8061 formatted time. + e.g. 2016-03-04T06:27:59Z. (Supported by API versions '2.58' - '2.latest') + +``--changes-before `` + List only instance actions changed earlier or equal to a certain + point of time. The provided time should be an ISO 8061 formatted time. + e.g. 2016-03-04T06:27:59Z. (Supported by API versions '2.66' - '2.latest') + +.. _nova_instance-usage-audit-log: + +nova instance-usage-audit-log +----------------------------- + +.. code-block:: console + + usage: nova instance-usage-audit-log [--before ] + +List/Get server usage audits. + +**Optional arguments:** + +``--before `` + Filters the response by the date and time before which to list usage audits. + The date and time stamp format is as follows: CCYY-MM-DD hh:mm:ss.NNNNNN + ex 2015-08-27 09:49:58 or 2015-08-27 09:49:58.123456. + +.. _nova_interface-attach: + +nova interface-attach +--------------------- + +.. code-block:: console + + usage: nova interface-attach [--port-id ] [--net-id ] + [--fixed-ip ] [--tag ] + + +Attach a network interface to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--port-id `` + Port ID. + +``--net-id `` + Network ID + +``--fixed-ip `` + Requested fixed IP. + +``--tag `` + Tag for the attached interface. + (Supported by API versions '2.49' - '2.latest') + +.. _nova_interface-detach: + +nova interface-detach +--------------------- + +.. code-block:: console + + usage: nova interface-detach + +Detach a network interface from a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Port ID. + +.. _nova_interface-list: + +nova interface-list +------------------- + +.. code-block:: console + + usage: nova interface-list + +List interfaces attached to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_keypair-add: + +nova keypair-add +---------------- + +.. code-block:: console + + usage: nova keypair-add [--pub-key ] [--key-type ] + [--user ] + + +Create a new key pair for use with servers. + +**Positional arguments:** + +```` + Name of key. + +**Optional arguments:** + +``--pub-key `` + Path to a public ssh key. + +``--key-type `` + Keypair type. Can be ssh or x509. (Supported by API + versions '2.2' - '2.latest') + +``--user `` + ID of user to whom to add key-pair (Admin only). + (Supported by API versions '2.10' - '2.latest') + +.. _nova_keypair-delete: + +nova keypair-delete +------------------- + +.. code-block:: console + + usage: nova keypair-delete [--user ] + +Delete keypair given by its name. (Supported by API versions '2.0' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Keypair name to delete. + +**Optional arguments:** + +``--user `` + ID of key-pair owner (Admin only). + +.. _nova_keypair-list: + +nova keypair-list +----------------- + +.. code-block:: console + + usage: nova keypair-list [--user ] [--marker ] + [--limit ] + +Print a list of keypairs for a user (Supported by API versions '2.0' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Optional arguments:** + +``--user `` + List key-pairs of specified user ID (Admin only). + +``--marker `` + The last keypair of the previous page; displays list of + keypairs after "marker". + +``--limit `` + Maximum number of keypairs to display. If limit is bigger + than 'CONF.api.max_limit' option of Nova API, limit + 'CONF.api.max_limit' will be used instead. + +.. _nova_keypair-show: + +nova keypair-show +----------------- + +.. code-block:: console + + usage: nova keypair-show [--user ] + +Show details about the given keypair. (Supported by API versions '2.0' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name of keypair. + +**Optional arguments:** + +``--user `` + ID of key-pair owner (Admin only). + +.. _nova_limits: + +nova limits +----------- + +.. code-block:: console + + usage: nova limits [--tenant []] [--reserved] + +Print rate and absolute limits. + +**Optional arguments:** + +``--tenant []`` + Display information from single tenant (Admin only). + +``--reserved`` + Include reservations count. + +.. _nova_list: + +nova list +--------- + +.. code-block:: console + + usage: nova list [--reservation-id ] [--ip ] + [--ip6 ] [--name ] + [--status ] [--flavor ] [--image ] + [--host ] [--all-tenants [<0|1>]] + [--tenant []] [--user []] [--deleted] + [--fields ] [--minimal] + [--sort [:]] [--marker ] + [--limit ] [--availability-zone ] + [--key-name ] [--[no-]config-drive] + [--progress ] [--vm-state ] + [--task-state ] [--power-state ] + [--changes-since ] + [--changes-before ] + [--tags ] [--tags-any ] + [--not-tags ] [--not-tags-any ] + [--locked] + +List servers. + +Note that from microversion 2.69, during partial infrastructure failures in the +deployment, the output of this command may return partial results for the servers +present in the failure domain. + +**Optional arguments:** + +``--reservation-id `` + Only return servers that match reservation-id. + +``--ip `` + Search with regular expression match by IP + address. + +``--ip6 `` + Search with regular expression match by IPv6 + address. + +``--name `` + Search with regular expression match by name. + +``--status `` + Search by server status. + +``--flavor `` + Search by flavor name or ID. + +``--image `` + Search by image name or ID. + +``--host `` + Search servers by hostname to which they are + assigned (Admin only). + +``--all-tenants [<0|1>]`` + Display information from all tenants (Admin + only). + +``--tenant []`` + Display information from single tenant (Admin + only). + +``--user []`` + Display information from single user (Admin + only until microversion 2.82). + +``--deleted`` + Only display deleted servers (Admin only). + +``--fields `` + Comma-separated list of fields to display. Use + the show command to see which fields are + available. + +``--minimal`` + Get only UUID and name. + +``--sort [:]`` + Comma-separated list of sort keys and + directions in the form of [:]. + The direction defaults to descending if not + specified. + +``--marker `` + The last server UUID of the previous page; + displays list of servers after "marker". + +``--limit `` + Maximum number of servers to display. If limit + == -1, all servers will be displayed. If limit + is bigger than 'CONF.api.max_limit' option of + Nova API, limit 'CONF.api.max_limit' will be + used instead. + +``--availability-zone `` + Display servers based on their availability zone + (Admin only until microversion 2.82). + +``--key-name `` + Display servers based on their keypair name + (Admin only until microversion 2.82). + +``--config-drive`` + Display servers that have a config drive attached. + It is mutually exclusive with '--no-config-drive'. + (Admin only until microversion 2.82). + +``--no-config-drive`` + Display servers that do not have a config drive attached. + It is mutually exclusive with '--config-drive'. + (Admin only until microversion 2.82). + +``--progress `` + Display servers based on their progress value + (Admin only until microversion 2.82). + +``--vm-state `` + Display servers based on their vm_state value + (Admin only until microversion 2.82). + +``--task-state `` + Display servers based on their task_state value + (Admin only until microversion 2.82). + +``--power-state `` + Display servers based on their power_state value + (Admin only until microversion 2.82). + +``--changes-since `` + List only servers changed later or equal to a + certain point of time. The provided time should + be an ISO 8061 formatted time. e.g. + 2016-03-04T06:27:59Z . + +``--changes-before `` + List only servers changed earlier or equal to a + certain point of time. The provided time should + be an ISO 8061 formatted time. e.g. + 2016-03-05T06:27:59Z . (Supported by API versions + '2.66' - '2.latest') + +``--tags `` + The given tags must all be present for a + server to be included in the list result. + Boolean expression in this case is 't1 AND + t2'. Tags must be separated by commas: --tags + (Supported by API versions '2.26' + - '2.latest') + +``--tags-any `` + If one of the given tags is present the server + will be included in the list result. Boolean + expression in this case is 't1 OR t2'. Tags + must be separated by commas: --tags-any + (Supported by API versions '2.26' + - '2.latest') + +``--not-tags `` + Only the servers that do not have any of the + given tags will be included in the list + results. Boolean expression in this case is + 'NOT(t1 AND t2)'. Tags must be separated by + commas: --not-tags (Supported by + API versions '2.26' - '2.latest') + +``--not-tags-any `` + Only the servers that do not have at least one + of the given tags will be included in the list + result. Boolean expression in this case is + 'NOT(t1 OR t2)'. Tags must be separated by + commas: --not-tags-any (Supported + by API versions '2.26' - '2.latest') + +``--locked `` + Display servers based on their locked value. A + value must be specified; eg. 'true' will list + only locked servers and 'false' will list only + unlocked servers. (Supported by API versions + '2.73' - '2.latest') + +.. _nova_list-secgroup: + +nova list-secgroup +------------------ + +.. code-block:: console + + usage: nova list-secgroup + +List Security Group(s) of a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_live-migration: + +nova live-migration +------------------- + +.. code-block:: console + + usage: nova live-migration [--block-migrate] [--force] [] + +Migrate running server to a new machine. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Destination host name. If no host is specified, the scheduler will choose + one. + +**Optional arguments:** + +``--block-migrate`` + True in case of block_migration. + (Default=auto:live_migration) (Supported by API versions + '2.25' - '2.latest') + +``--force`` + Force a live-migration by not verifying the provided destination host by + the scheduler. (Supported by API versions '2.30' - '2.67') + + .. warning:: This could result in failures to actually live migrate the + server to the specified host. It is recommended to either not specify + a host so that the scheduler will pick one, or specify a host without + ``--force``. + +.. _nova_live-migration-abort: + +nova live-migration-abort +------------------------- + +.. code-block:: console + + usage: nova live-migration-abort + +Abort an on-going live migration. (Supported by API versions '2.24' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +For microversions from 2.24 to 2.64 the migration status must be ``running``; +for microversion 2.65 and greater, the migration status can also be ``queued`` +and ``preparing``. + +.. versionadded:: 3.3.0 + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of migration. + +.. _nova_live-migration-force-complete: + +nova live-migration-force-complete +---------------------------------- + +.. code-block:: console + + usage: nova live-migration-force-complete + +Force on-going live migration to complete. (Supported by API versions '2.22' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +.. versionadded:: 3.3.0 + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of migration. + +.. _nova_lock: + +nova lock +--------- + +.. code-block:: console + + usage: nova lock [--reason ] + +Lock a server. A normal (non-admin) user will not be able to execute actions +on a locked server. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--reason `` + Reason for locking the server. (Supported by API versions + '2.73' - '2.latest') + +.. _nova_meta: + +nova meta +--------- + +.. code-block:: console + + usage: nova meta [ ...] + +Set or delete metadata on a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Actions: 'set' or 'delete'. + +```` + Metadata to set or delete (only key is necessary on delete). + +.. _nova_migrate: + +nova migrate +------------ + +.. code-block:: console + + usage: nova migrate [--host ] [--poll] + +Migrate a server. The new host will be selected by the scheduler. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--host `` + Destination host name. (Supported by API versions '2.56' - '2.latest') + +``--poll`` + Report the server migration progress until it completes. + +.. _nova_migration-list: + +nova migration-list +------------------- + +.. code-block:: console + + usage: nova migration-list [--instance-uuid ] + [--host ] + [--status ] + [--migration-type ] + [--source-compute ] + [--marker ] + [--limit ] + [--changes-since ] + [--changes-before ] + [--project-id ] + [--user-id ] + +Print a list of migrations. + +**Examples** + +To see the list of evacuation operations *from* a compute service host: + +.. code-block:: console + + nova migration-list --migration-type evacuation --source-compute host.foo.bar + +**Optional arguments:** + +``--instance-uuid `` + Fetch migrations for the given instance. + +``--host `` + Fetch migrations for the given source or destination host. + +``--status `` + Fetch migrations for the given status. + +``--migration-type `` + Filter migrations by type. Valid values are: + + * evacuation + * live-migration + * migration + + .. note:: This is a cold migration. + + * resize + +``--source-compute `` + Filter migrations by source compute host name. + +``--marker `` + The last migration of the previous page; displays list of migrations after + "marker". Note that the marker is the migration UUID. + (Supported by API versions '2.59' - '2.latest') + +``--limit `` + Maximum number of migrations to display. Note that there is a configurable + max limit on the server, and the limit that is used will be the minimum of + what is requested here and what is configured in the server. + (Supported by API versions '2.59' - '2.latest') + +``--changes-since `` + List only migrations changed later or equal to a certain + point of time. The provided time should be an ISO 8061 formatted time. + e.g. 2016-03-04T06:27:59Z . (Supported by API versions '2.59' - '2.latest') + +``--changes-before `` + List only migrations changed earlier or equal to a certain + point of time. The provided time should be an ISO 8061 formatted time. + e.g. 2016-03-04T06:27:59Z . (Supported by API versions '2.66' - '2.latest') + +``--project-id `` + Filter the migrations by the given project ID. + (Supported by API versions '2.80' - '2.latest') + +``--user-id `` + Filter the migrations by the given user ID. + (Supported by API versions '2.80' - '2.latest') + +.. _nova_pause: + +nova pause +---------- + +.. code-block:: console + + usage: nova pause + +Pause a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_quota-class-show: + +nova quota-class-show +--------------------- + +.. code-block:: console + + usage: nova quota-class-show + +List the quotas for a quota class. + +**Positional arguments:** + +```` + Name of quota class to list the quotas for. + +.. _nova_quota-class-update: + +nova quota-class-update +----------------------- + +.. code-block:: console + + usage: nova quota-class-update [--instances ] [--cores ] + [--ram ] + [--metadata-items ] + [--key-pairs ] + [--server-groups ] + [--server-group-members ] + + +Update the quotas for a quota class. (Supported by API versions '2.0' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + Name of quota class to set the quotas for. + +**Optional arguments:** + +``--instances `` + New value for the "instances" quota. + +``--cores `` + New value for the "cores" quota. + +``--ram `` + New value for the "ram" quota. + +``--metadata-items `` + New value for the "metadata-items" quota. + +``--key-pairs `` + New value for the "key-pairs" quota. + +``--server-groups `` + New value for the "server-groups" quota. + +``--server-group-members `` + New value for the "server-group-members" + quota. + +.. _nova_quota-defaults: + +nova quota-defaults +------------------- + +.. code-block:: console + + usage: nova quota-defaults [--tenant ] + +List the default quotas for a tenant. + +**Optional arguments:** + +``--tenant `` + ID of tenant to list the default quotas for. + +.. _nova_quota-delete: + +nova quota-delete +----------------- + +.. code-block:: console + + usage: nova quota-delete --tenant [--user ] + +Delete quota for a tenant/user so their quota will Revert back to default. + +**Optional arguments:** + +``--tenant `` + ID of tenant to delete quota for. + +``--user `` + ID of user to delete quota for. + +.. _nova_quota-show: + +nova quota-show +--------------- + +.. code-block:: console + + usage: nova quota-show [--tenant ] [--user ] [--detail] + +List the quotas for a tenant/user. + +**Optional arguments:** + +``--tenant `` + ID of tenant to list the quotas for. + +``--user `` + ID of user to list the quotas for. + +``--detail`` + Show detailed info (limit, reserved, in-use). + +.. _nova_quota-update: + +nova quota-update +----------------- + +.. code-block:: console + + usage: nova quota-update [--user ] [--instances ] + [--cores ] [--ram ] + [--metadata-items ] + [--key-pairs ] + [--server-groups ] + [--server-group-members ] + [--force] + + +Update the quotas for a tenant/user. (Supported by API versions '2.0' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +**Positional arguments:** + +```` + ID of tenant to set the quotas for. + +**Optional arguments:** + +``--user `` + ID of user to set the quotas for. + +``--instances `` + New value for the "instances" quota. + +``--cores `` + New value for the "cores" quota. + +``--ram `` + New value for the "ram" quota. + +``--metadata-items `` + New value for the "metadata-items" quota. + +``--key-pairs `` + New value for the "key-pairs" quota. + +``--server-groups `` + New value for the "server-groups" quota. + +``--server-group-members `` + New value for the "server-group-members" + quota. + +``--force`` + Whether force update the quota even if the + already used and reserved exceeds the new + quota. + +.. _nova_reboot: + +nova reboot +----------- + +.. code-block:: console + + usage: nova reboot [--hard] [--poll] [ ...] + +Reboot a server. + +**Positional arguments:** + +```` + Name or ID of server(s). + +**Optional arguments:** + +``--hard`` + Perform a hard reboot (instead of a soft one). Note: Ironic does + not currently support soft reboot; consequently, bare metal nodes + will always do a hard reboot, regardless of the use of this + option. + +``--poll`` + Poll until reboot is complete. + +.. _nova_rebuild: + +nova rebuild +------------ + +.. code-block:: console + + usage: nova rebuild [--rebuild-password ] [--poll] + [--minimal] [--preserve-ephemeral] [--name ] + [--description ] [--meta ] + [--key-name ] [--key-unset] + [--user-data ] [--user-data-unset] + [--trusted-image-certificate-id ] + [--trusted-image-certificates-unset] + [--hostname ] + + +Shutdown, re-image, and re-boot a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name or ID of new image. + +**Optional arguments:** + +``--rebuild-password `` + Set the provided admin password on the rebuilt + server. + +``--poll`` + Report the server rebuild progress until it + completes. + +``--minimal`` + Skips flavor/image lookups when showing + servers. + +``--preserve-ephemeral`` + Preserve the default ephemeral storage + partition on rebuild. + +``--name `` + Name for the new server. + +``--description `` + New description for the server. (Supported by + API versions '2.19' - '2.latest') + +``--meta `` + Record arbitrary key/value metadata to + /meta_data.json on the metadata server. Can be + specified multiple times. + +``--key-name `` + Keypair name to set in the server. Cannot be specified with + the '--key-unset' option. + (Supported by API versions '2.54' - '2.latest') + +``--key-unset`` + Unset keypair in the server. Cannot be specified with + the '--key-name' option. + (Supported by API versions '2.54' - '2.latest') + +``--user-data `` + User data file to pass to be exposed by the metadata server. + (Supported by API versions '2.57' - '2.latest') + +``--user-data-unset`` + Unset user_data in the server. Cannot be specified with + the '--user-data' option. + (Supported by API versions '2.57' - '2.latest') + +``--trusted-image-certificate-id `` + Trusted image certificate IDs used to validate certificates + during the image signature verification process. + Defaults to env[OS_TRUSTED_IMAGE_CERTIFICATE_IDS]. + May be specified multiple times to pass multiple trusted image + certificate IDs. (Supported by API versions '2.63' - '2.latest') + +``--trusted-image-certificates-unset`` + Unset trusted_image_certificates in the server. Cannot be + specified with the ``--trusted-image-certificate-id`` option. + (Supported by API versions '2.63' - '2.latest') + +``--hostname `` + New hostname for the instance. This only updates the hostname + stored in the metadata server: a utility running on the guest + is required to propagate these changes to the guest. + (Supported by API versions '2.90' - '2.latest') + +.. _nova_refresh-network: + +nova refresh-network +-------------------- + +.. code-block:: console + + usage: nova refresh-network + +Refresh server network information. + +**Positional arguments:** + +```` + Name or ID of a server for which the network cache should be + refreshed from neutron (Admin only). + +.. _nova_remove-secgroup: + +nova remove-secgroup +-------------------- + +.. code-block:: console + + usage: nova remove-secgroup + +Remove a Security Group from a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name of Security Group. + +.. _nova_rescue: + +nova rescue +----------- + +.. code-block:: console + + usage: nova rescue [--password ] [--image ] + +Reboots a server into rescue mode, which starts the machine from either the +initial image or a specified image, attaching the current boot disk as +secondary. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--password `` + The admin password to be set in the rescue + environment. + +``--image `` + The image to rescue with. + +.. _nova_reset-network: + +nova reset-network +------------------ + +.. code-block:: console + + usage: nova reset-network + +Reset network of a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_reset-state: + +nova reset-state +---------------- + +.. code-block:: console + + usage: nova reset-state [--all-tenants] [--active] [ ...] + +Reset the state of a server. + +**Positional arguments:** + +```` + Name or ID of server(s). + +**Optional arguments:** + +``--all-tenants`` + Reset state server(s) in another tenant by name (Admin only). + +``--active`` + Request the server be reset to "active" state instead of + "error" state (the default). + +.. _nova_resize: + +nova resize +----------- + +.. code-block:: console + + usage: nova resize [--poll] + +Resize a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Name or ID of new flavor. + +**Optional arguments:** + +``--poll`` + Report the server resize progress until it completes. + +.. _nova_resize-confirm: + +nova resize-confirm +------------------- + +.. code-block:: console + + usage: nova resize-confirm + +Confirm a previous resize. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_resize-revert: + +nova resize-revert +------------------ + +.. code-block:: console + + usage: nova resize-revert + +Revert a previous resize (and return to the previous VM). + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_restore: + +nova restore +------------ + +.. code-block:: console + + usage: nova restore + +Restore a soft-deleted server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_resume: + +nova resume +----------- + +.. code-block:: console + + usage: nova resume + +Resume a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_server-group-create: + +nova server-group-create +------------------------ + +.. code-block:: console + + usage: nova server-group-create [--rules ] + +Create a new server group with the specified details. + +**Positional arguments:** + +```` + Server group name. + +```` + Policy for the server groups. + +**Optional arguments:** + +``--rule`` + Policy rules for the server groups. (Supported by API versions + '2.64' - '2.latest'). Currently, only the ``max_server_per_host`` rule + is supported for the ``anti-affinity`` policy. The ``max_server_per_host`` + rule allows specifying how many members of the anti-affinity group can + reside on the same compute host. If not specified, only one member from + the same anti-affinity group can reside on a given host. + +.. _nova_server-group-delete: + +nova server-group-delete +------------------------ + +.. code-block:: console + + usage: nova server-group-delete [ ...] + +Delete specific server group(s). + +**Positional arguments:** + +```` + Unique ID(s) of the server group to delete. + +.. _nova_server-group-get: + +nova server-group-get +--------------------- + +.. code-block:: console + + usage: nova server-group-get + +Get a specific server group. + +**Positional arguments:** + +```` + Unique ID of the server group to get. + +.. _nova_server-group-list: + +nova server-group-list +---------------------- + +.. code-block:: console + + usage: nova server-group-list [--limit ] [--offset ] + [--all-projects] + +Print a list of all server groups. + +**Optional arguments:** + +``--limit `` + Maximum number of server groups to display. If limit is + bigger than 'CONF.api.max_limit' option of Nova API, + limit 'CONF.api.max_limit' will be used instead. + +``--offset `` + The offset of groups list to display; use with limit to + return a slice of server groups. + +``--all-projects`` + Display server groups from all projects (Admin only). + +.. _nova_server-migration-list: + +nova server-migration-list +-------------------------- + +.. code-block:: console + + usage: nova server-migration-list + +Get the migrations list of specified server. (Supported by API versions '2.23' +- '2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +.. versionadded:: 3.3.0 + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_server-migration-show: + +nova server-migration-show +-------------------------- + +.. code-block:: console + + usage: nova server-migration-show + +Get the migration of specified server. (Supported by API versions '2.23' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +.. versionadded:: 3.3.0 + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of migration. + +.. _nova_server-tag-add: + +nova server-tag-add +------------------- + +.. code-block:: console + + usage: nova server-tag-add [ ...] + +Add one or more tags to a server. (Supported by API versions '2.26' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +.. versionadded:: 4.1.0 + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Tag(s) to add. + +.. _nova_server-tag-delete: + +nova server-tag-delete +---------------------- + +.. code-block:: console + + usage: nova server-tag-delete [ ...] + +Delete one or more tags from a server. (Supported by API versions '2.26' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +.. versionadded:: 4.1.0 + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Tag(s) to delete. + +.. _nova_server-tag-delete-all: + +nova server-tag-delete-all +-------------------------- + +.. code-block:: console + + usage: nova server-tag-delete-all + +Delete all tags from a server. (Supported by API versions '2.26' - '2.latest') +[hint: use '--os-compute-api-version' flag to show help message for proper +version] + +.. versionadded:: 4.1.0 + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_server-tag-list: + +nova server-tag-list +-------------------- + +.. code-block:: console + + usage: nova server-tag-list + +Get list of tags from a server. (Supported by API versions '2.26' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +.. versionadded:: 4.1.0 + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_server-tag-set: + +nova server-tag-set +------------------- + +.. code-block:: console + + usage: nova server-tag-set [ ...] + +Set list of tags to a server. (Supported by API versions '2.26' - '2.latest') +[hint: use '--os-compute-api-version' flag to show help message for proper +version] + +.. versionadded:: 4.1.0 + +**Positional arguments:** + +```` + Name or ID of server. + +```` + Tag(s) to set. + +.. _nova_server_topology: + +nova server-topology +-------------------- + +.. code-block:: console + + usage: nova server-topology + +Retrieve server NUMA topology information. Host specific fields are only +visible to users with the administrative role. +(Supported by API versions '2.78' - '2.latest') + +.. versionadded:: 15.1.0 + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_service-delete: + +nova service-delete +------------------- + +.. code-block:: console + + usage: nova service-delete + +Delete the service. + +.. important:: If deleting a nova-compute service, be sure to stop the actual + ``nova-compute`` process on the physical host *before* deleting the + service with this command. Failing to do so can lead to the running + service re-creating orphaned **compute_nodes** table records in the + database. + +**Positional arguments:** + +```` + ID of service as a UUID. (Supported by API versions '2.53' - '2.latest') + +.. _nova_service-disable: + +nova service-disable +-------------------- + +.. code-block:: console + + usage: nova service-disable [--reason ] + +Disable the service. + +**Positional arguments:** + +```` + ID of the service as a UUID. (Supported by API versions '2.53' - '2.latest') + +**Optional arguments:** + +``--reason `` + Reason for disabling the service. + +.. _nova_service-enable: + +nova service-enable +------------------- + +.. code-block:: console + + usage: nova service-enable + +Enable the service. + +**Positional arguments:** + +```` + ID of the service as a UUID. (Supported by API versions '2.53' - '2.latest') + +.. _nova_service-force-down: + +nova service-force-down +----------------------- + +.. code-block:: console + + usage: nova service-force-down [--unset] + +Force service to down. (Supported by API versions '2.11' - '2.latest') [hint: +use '--os-compute-api-version' flag to show help message for proper version] + +.. versionadded:: 2.27.0 + +**Positional arguments:** + +```` + ID of the service as a UUID. (Supported by API versions '2.53' - '2.latest') + + +**Optional arguments:** + +``--unset`` + Unset the forced_down state of the service. + +.. _nova_service-list: + +nova service-list +----------------- + +.. code-block:: console + + usage: nova service-list [--host ] [--binary ] + +Show a list of all running services. Filter by host & binary. + +Note that from microversion 2.69, during partial infrastructure failures in the +deployment, the output of this command may return partial results for the +services present in the failure domain. + +**Optional arguments:** + +``--host `` + Name of host. + +``--binary `` + Service binary. + +.. _nova_set-password: + +nova set-password +----------------- + +.. code-block:: console + + usage: nova set-password + +Change the admin password for a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_shelve: + +nova shelve +----------- + +.. code-block:: console + + usage: nova shelve + +Shelve a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_shelve-offload: + +nova shelve-offload +------------------- + +.. code-block:: console + + usage: nova shelve-offload + +Remove a shelved server from the compute node. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_show: + +nova show +--------- + +.. code-block:: console + + usage: nova show [--minimal] [--wrap ] + +Show details about the given server. + +Note that from microversion 2.69, during partial infrastructure failures in the +deployment, the output of this command may return partial results for the server +if it exists in the failure domain. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--minimal`` + Skips flavor/image lookups when showing servers. + +``--wrap `` + Wrap the output to a specified length, or 0 to disable. + +.. _nova_ssh: + +nova ssh +-------- + +.. code-block:: console + + usage: nova ssh [--port PORT] [--address-type ADDRESS_TYPE] + [--network ] [--ipv6] [--login ] [-i IDENTITY] + [--extra-opts EXTRA] + + +SSH into a server. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--port PORT`` + Optional flag to indicate which port to use + for ssh. (Default=22) + +``--address-type ADDRESS_TYPE`` + Optional flag to indicate which IP type to + use. Possible values includes fixed and + floating (the Default). + +``--network `` + Network to use for the ssh. + +``--ipv6`` + Optional flag to indicate whether to use an + IPv6 address attached to a server. (Defaults + to IPv4 address) + +``--login `` + Login to use. + +``-i IDENTITY, --identity IDENTITY`` + Private key file, same as the -i option to the + ssh command. + +``--extra-opts EXTRA`` + Extra options to pass to ssh. see: man ssh. + +.. _nova_start: + +nova start +---------- + +.. code-block:: console + + usage: nova start [--all-tenants] [ ...] + +Start the server(s). + +**Positional arguments:** + +```` + Name or ID of server(s). + +**Optional arguments:** + +``--all-tenants`` + Start server(s) in another tenant by name (Admin only). + +.. _nova_stop: + +nova stop +--------- + +.. code-block:: console + + usage: nova stop [--all-tenants] [ ...] + +Stop the server(s). + +**Positional arguments:** + +```` + Name or ID of server(s). + +**Optional arguments:** + +``--all-tenants`` + Stop server(s) in another tenant by name (Admin only). + +.. _nova_suspend: + +nova suspend +------------ + +.. code-block:: console + + usage: nova suspend + +Suspend a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_trigger-crash-dump: + +nova trigger-crash-dump +----------------------- + +.. code-block:: console + + usage: nova trigger-crash-dump + +Trigger crash dump in an instance. (Supported by API versions '2.17' - +'2.latest') [hint: use '--os-compute-api-version' flag to show help message +for proper version] + +.. versionadded:: 3.3.0 + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_unlock: + +nova unlock +----------- + +.. code-block:: console + + usage: nova unlock + +Unlock a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_unpause: + +nova unpause +------------ + +.. code-block:: console + + usage: nova unpause + +Unpause a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_unrescue: + +nova unrescue +------------- + +.. code-block:: console + + usage: nova unrescue + +Restart the server from normal boot disk again. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_unshelve: + +nova unshelve +------------- + +.. code-block:: console + + usage: nova unshelve [--availability-zone ] + +Unshelve a server. + +**Positional arguments:** + +```` + Name or ID of server. + +**Optional arguments:** + +``--availability-zone `` + Name of the availability zone in which to unshelve a ``SHELVED_OFFLOADED`` + server. (Supported by API versions '2.77' - '2.latest') + +.. _nova_update: + +nova update +----------- + +.. code-block:: console + + usage: nova update [--name ] [--description ] + [--hostname ] + + +Update attributes of a server. + +**Positional arguments:** + +```` + Name (old name) or ID of server. + +**Optional arguments:** + +``--name `` + New name for the server. + +``--description `` + New description for the server. If it equals to + empty string (i.g. ""), the server description + will be removed. (Supported by API versions + '2.19' - '2.latest') + +``--hostname `` + New hostname for the instance. This only updates the hostname + stored in the metadata server: a utility running on the guest + is required to propagate these changes to the guest. + (Supported by API versions '2.90' - '2.latest') + +.. _nova_usage: + +nova usage +---------- + +.. code-block:: console + + usage: nova usage [--start ] [--end ] [--tenant ] + +Show usage data for a single tenant. + +**Optional arguments:** + +``--start `` + Usage range start date ex 2012-01-20. (default: 4 + weeks ago) + +``--end `` + Usage range end date, ex 2012-01-20. (default: + tomorrow) + +``--tenant `` + UUID of tenant to get usage for. + +.. _nova_usage-list: + +nova usage-list +--------------- + +.. code-block:: console + + usage: nova usage-list [--start ] [--end ] + +List usage data for all tenants. + +**Optional arguments:** + +``--start `` + Usage range start date ex 2012-01-20. (default: 4 weeks + ago) + +``--end `` + Usage range end date, ex 2012-01-20. (default: tomorrow) + +.. _nova_version-list: + +nova version-list +----------------- + +.. code-block:: console + + usage: nova version-list + +List all API versions. + +.. _nova_volume-attach: + +nova volume-attach +------------------ + +.. code-block:: console + + usage: nova volume-attach [--delete-on-termination] [--tag ] + [] + +Attach a volume to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of the volume to attach. + +```` + Name of the device e.g. /dev/vdb. Use "auto" for autoassign (if + supported). Libvirt driver will use default device name. + +**Optional arguments:** + +``--tag `` + Tag for the attached volume. (Supported by API versions '2.49' - '2.latest') + +``--delete-on-termination`` + Specify if the attached volume should be deleted when the server is + destroyed. By default the attached volume is not deleted when the server is + destroyed. (Supported by API versions '2.79' - '2.latest') + +.. _nova_volume-attachments: + +nova volume-attachments +----------------------- + +.. code-block:: console + + usage: nova volume-attachments + +List all the volumes attached to a server. + +**Positional arguments:** + +```` + Name or ID of server. + +.. _nova_volume-detach: + +nova volume-detach +------------------ + +.. code-block:: console + + usage: nova volume-detach + +Detach a volume from a server. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of the volume to detach. + +.. _nova_volume-update: + +nova volume-update +------------------ + +.. code-block:: console + + usage: nova volume-update [--[no-]delete-on-termination] + + +Update the attachment on the server. Migrates the data from an attached volume +to the specified available volume and swaps out the active attachment to the +new volume. + +**Positional arguments:** + +```` + Name or ID of server. + +```` + ID of the source (original) volume. + +```` + ID of the destination volume. + +**Optional arguments:** + +``--delete-on-termination`` + Specify that the volume should be deleted when the server is destroyed. + It is mutually exclusive with '--no-delete-on-termination'. + (Supported by API versions '2.85' - '2.latest') + +``--no-delete-on-termination`` + Specify that the attached volume should not be deleted when + the server is destroyed. It is mutually exclusive with '--delete-on-termination'. + (Supported by API versions '2.85' - '2.latest') + +.. _nova_bash-completion: + +nova bash-completion +-------------------- + +.. code-block:: console + + usage: nova bash-completion + +Prints all of the commands and options to stdout so that the +nova.bash_completion script doesn't have to hard code them. diff --git a/doc/source/conf.py b/doc/source/conf.py index c252baab1..0552cccab 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -17,43 +17,27 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', 'openstackdocstheme', + 'sphinx.ext.autodoc', + 'sphinxcontrib.apidoc', ] +# sphinxcontrib.apidoc options +apidoc_module_dir = '../../novaclient' +apidoc_output_dir = 'reference/api' +apidoc_excluded_paths = [ + 'tests/*'] +apidoc_separate_modules = True + # The content that will be inserted into the main body of an autoclass # directive. autoclass_content = 'both' -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - # The master toctree document. master_doc = 'index' -# openstackdocstheme options -repository_name = 'openstack/python-novaclient' -bug_project = 'python-novaclient' -bug_tag = 'doc' -project = 'python-novaclient' copyright = 'OpenStack Contributors' -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = [] - -# If true, '()' will be appended to :func: etc. cross-reference text. -add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -add_module_names = True - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' # -- Options for HTML output -------------------------------------------------- @@ -61,11 +45,37 @@ # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'openstackdocs' +# Add any paths that contain "extra" files, such as .htaccess or +# robots.txt. +html_extra_path = ['_extra'] + + +# -- Options for LaTeX output ------------------------------------------------- + +latex_documents = [ + ('index', 'doc-python-novaclient.tex', 'python-novaclient Documentation', + 'OpenStack Foundation', 'manual'), +] + +latex_elements = { + 'extraclassoptions': 'openany,oneside', + 'preamble': r'\setcounter{tocdepth}{4}', + 'makeindex': '', + 'printindex': '', +} + # -- Options for openstackdocstheme ------------------------------------------- -repository_name = 'openstack/python-novaclient' -bug_project = 'python-novaclient' -bug_tag = '' +openstackdocs_repo_name = 'openstack/python-novaclient' +openstackdocs_bug_project = 'python-novaclient' +openstackdocs_bug_tag = '' +openstackdocs_pdf_link = True +openstackdocs_projects = [ + 'keystoneauth', + 'nova', + 'os-client-config', + 'python-openstackclient', +] # -- Options for manual page output ------------------------------------------ diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst new file mode 100644 index 000000000..10365558c --- /dev/null +++ b/doc/source/contributor/contributing.rst @@ -0,0 +1,58 @@ +============================ +So You Want to Contribute... +============================ + +For general information on contributing to OpenStack, please check out the +`contributor guide `_ to get started. +It covers all the basics that are common to all OpenStack projects: the accounts +you need, the basics of interacting with our Gerrit review system, how we +communicate as a community, etc. + +Below will cover the more project specific information you need to get started +with python-novaclient. + +.. important:: + + The ``nova`` CLI has been deprecated in favour of the unified ``openstack`` + CLI. Changes to the Python bindings are still welcome, however, no further + changes should be made to the shell. + +Communication +~~~~~~~~~~~~~ + +Please refer `how-to-get-involved `_. + +Contacting the Core Team +~~~~~~~~~~~~~~~~~~~~~~~~ + +The easiest way to reach the core team is via IRC, using the ``openstack-nova`` +OFTC IRC channel. + +New Feature Planning +~~~~~~~~~~~~~~~~~~~~ + +If you want to propose a new feature please read the +`blueprints `_ page. + +Task Tracking +~~~~~~~~~~~~~ + +We track our tasks in `Launchpad `__. + +If you're looking for some smaller, easier work item to pick up and get started +on, search for the 'low-hanging-fruit' tag. + +Reporting a Bug +~~~~~~~~~~~~~~~ + +You found an issue and want to make sure we are aware of it? You can do so on +`Launchpad `__. +More info about Launchpad usage can be found on `OpenStack docs page +`_. + +Getting Your Patch Merged +~~~~~~~~~~~~~~~~~~~~~~~~~ + +All changes proposed to the python-novaclient requires two ``Code-Review +2`` +votes from ``python-novaclient`` core reviewers before one of the core reviewers +can approve patch by giving ``Workflow +1`` vote.. diff --git a/doc/source/reference/deprecation-policy.rst b/doc/source/contributor/deprecation-policy.rst similarity index 90% rename from doc/source/reference/deprecation-policy.rst rename to doc/source/contributor/deprecation-policy.rst index 0085bca90..c7e84fbbc 100644 --- a/doc/source/reference/deprecation-policy.rst +++ b/doc/source/contributor/deprecation-policy.rst @@ -21,9 +21,9 @@ The process for command deprecation is: 2. Once the change is approved, have a member of the `nova-release`_ team release a new version of `python-novaclient`. - .. _nova-release: https://review.openstack.org/#/admin/groups/147,members + .. _nova-release: https://review.opendev.org/#/admin/groups/147,members -3. Example: ``_ +3. Example: ``_ This change was made while the nova 12.0.0 Liberty release was in development. The current version of `python-novaclient` at the time was diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 4df1ed3f3..b52cfce63 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -2,15 +2,19 @@ Contributor Guide =================== -Code is hosted at `git.openstack.org`__. Submit bugs to the Nova project on -`Launchpad`__. Submit code to the `openstack/python-novaclient` project using -`Gerrit`__. +Basic Information +================= -__ https://git.openstack.org/cgit/openstack/python-novaclient -__ https://launchpad.net/nova -__ https://docs.openstack.org/infra/manual/developers.html#development-workflow +.. toctree:: + :maxdepth: 2 + + contributing +Developer Guide +=============== .. toctree:: :maxdepth: 2 + microversions testing + deprecation-policy diff --git a/doc/source/contributor/microversions.rst b/doc/source/contributor/microversions.rst new file mode 100644 index 000000000..a3194a900 --- /dev/null +++ b/doc/source/contributor/microversions.rst @@ -0,0 +1,71 @@ +===================================== +Adding support for a new microversion +===================================== + +If a new microversion is added on the nova side, +then support must be added on the *python-novaclient* side also. +The following procedure describes how to add support for a new microversion +in *python-novaclient*. + +#. Update ``API_MAX_VERSION`` + + Set ``API_MAX_VERSION`` in ``novaclient/__init__.py`` to the version + you are going to support. + + .. note:: + + Microversion support should be added one by one in order. + For example, microversion 2.74 should be added right after + microversion 2.73. Microversion 2.74 should not be added right + after microversion 2.72 or earlier. + +#. Update CLI and Python API + + Update CLI (``novaclient/v2/shell.py``) and/or Python API + (``novaclient/v2/*.py``) to support the microversion. + +#. Add tests + + Add unit tests for the change. Add unit tests for the previous microversion + to check raising an error or an exception when new arguments or parameters + are specified. Add functional tests if necessary. + + Add the microversion in the ``exclusions`` in the ``test_versions`` + method of the ``novaclient.tests.unit.v2.test_shell.ShellTest`` class + if there are no versioned wrapped method changes for the microversion. + The versioned wrapped methods have ``@api_versions.wraps`` decorators. + + For example (microversion 2.72 example):: + + exclusions = set([ + (snipped...) + 72, # There are no version-wrapped shell method changes for this. + ]) + +#. Update the CLI reference + + Update the CLI reference (``doc/source/cli/nova.rst``) + if the CLI commands and/or arguments are modified. + +#. Add a release note + + Add a release note for the change. The release note should include a link + to the description for the microversion in the + :nova-doc:`Compute API Microversion History + `. + +#. Commit message + + The description of the blueprint and dependency on the patch in nova side + should be added in the commit message. For example:: + + Implements: blueprint remove-force-flag-from-live-migrate-and-evacuate + Depends-On: https://review.opendev.org/#/c/634600/ + +See the following examples: + +- `Microversion 2.71 - show server group `_ +- `API microversion 2.69: Handles Down Cells `_ +- `Microversion 2.68: Remove 'forced' live migrations, evacuations `_ +- `Add support changes-before for microversion 2.66 `_ +- `Microversion 2.64 - Use new format policy in server group `_ diff --git a/doc/source/contributor/testing.rst b/doc/source/contributor/testing.rst index b789dc2d2..d6a2cfd71 100644 --- a/doc/source/contributor/testing.rst +++ b/doc/source/contributor/testing.rst @@ -6,23 +6,23 @@ The preferred way to run the unit tests is using ``tox``. There are multiple test targets that can be run to validate the code. ``tox -e pep8`` - Style guidelines enforcement. -``tox -e py27`` - - Traditional unit testing. +``tox -e py310`` + Traditional unit testing (Python 3.10). ``tox -e functional`` + Live functional testing against an existing OpenStack instance. (Python 3.10) - Live functional testing against an existing OpenStack instance. +``tox -e cover`` + Generate a coverage report on unit testing. Functional testing assumes the existence of a `clouds.yaml` file as supported -by `os-client-config `__ +by :os-client-config-doc:`os-client-config <>`. It assumes the existence of a cloud named `devstack` that behaves like a normal DevStack installation with a demo and an admin user/tenant - or clouds named `functional_admin` and `functional_nonadmin`. Refer to `Consistent Testing Interface`__ for more details. -__ http://git.openstack.org/cgit/openstack/governance/tree/reference/project-testing-interface.rst +__ https://governance.openstack.org/tc/reference/project-testing-interface.html diff --git a/doc/source/index.rst b/doc/source/index.rst index 4ee0122c1..60791b8e7 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -2,13 +2,13 @@ Python bindings to the OpenStack Nova API =========================================== -This is a client for OpenStack Nova API. There's :doc:`a Python API -` (the :mod:`novaclient` module), and a :doc:`command-line -script ` (installed as :program:`nova`). Each implements the -entire OpenStack Nova API. +This is a client for OpenStack Nova API. There's a :doc:`Python API +` (the :mod:`novaclient` module), and a deprecated +:doc:`command-line script ` (installed as :program:`nova`). +Each implements the entire OpenStack Nova API. -You'll need credentials for an OpenStack cloud that implements the Compute API, -such as TryStack, HP, or Rackspace, in order to use the nova client. +You'll need credentials for an OpenStack cloud that implements the Compute API +in order to use the nova client. .. seealso:: @@ -16,12 +16,12 @@ such as TryStack, HP, or Rackspace, in order to use the nova client. to get an idea of the concepts. By understanding the concepts this library should make more sense. - __ https://developer.openstack.org/api-guide/compute/index.html + __ https://docs.openstack.org/api-guide/compute/index.html .. toctree:: :maxdepth: 2 user/index - reference/index cli/index + reference/index contributor/index diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst index 92e93855a..272b64ada 100644 --- a/doc/source/reference/index.rst +++ b/doc/source/reference/index.rst @@ -1,8 +1,8 @@ +========= Reference ========= .. toctree:: - :maxdepth: 1 + :maxdepth: 6 - api/index - deprecation-policy + api/modules diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst index 3c54920b2..32510e676 100644 --- a/doc/source/user/index.rst +++ b/doc/source/user/index.rst @@ -6,3 +6,4 @@ :maxdepth: 2 shell + python-api diff --git a/doc/source/reference/api/index.rst b/doc/source/user/python-api.rst similarity index 89% rename from doc/source/reference/api/index.rst rename to doc/source/user/python-api.rst index 0d2938347..80adda3a1 100644 --- a/doc/source/reference/api/index.rst +++ b/doc/source/user/python-api.rst @@ -4,6 +4,7 @@ .. module:: novaclient :synopsis: A client for the OpenStack Nova API. + :no-index: .. currentmodule:: novaclient @@ -53,9 +54,8 @@ application, you can append a (name, version) tuple to the session's >>> sess = session.Session(auth=auth) >>> sess.additional_user_agent.append(('shade', '1.2.3')) -For more information on this keystoneauth API, see `Using Sessions`_. - -.. _Using Sessions: https://docs.openstack.org/keystoneauth/latest/using-sessions.html +For more information on this keystoneauth API, see +:keystoneauth-doc:`Using Sessions `. It is also possible to use an instance as a context manager in which case there will be a session kept alive for the duration of the with statement:: @@ -82,11 +82,11 @@ Then call methods on its managers:: >>> nova.flavors.list() [, , - , - , - , - , - ] + , + , + , + , + ] >>> fl = nova.flavors.find(ram=512) >>> nova.servers.create("my-server", flavor=fl) @@ -101,9 +101,4 @@ Then call methods on its managers:: Reference --------- -For more information, see the reference: - -.. toctree:: - :maxdepth: 2 - - autoindex +See :doc:`the module reference `. diff --git a/doc/source/user/shell.rst b/doc/source/user/shell.rst index b2d7d9ef8..de96637a7 100644 --- a/doc/source/user/shell.rst +++ b/doc/source/user/shell.rst @@ -8,45 +8,82 @@ The :program:`nova` shell utility interacts with OpenStack Nova API from the command line. It supports the entirety of the OpenStack Nova API. -First, you'll need an OpenStack Nova account and an API key. You get this by -using the `nova-manage` command in OpenStack Nova. +You'll need to provide :program:`nova` with your OpenStack Keystone user +information. You can do this with the `--os-username`, `--os-password`, +`--os-project-name` (`--os-project-id`), `--os-project-domain-name` +(`--os-project-domain-id`) and `--os-user-domain-name` (`--os-user-domain-id`) +options, but it's easier to just set them as environment variables by setting +some environment variables: -You'll need to provide :program:`nova` with your OpenStack username and API -key. You can do this with the `--os-username`, `--os-password` and -`--os-tenant-id` options, but it's easier to just set them as environment -variables by setting some environment variables: +.. deprecated:: 17.8.0 + + The ``nova`` CLI has been deprecated in favour of the unified + ``openstack`` CLI. For information on using the ``openstack`` CLI, see + :python-openstackclient-doc:`OpenStackClient <>`. .. envvar:: OS_USERNAME - Your OpenStack Nova username. + Your OpenStack Keystone user name. .. envvar:: OS_PASSWORD Your password. -.. envvar:: OS_TENANT_NAME +.. envvar:: OS_PROJECT_NAME + + The name of project for work. + +.. envvar:: OS_PROJECT_ID + + The ID of project for work. + +.. envvar:: OS_PROJECT_DOMAIN_NAME + + The name of domain containing the project. + +.. envvar:: OS_PROJECT_DOMAIN_ID + + The ID of domain containing the project. + +.. envvar:: OS_USER_DOMAIN_NAME - Project for work. + The user's domain name. + +.. envvar:: OS_USER_DOMAIN_ID + + The user's domain ID. .. envvar:: OS_AUTH_URL - The OpenStack API server URL. + The OpenStack Keystone endpoint URL. .. envvar:: OS_COMPUTE_API_VERSION - The OpenStack API version. + The OpenStack Nova API version (microversion). .. envvar:: OS_REGION_NAME The Keystone region name. Defaults to the first region if multiple regions are available. +.. envvar:: OS_TRUSTED_IMAGE_CERTIFICATE_IDS + + A comma-delimited list of trusted image certificate IDs. Only used + with the ``nova boot`` and ``nova rebuild`` commands starting with the + 2.63 microversion. + + For example:: + + export OS_TRUSTED_IMAGE_CERTIFICATE_IDS=trusted-cert-id1,trusted-cert-id2 + For example, in Bash you'd use:: export OS_USERNAME=yourname export OS_PASSWORD=yadayadayada - export OS_TENANT_NAME=myproject - export OS_AUTH_URL=http://:5000/v3/ + export OS_PROJECT_NAME=myproject + export OS_PROJECT_DOMAIN_NAME=default + export OS_USER_DOMAIN_NAME=default + export OS_AUTH_URL=http:///identity export OS_COMPUTE_API_VERSION=2.1 From there, all shell commands take the form:: @@ -56,12 +93,4 @@ From there, all shell commands take the form:: Run :program:`nova help` to get a full list of all possible commands, and run :program:`nova help ` to get detailed help for that command. -Reference ---------- - -For more information, see the reference: - -.. toctree:: - :maxdepth: 2 - - /cli/nova +For more information, see :doc:`the command reference `. diff --git a/doc/test/redirect-tests.txt b/doc/test/redirect-tests.txt new file mode 100644 index 000000000..0959ec54f --- /dev/null +++ b/doc/test/redirect-tests.txt @@ -0,0 +1,3 @@ +/python-novaclient/latest/api.html 301 /python-novaclient/latest/reference/api/index.html +/python-novaclient/latest/man/nova.html 301 /python-novaclient/latest/cli/nova.html +/python-novaclient/latest/shell.html 301 /python-novaclient/latest/user/shell.html diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 3fe7488d0..bf021c6ac 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.53") +API_MAX_VERSION = api_versions.APIVersion("2.96") diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py index 61cded280..59b8c1cbc 100644 --- a/novaclient/api_versions.py +++ b/novaclient/api_versions.py @@ -14,7 +14,6 @@ import functools import logging import os -import pkgutil import re import traceback import warnings @@ -195,35 +194,59 @@ def __repr__(self): def get_available_major_versions(): - # NOTE(andreykurilin): available clients version should not be - # hardcoded, so let's discover them. - matcher = re.compile(r"v[0-9]*$") - submodules = pkgutil.iter_modules([os.path.dirname(__file__)]) - available_versions = [name[1:] for loader, name, ispkg in submodules - if matcher.search(name)] - - return available_versions + return ['2'] def check_major_version(api_version): """Checks major part of ``APIVersion`` obj is supported. :raises novaclient.exceptions.UnsupportedVersion: if major part is not - supported + supported """ - available_versions = get_available_major_versions() - if (not api_version.is_null() and - str(api_version.ver_major) not in available_versions): - if len(available_versions) == 1: - msg = _("Invalid client version '%(version)s'. " - "Major part should be '%(major)s'") % { - "version": api_version.get_string(), - "major": available_versions[0]} - else: - msg = _("Invalid client version '%(version)s'. " - "Major part must be one of: '%(major)s'") % { - "version": api_version.get_string(), - "major": ", ".join(available_versions)} + if api_version.is_null(): + return + + if api_version.ver_major == 2: + return + + msg = _( + "Invalid client version '%(version)s'. Major part should be '2'" + ) % {"version": api_version.get_string()} + raise exceptions.UnsupportedVersion(msg) + + +def check_version(api_version): + """Checks if version of ``APIVersion`` is supported. + + Provided as an alternative to :func:`check_major_version` to avoid changing + the behavior of that function. + + :raises novaclient.exceptions.UnsupportedVersion: if major part is not + supported + """ + if api_version.is_null(): + return + + # we can't use API_MIN_VERSION since we do support 2.0 (which is 2.1 but + # less strict) + if api_version < APIVersion('2.0'): + msg = _( + "Invalid client version '%(version)s'. " + "Min version supported is '%(min_version)s'" + ) % { + "version": api_version.get_string(), + "min_version": novaclient.API_MIN_VERSION, + } + raise exceptions.UnsupportedVersion(msg) + + if api_version > novaclient.API_MAX_VERSION: + msg = _( + "Invalid client version '%(version)s'. " + "Max version supported is '%(max_version)s'" + ) % { + "version": api_version.get_string(), + "max_version": novaclient.API_MAX_VERSION, + } raise exceptions.UnsupportedVersion(msg) @@ -324,11 +347,11 @@ def update_headers(headers, api_version): def check_headers(response, api_version): """Checks that microversion header is in response.""" if api_version.ver_minor > 0: - if (api_version.ver_minor < 27 - and LEGACY_HEADER_NAME not in response.headers): + if (api_version.ver_minor < 27 and + LEGACY_HEADER_NAME not in response.headers): _warn_missing_microversion_header(LEGACY_HEADER_NAME) - elif (api_version.ver_minor >= 27 - and HEADER_NAME not in response.headers): + elif (api_version.ver_minor >= 27 and + HEADER_NAME not in response.headers): _warn_missing_microversion_header(HEADER_NAME) @@ -393,7 +416,7 @@ def substitution(obj, *args, **kwargs): return methods[-1].func(obj, *args, **kwargs) # Let's share "arguments" with original method and substitution to - # allow put cliutils.arg and wraps decorators in any order + # allow put utils.arg and wraps decorators in any order if not hasattr(func, 'arguments'): func.arguments = [] substitution.arguments = func.arguments diff --git a/novaclient/base.py b/novaclient/base.py index 6bcb527c4..48c06f372 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -28,8 +28,7 @@ from oslo_utils import reflection from oslo_utils import strutils -from requests import Response -import six +import requests from novaclient import exceptions from novaclient import utils @@ -41,10 +40,7 @@ def getid(obj): Abstracts the common pattern of allowing both an object or an object's ID as a parameter when dealing with relationships. """ - try: - return obj.id - except AttributeError: - return obj + return getattr(obj, 'id', obj) # TODO(aababilov): call run_hooks() in HookableMixin's child classes @@ -103,7 +99,7 @@ def append_request_ids(self, resp): self._append_request_id(resp) def _append_request_id(self, resp): - if isinstance(resp, Response): + if isinstance(resp, requests.Response): # Extract 'x-openstack-request-id' from headers if # response is a Response object. request_id = (resp.headers.get('x-openstack-request-id') or @@ -247,7 +243,10 @@ def client(self): def api_version(self): return self.api.api_version - def _list(self, url, response_key, obj_class=None, body=None): + def _list(self, url, response_key, obj_class=None, body=None, + filters=None): + if filters: + url = utils.get_url_with_filter(url, filters) if body: resp, body = self.api.client.post(url, body=body) else: @@ -307,8 +306,8 @@ def completion_cache(self, cache_type, obj_class, mode): # endpoint pair username = utils.env('OS_USERNAME', 'NOVA_USERNAME') url = utils.env('OS_URL', 'NOVA_URL') - uniqifier = hashlib.md5(username.encode('utf-8') + - url.encode('utf-8')).hexdigest() + uniqifier = hashlib.sha256(username.encode('utf-8') + + url.encode('utf-8')).hexdigest() cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) @@ -347,7 +346,9 @@ def write_to_completion_cache(self, cache_type, val): if cache: cache.write("%s\n" % val) - def _get(self, url, response_key): + def _get(self, url, response_key, filters=None): + if filters: + url = utils.get_url_with_filter(url, filters) resp, body = self.api.client.get(url) if response_key is not None: content = body[response_key] @@ -356,15 +357,19 @@ def _get(self, url, response_key): return self.resource_class(self, content, loaded=True, resp=resp) - def _create(self, url, body, response_key, return_raw=False, **kwargs): + def _create(self, url, body, response_key, return_raw=False, + obj_class=None, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) resp, body = self.api.client.post(url, body=body) if return_raw: return self.convert_into_with_meta(body[response_key], resp) - with self.completion_cache('human_id', self.resource_class, mode="a"): - with self.completion_cache('uuid', self.resource_class, mode="a"): - return self.resource_class(self, body[response_key], resp=resp) + if obj_class is None: + obj_class = self.resource_class + + with self.completion_cache('human_id', obj_class, mode="a"): + with self.completion_cache('uuid', obj_class, mode="a"): + return obj_class(self, body[response_key], resp=resp) def _delete(self, url): resp, body = self.api.client.delete(url) @@ -382,12 +387,9 @@ def _update(self, url, body, response_key=None, **kwargs): return StrWithMeta(body, resp) def convert_into_with_meta(self, item, resp): - if isinstance(item, six.string_types): - if six.PY2 and isinstance(item, six.text_type): - return UnicodeWithMeta(item, resp) - else: - return StrWithMeta(item, resp) - elif isinstance(item, six.binary_type): + if isinstance(item, str): + return StrWithMeta(item, resp) + elif isinstance(item, bytes): return BytesWithMeta(item, resp) elif isinstance(item, list): return ListWithMeta(item, resp) @@ -399,8 +401,7 @@ def convert_into_with_meta(self, item, resp): return DictWithMeta(item, resp) -@six.add_metaclass(abc.ABCMeta) -class ManagerWithFind(Manager): +class ManagerWithFind(Manager, metaclass=abc.ABCMeta): """Like a `Manager`, but with additional `find()`/`findall()` methods.""" @abc.abstractmethod @@ -496,7 +497,7 @@ def _parse_block_device_mapping(self, block_device_mapping): for device_name, mapping in block_device_mapping.items(): # # The mapping is in the format: - # :[]:[]:[] + # :[]:[]:[] # bdm_dict = {'device_name': device_name} @@ -554,20 +555,10 @@ def __init__(self, values, resp): self.append_request_ids(resp) -class BytesWithMeta(six.binary_type, RequestIdMixin): +class BytesWithMeta(bytes, RequestIdMixin): def __new__(cls, value, resp): return super(BytesWithMeta, cls).__new__(cls, value) def __init__(self, values, resp): self.request_ids_setup() self.append_request_ids(resp) - - -if six.PY2: - class UnicodeWithMeta(six.text_type, RequestIdMixin): - def __new__(cls, value, resp): - return super(UnicodeWithMeta, cls).__new__(cls, value) - - def __init__(self, values, resp): - self.request_ids_setup() - self.append_request_ids(resp) diff --git a/novaclient/client.py b/novaclient/client.py index 2d2716310..369d8d08e 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -28,10 +28,7 @@ from keystoneauth1 import identity from keystoneauth1 import session as ksession from oslo_utils import importutils -import pkg_resources - -osprofiler_profiler = importutils.try_import("osprofiler.profiler") -osprofiler_web = importutils.try_import("osprofiler.web") +import stevedore import novaclient from novaclient import api_versions @@ -40,10 +37,8 @@ from novaclient.i18n import _ from novaclient import utils -# TODO(jichenjc): when an extension in contrib is moved to core extension, -# Add the name into the following list, then after last patch merged, -# remove the whole function -extensions_ignored_name = ["__init__"] +osprofiler_profiler = importutils.try_import("osprofiler.profiler") +osprofiler_web = importutils.try_import("osprofiler.web") class SessionClient(adapter.LegacyJsonAdapter): @@ -56,6 +51,12 @@ def __init__(self, *args, **kwargs): self.timings = kwargs.pop('timings', False) self.api_version = kwargs.pop('api_version', None) self.api_version = self.api_version or api_versions.APIVersion() + + if isinstance(self.api_version, str): + self.api_version = api_versions.APIVersion(self.api_version) + + api_versions.check_version(self.api_version) + super(SessionClient, self).__init__(*args, **kwargs) def request(self, url, method, **kwargs): @@ -90,21 +91,6 @@ def get_timings(self): def reset_timings(self): self.times = [] - @property - def management_url(self): - self.logger.warning( - _("Property `management_url` is deprecated for SessionClient. " - "Use `endpoint_override` instead.")) - return self.endpoint_override - - @management_url.setter - def management_url(self, value): - self.logger.warning( - _("Property `management_url` is deprecated for SessionClient. " - "It should be set via `endpoint_override` variable while class" - " initialization.")) - self.endpoint_override = value - def _construct_http_client(api_version=None, auth=None, @@ -178,14 +164,6 @@ def discover_extensions(*args, **kwargs): """Returns the list of extensions, which can be discovered by python path and by entry-point 'novaclient.extension'. """ - # TODO(mriedem): Remove support for 'only_contrib' in Queens. - if 'only_contrib' in kwargs and kwargs['only_contrib']: - warnings.warn(_('Discovering extensions only by contrib path is no ' - 'longer supported since all contrib extensions ' - 'have either been made required or removed. The ' - 'only_contrib argument is deprecated and will be ' - 'removed in a future release.')) - return [] chain = itertools.chain(_discover_via_python_path(), _discover_via_entry_points()) return [ext.Extension(name, module) for name, module in chain] @@ -204,12 +182,15 @@ def _discover_via_python_path(): yield name, module -def _discover_via_entry_points(): - for ep in pkg_resources.iter_entry_points('novaclient.extension'): - name = ep.name - module = ep.load() +def _make_discovery_manager(): + # This function provides a place to mock out the entry point scan + return stevedore.ExtensionManager('novaclient.extension') - yield name, module + +def _discover_via_entry_points(): + mgr = _make_discovery_manager() + for extension in mgr: + yield extension.name, extension.plugin def _get_client_class_and_version(version): @@ -224,14 +205,6 @@ def _get_client_class_and_version(version): "novaclient.v%s.client.Client" % version.ver_major) -def get_client_class(version): - """Returns Client class based on given version.""" - warnings.warn(_("'get_client_class' is deprecated. " - "Please use `novaclient.client.Client` instead.")) - _api_version, client_class = _get_client_class_and_version(version) - return client_class - - def _check_arguments(kwargs, release, deprecated_name, right_name=None): """Process deprecation of arguments. diff --git a/novaclient/crypto.py b/novaclient/crypto.py index 21af60cdc..f6d77fa75 100644 --- a/novaclient/crypto.py +++ b/novaclient/crypto.py @@ -14,7 +14,7 @@ # under the License. import base64 -import subprocess +import subprocess # nosec: B404 class DecryptionFailure(Exception): @@ -30,9 +30,12 @@ def decrypt_password(private_key, password): cmd = ['openssl', 'rsautl', '-decrypt', '-inkey', private_key] proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + stderr=subprocess.PIPE) # nosec: B603 out, err = proc.communicate(unencoded) proc.stdin.close() if proc.returncode: raise DecryptionFailure(err) + + if isinstance(out, bytes): + return out.decode('utf-8') return out diff --git a/novaclient/exceptions.py b/novaclient/exceptions.py index 707aa889e..4c0a5d8df 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -48,52 +48,17 @@ def __init__(self, argument_name, start_version, end_version=None): self.message = ( "'%(name)s' argument is only allowed since microversion " "%(start)s." % {"name": argument_name, "start": start_version}) + super(UnsupportedAttribute, self).__init__(self.message) class CommandError(Exception): pass -class AuthorizationFailure(Exception): - pass - - class NoUniqueMatch(Exception): pass -class NoTokenLookupException(Exception): - """This form of authentication does not support looking up - endpoints from an existing token. - """ - pass - - -class EndpointNotFound(Exception): - """Could not find Service or Region in Service Catalog.""" - pass - - -class AmbiguousEndpoints(Exception): - """Found more than one matching endpoint in Service Catalog.""" - def __init__(self, endpoints=None): - self.endpoints = endpoints - - def __str__(self): - return "AmbiguousEndpoints: %s" % repr(self.endpoints) - - -class ConnectionRefused(Exception): - """ - Connection refused: the server refused the connection. - """ - def __init__(self, response=None): - self.response = response - - def __str__(self): - return "ConnectionRefused: %s" % repr(self.response) - - class ResourceInErrorState(Exception): """Resource is in the error state.""" diff --git a/novaclient/shell.py b/novaclient/shell.py index 4bf46aed6..303c7f3a8 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -18,9 +18,7 @@ Command-line interface to the OpenStack Nova API. """ -from __future__ import print_function import argparse -import getpass import logging import sys @@ -28,17 +26,6 @@ from oslo_utils import encodeutils from oslo_utils import importutils from oslo_utils import strutils -import six - -osprofiler_profiler = importutils.try_import("osprofiler.profiler") - -HAS_KEYRING = False -all_errors = ValueError -try: - import keyring - HAS_KEYRING = True -except ImportError: - pass import novaclient from novaclient import api_versions @@ -48,6 +35,8 @@ from novaclient.i18n import _ from novaclient import utils +osprofiler_profiler = importutils.try_import("osprofiler.profiler") + DEFAULT_MAJOR_OS_COMPUTE_API_VERSION = "2.0" # The default behaviour of nova client CLI is that CLI negotiates with server # to find out the most recent version between client and server, and @@ -132,7 +121,7 @@ def __init__(self, option_strings, dest, help=None, # option self.real_action_args = False self.real_action = None - elif real_action is None or isinstance(real_action, six.string_types): + elif real_action is None or isinstance(real_action, str): # Specified by string (or None); we have to have a parser # to look up the actual action, so defer to later self.real_action_args = (option_strings, dest, help, kwargs) @@ -210,133 +199,6 @@ def __call__(self, parser, namespace, values, option_string): action(parser, namespace, values, option_string) -class SecretsHelper(object): - def __init__(self, args, client): - self.args = args - self.client = client - self.key = None - self._password = None - - def _validate_string(self, text): - if text is None or len(text) == 0: - return False - return True - - def _make_key(self): - if self.key is not None: - return self.key - keys = [ - self.client.auth_url, - self.client.projectid, - self.client.user, - self.client.region_name, - self.client.endpoint_type, - self.client.service_type, - self.client.service_name, - ] - for (index, key) in enumerate(keys): - if key is None: - keys[index] = '?' - else: - keys[index] = str(keys[index]) - self.key = "/".join(keys) - return self.key - - def _prompt_password(self, verify=True): - pw = None - if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): - # Check for Ctl-D - try: - while True: - pw1 = getpass.getpass('OS Password: ') - if verify: - pw2 = getpass.getpass('Please verify: ') - else: - pw2 = pw1 - if pw1 == pw2 and self._validate_string(pw1): - pw = pw1 - break - except EOFError: - pass - return pw - - def save(self, auth_token, management_url, tenant_id): - if not HAS_KEYRING or not self.args.os_cache: - return - if (auth_token == self.auth_token and - management_url == self.management_url): - # Nothing changed.... - return - if not all([management_url, auth_token, tenant_id]): - raise ValueError(_("Unable to save empty management url/auth " - "token")) - value = "|".join([str(auth_token), - str(management_url), - str(tenant_id)]) - keyring.set_password("novaclient_auth", self._make_key(), value) - - @property - def password(self): - # Cache password so we prompt user at most once - if self._password: - pass - elif self._validate_string(self.args.os_password): - self._password = self.args.os_password - else: - verify_pass = strutils.bool_from_string( - utils.env("OS_VERIFY_PASSWORD", default=False), True) - self._password = self._prompt_password(verify_pass) - if not self._password: - raise exc.CommandError( - 'Expecting a password provided via either ' - '--os-password, env[OS_PASSWORD], or ' - 'prompted response') - return self._password - - @property - def management_url(self): - if not HAS_KEYRING or not self.args.os_cache: - return None - management_url = None - try: - block = keyring.get_password('novaclient_auth', self._make_key()) - if block: - _token, management_url, _tenant_id = block.split('|', 2) - except all_errors: - pass - return management_url - - @property - def auth_token(self): - # Now is where it gets complicated since we - # want to look into the keyring module, if it - # exists and see if anything was provided in that - # file that we can use. - if not HAS_KEYRING or not self.args.os_cache: - return None - token = None - try: - block = keyring.get_password('novaclient_auth', self._make_key()) - if block: - token, _management_url, _tenant_id = block.split('|', 2) - except all_errors: - pass - return token - - @property - def tenant_id(self): - if not HAS_KEYRING or not self.args.os_cache: - return None - tenant_id = None - try: - block = keyring.get_password('novaclient_auth', self._make_key()) - if block: - _token, _management_url, tenant_id = block.split('|', 2) - except all_errors: - pass - return tenant_id - - class NovaClientArgumentParser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): @@ -369,8 +231,11 @@ def _get_option_tuples(self, option_string): option_tuples = (super(NovaClientArgumentParser, self) ._get_option_tuples(option_string)) if len(option_tuples) > 1: - normalizeds = [option.replace('_', '-') - for action, option, value in option_tuples] + # In Python < 3.12, this is a 3-part tuple: + # action, option_string, explicit_arg + # In Python >= 3.12, this is a 4 part tuple: + # action, option_string, sep, explicit_arg + normalizeds = [opt[1].replace('_', '-') for opt in option_tuples] if len(set(normalizeds)) == 1: return option_tuples[:1] return option_tuples @@ -385,11 +250,11 @@ def __init__(self): def _append_global_identity_args(self, parser, argv): # Register the CLI arguments that have moved to the session object. loading.register_session_argparse_arguments(parser) - # Peek into argv to see if os-auth-token or os-token were given, + # Peek into argv to see if os-token was given, # in which case, the token auth plugin is what the user wants # else, we'll default to password default_auth_plugin = 'password' - if 'os-token' in argv: + if "--os-token" in argv: default_auth_plugin = 'token' loading.register_auth_argparse_arguments( parser, argv, default=default_auth_plugin) @@ -406,6 +271,14 @@ def _append_global_identity_args(self, parser, argv): 'OS_PROJECT_NAME', 'OS_TENANT_NAME', 'NOVA_PROJECT_ID')) parser.set_defaults(os_project_id=utils.env( 'OS_PROJECT_ID', 'OS_TENANT_ID')) + parser.set_defaults( + os_project_domain_id=utils.env('OS_PROJECT_DOMAIN_ID')) + parser.set_defaults( + os_project_domain_name=utils.env('OS_PROJECT_DOMAIN_NAME')) + parser.set_defaults( + os_user_domain_id=utils.env('OS_USER_DOMAIN_ID')) + parser.set_defaults( + os_user_domain_name=utils.env('OS_USER_DOMAIN_NAME')) def get_base_parser(self, argv): parser = NovaClientArgumentParser( @@ -486,21 +359,17 @@ def get_base_parser(self, argv): '"X.latest", defaults to env[OS_COMPUTE_API_VERSION].')) parser.add_argument( - '--endpoint-override', + '--os-endpoint-override', metavar='', dest='endpoint_override', - default=utils.env('NOVACLIENT_ENDPOINT_OVERRIDE', + default=utils.env('OS_ENDPOINT_OVERRIDE', + 'NOVACLIENT_ENDPOINT_OVERRIDE', 'NOVACLIENT_BYPASS_URL'), help=_("Use this API endpoint instead of the Service Catalog. " - "Defaults to env[NOVACLIENT_ENDPOINT_OVERRIDE].")) + "Defaults to env[OS_ENDPOINT_OVERRIDE].")) - parser.add_argument( - '--bypass-url', - action=DeprecatedAction, - use=_('use "%s"; this option will be removed after Pike OpenStack ' - 'release.') % '--os-endpoint-override', - dest='endpoint_override', - help=argparse.SUPPRESS) + parser.set_defaults(func=self.do_help) + parser.set_defaults(command='') if osprofiler_profiler: parser.add_argument('--profile', @@ -578,6 +447,7 @@ def _find_actions(self, subparsers, actions_module, version, do_help): action_help = desc.strip() arguments = getattr(callback, 'arguments', []) + groups = {} subparser = subparsers.add_parser( command, @@ -592,10 +462,14 @@ def _find_actions(self, subparsers, actions_module, version, do_help): ) self.subcommands[command] = subparser for (args, kwargs) in arguments: - start_version = kwargs.get("start_version", None) + kwargs = kwargs.copy() + + start_version = kwargs.pop("start_version", None) + end_version = kwargs.pop("end_version", None) + group = kwargs.pop("group", None) + if start_version: start_version = api_versions.APIVersion(start_version) - end_version = kwargs.get("end_version", None) if end_version: end_version = api_versions.APIVersion(end_version) else: @@ -607,10 +481,16 @@ def _find_actions(self, subparsers, actions_module, version, do_help): "end": end_version.get_string()}) if not version.matches(start_version, end_version): continue - kw = kwargs.copy() - kw.pop("start_version", None) - kw.pop("end_version", None) - subparser.add_argument(*args, **kw) + + if group: + if group not in groups: + groups[group] = ( + subparser.add_mutually_exclusive_group() + ) + kwargs['dest'] = kwargs.get('dest', group) + groups[group].add_argument(*args, **kwargs) + else: + subparser.add_argument(*args, **kwargs) subparser.set_defaults(func=callback) def setup_debugging(self, debug): @@ -649,8 +529,10 @@ def main(self, argv): api_version = api_versions.get_api_version( args.os_compute_api_version) - os_username = args.os_username - os_user_id = args.os_user_id + auth_token = getattr(args, "os_token", None) + + os_username = getattr(args, "os_username", None) + os_user_id = getattr(args, "os_user_id", None) os_password = None # Fetched and set later as needed os_project_name = getattr( args, 'os_project_name', getattr(args, 'os_tenant_name', None)) @@ -665,13 +547,16 @@ def main(self, argv): if (not args.os_project_domain_id and not args.os_project_domain_name): setattr(args, "os_project_domain_id", "default") - if not args.os_user_domain_id and not args.os_user_domain_name: + + # os_user_domain_id is redundant in case of Token auth type + if not auth_token and (not args.os_user_domain_id and + not args.os_user_domain_name): setattr(args, "os_user_domain_id", "default") os_project_domain_id = args.os_project_domain_id os_project_domain_name = args.os_project_domain_name - os_user_domain_id = args.os_project_domain_id - os_user_domain_name = args.os_project_domain_name + os_user_domain_id = getattr(args, "os_user_domain_id", None) + os_user_domain_name = getattr(args, "os_user_domain_name", None) endpoint_type = args.endpoint_type insecure = args.insecure @@ -686,14 +571,6 @@ def main(self, argv): keystone_session = None keystone_auth = None - # We may have either, both or none of these. - # If we have both, we don't need USERNAME, PASSWORD etc. - # Fill in the blanks from the SecretsHelper if possible. - # Finally, authenticate unless we have both. - # Note if we don't auth we probably don't have a tenant ID so we can't - # cache the token. - auth_token = getattr(args, 'os_token', None) - if not endpoint_type: endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE @@ -716,11 +593,11 @@ def main(self, argv): # for os_username or os_password but for compatibility it is not. if must_auth and not skip_auth: - if not os_username and not os_user_id: + if not any([auth_token, os_username, os_user_id]): raise exc.CommandError( - _("You must provide a username " - "or user ID via --os-username, --os-user-id, " - "env[OS_USERNAME] or env[OS_USER_ID]")) + _("You must provide a user name/id (via --os-username, " + "--os-user-id, env[OS_USERNAME] or env[OS_USER_ID]) or " + "an auth token (via --os-token).")) if not any([os_project_name, os_project_id]): raise exc.CommandError(_("You must provide a project name or" @@ -735,6 +612,26 @@ def main(self, argv): _("You must provide an auth url " "via either --os-auth-url or env[OS_AUTH_URL].")) + # TODO(Shilpasd): need to provide support in python - novaclient + # for required options for below default auth type plugins: + # 1. v3oidcclientcredential + # 2. v3oidcpassword + # 3. v3oidcauthcode + # 4. v3oidcaccesstoken + # 5. v3oauth1 + # 6. v3fedkerb + # 7. v3adfspassword + # 8. v3samlpassword + # 9. v3applicationcredential + # TODO(Shilpasd): need to provide support in python - novaclient + # for below extra keystoneauth auth type plugins: + # We will need to add code to support discovering of versions + # supported by the keystone service based on the auth_url similar + # to the one supported by glanceclient. + # 1. v3password + # 2. v3token + # 3. v3kerberos + # 4. v3totp with utils.record_time(self.times, args.timings, 'auth_url', args.os_auth_url): keystone_session = ( @@ -847,27 +744,6 @@ def main(self, argv): user_domain_id=os_user_domain_id, user_domain_name=os_user_domain_name) - # Now check for the password/token of which pieces of the - # identifying keyring key can come from the underlying client - if must_auth: - helper = SecretsHelper(args, self.cs.client) - self.cs.client.keyring_saver = helper - - tenant_id = helper.tenant_id - # Allow commandline to override cache - if not auth_token: - auth_token = helper.auth_token - endpoint_override = endpoint_override or helper.management_url - if tenant_id and auth_token and endpoint_override: - self.cs.client.tenant_id = tenant_id - self.cs.client.auth_token = auth_token - self.cs.client.management_url = endpoint_override - self.cs.client.password_func = lambda: helper.password - else: - # We're missing something, so auth with user/pass and save - # the result in our helper. - self.cs.client.password = helper.password - args.func(self.cs, args) if osprofiler_profiler and args.profile: @@ -943,19 +819,22 @@ def start_section(self, heading): super(OpenStackHelpFormatter, self).start_section(heading) -def main(): +def main(argv=sys.argv[1:]): try: - argv = [encodeutils.safe_decode(a) for a in sys.argv[1:]] + print( + _( + "nova CLI is deprecated and will be removed in a future " + "release" + ), + file=sys.stderr, + ) + argv = [encodeutils.safe_decode(a) for a in argv] OpenStackComputeShell().main(argv) except Exception as exc: logger.debug(exc, exc_info=1) - if six.PY2: - message = encodeutils.safe_encode(six.text_type(exc)) - else: - message = encodeutils.exception_to_unicode(exc) print("ERROR (%(type)s): %(msg)s" % { 'type': exc.__class__.__name__, - 'msg': message}, + 'msg': exc}, file=sys.stderr) sys.exit(1) except KeyboardInterrupt: diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py index 637ae52b9..9a391d1a0 100644 --- a/novaclient/tests/functional/base.py +++ b/novaclient/tests/functional/base.py @@ -13,40 +13,25 @@ import os import time -from cinderclient.v2 import client as cinderclient import fixtures -from glanceclient import client as glanceclient -from keystoneauth1.exceptions import discovery as discovery_exc from keystoneauth1 import identity from keystoneauth1 import session as ksession -from keystoneclient import client as keystoneclient -from keystoneclient import discover as keystone_discover -from neutronclient.v2_0 import client as neutronclient -import os_client_config +import openstack.config +import openstack.config.exceptions +import openstack.connection from oslo_utils import uuidutils import tempest.lib.cli.base import testtools import novaclient import novaclient.api_versions +from novaclient import base import novaclient.client from novaclient.v2 import networks import novaclient.v2.shell BOOT_IS_COMPLETE = ("login as 'cirros' user. default password: " - "'cubswin:)'. use 'sudo' for root.") - - -def is_keystone_version_available(session, version): - """Given a (major, minor) pair, check if the API version is enabled.""" - - d = keystone_discover.Discover(session) - try: - d.create_client(version) - except (discovery_exc.DiscoveryFailure, discovery_exc.VersionNotAvailable): - return False - else: - return True + "'gocubsgo'. use 'sudo' for root.") # The following are simple filter functions that filter our available @@ -172,18 +157,18 @@ def setUp(self): # tempest-lib, we do it in a way that's not available for top # level tests. Long term this probably needs to be in the base # class. - openstack_config = os_client_config.config.OpenStackConfig() + openstack_config = openstack.config.OpenStackConfig() try: cloud_config = openstack_config.get_one_cloud('functional_admin') - except os_client_config.exceptions.OpenStackConfigException: + except openstack.config.exceptions.OpenStackConfigException: try: cloud_config = openstack_config.get_one_cloud( 'devstack', auth=dict( username='admin', project_name='admin')) - except os_client_config.exceptions.OpenStackConfigException: + except openstack.config.exceptions.OpenStackConfigException: try: cloud_config = openstack_config.get_one_cloud('envvars') - except os_client_config.exceptions.OpenStackConfigException: + except openstack.config.exceptions.OpenStackConfigException: cloud_config = None if cloud_config is None: @@ -204,6 +189,8 @@ def setUp(self): self.insecure = cloud_config.config['insecure'] else: self.insecure = False + self.cacert = cloud_config.config['cacert'] + self.cert = cloud_config.config['cert'] auth = identity.Password(username=user, password=passwd, @@ -211,24 +198,27 @@ def setUp(self): auth_url=auth_url, project_domain_id=self.project_domain_id, user_domain_id=user_domain_id) - session = ksession.Session(auth=auth, verify=(not self.insecure)) + session = ksession.Session( + cert=self.cert, + auth=auth, + verify=(self.cacert or not self.insecure) + ) self.client = self._get_novaclient(session) - self.glance = glanceclient.Client('2', session=session) + self.openstack = openstack.connection.Connection(session=session) # pick some reasonable flavor / image combo if "flavor" not in CACHE: CACHE["flavor"] = pick_flavor(self.client.flavors.list()) if "image" not in CACHE: - CACHE["image"] = pick_image(self.glance.images.list()) + CACHE["image"] = pick_image(self.openstack.image.images()) self.flavor = CACHE["flavor"] self.image = CACHE["image"] if "network" not in CACHE: # Get the networks from neutron. - neutron = neutronclient.Client(session=session) - neutron_networks = neutron.list_networks()['networks'] + neutron_networks = self.openstack.network.networks() # Convert the neutron dicts to Network objects. nets = [] for network in neutron_networks: @@ -250,7 +240,7 @@ def setUp(self): # something more sensible. cli_dir = os.environ.get( 'OS_NOVACLIENT_EXEC_DIR', - os.path.join(os.path.abspath('.'), '.tox/functional/bin')) + os.path.join(os.environ['TOX_ENV_DIR'], 'bin')) self.cli_clients = tempest.lib.cli.base.CLIClient( username=user, @@ -260,11 +250,6 @@ def setUp(self): cli_dir=cli_dir, insecure=self.insecure) - self.keystone = keystoneclient.Client(session=session, - username=user, - password=passwd) - self.cinder = cinderclient.Client(auth=auth, session=session) - def _get_novaclient(self, session): nc = novaclient.client.Client("2", session=session) @@ -324,7 +309,7 @@ def wait_for_volume_status(self, volume, status, timeout=60, """ start_time = time.time() while time.time() - start_time < timeout: - volume = self.cinder.volumes.get(volume.id) + volume = self.openstack.block_storage.get_volume(volume) if volume.status == status: break time.sleep(poll_interval) @@ -371,15 +356,22 @@ def wait_for_resource_delete(self, resource, manager, raise time.sleep(poll_interval) else: - self.fail("The resource '%s' still exists." % resource.id) - - def name_generate(self, prefix='Entity'): - """Generate randomized name for some entity. - - :param prefix: string prefix - """ - name = "%s-%s" % (prefix, uuidutils.generate_uuid()) - return name + self.fail("The resource '%s' still exists." % base.getid(resource)) + + def name_generate(self): + """Generate randomized name for some entity.""" + # NOTE(andreykurilin): name_generator method is used for various + # resources (servers, flavors, volumes, keystone users, etc). + # Since the length of name has limits we cannot use the whole UUID, + # so the first 8 chars is taken from it. + # Based on the fact that the new name includes class and method + # names, 8 chars of uuid should be enough to prevent any conflicts, + # even if the single test will be launched in parallel thousand times + return "%(prefix)s-%(test_cls)s-%(test_name)s" % { + "prefix": uuidutils.generate_uuid()[:8], + "test_cls": self.__class__.__name__, + "test_name": self.id().rsplit(".", 1)[-1] + } def _get_value_from_the_table(self, table, key): """Parses table to get desired value. @@ -468,14 +460,15 @@ def _get_list_of_values_from_single_column_table(self, table, column): values.append(line.split("|")[1].strip()) return values - def _create_server(self, name=None, with_network=True, add_cleanup=True, - **kwargs): - name = name or self.name_generate(prefix='server') + def _create_server(self, name=None, flavor=None, with_network=True, + add_cleanup=True, **kwargs): + name = name or self.name_generate() if with_network: nics = [{"net-id": self.network.id}] else: nics = None - server = self.client.servers.create(name, self.image, self.flavor, + flavor = flavor or self.flavor + server = self.client.servers.create(name, self.image, flavor, nics=nics, **kwargs) if add_cleanup: self.addCleanup(server.delete) @@ -491,11 +484,9 @@ def _wait_for_state_change(self, server_id, status): def _get_project_id(self, name): """Obtain project id by project name.""" - if self.keystone.version == "v3": - project = self.keystone.projects.find(name=name) - else: - project = self.keystone.tenants.find(name=name) - return project.id + return self.openstack.identity.find_project( + name, ignore_missing=False + ).id def _cleanup_server(self, server_id): """Deletes a server and waits for it to be gone.""" @@ -512,42 +503,61 @@ def _get_absolute_limits(self): return {limit.name: limit.value for limit in self.client.limits.get(reserved=True).absolute} - -class TenantTestBase(ClientTestBase): - """Base test class for additional tenant and user creation which + def _pick_alternate_flavor(self): + """Given the flavor picked in the base class setup, this finds the + opposite flavor to use for a resize test. For example, if m1.nano is + the flavor, then use m1.micro, but those are only available if Tempest + is configured. If m1.tiny, then use m1.small. + """ + flavor_name = self.flavor.name + if flavor_name == 'm1.nano': + # This is an upsize test. + return 'm1.micro' + if flavor_name == 'm1.micro': + # This is a downsize test. + return 'm1.nano' + if flavor_name == 'm1.tiny': + # This is an upsize test. + return 'm1.small' + if flavor_name == 'm1.small': + # This is a downsize test. + return 'm1.tiny' + self.fail('Unable to find alternate for flavor: %s' % flavor_name) + + +class ProjectTestBase(ClientTestBase): + """Base test class for additional project and user creation which could be required in various test scenarios """ def setUp(self): - super(TenantTestBase, self).setUp() - user_name = self.name_generate('v' + self.COMPUTE_API_VERSION) - project_name = self.name_generate('v' + self.COMPUTE_API_VERSION) + super(ProjectTestBase, self).setUp() + user_name = uuidutils.generate_uuid() + project_name = uuidutils.generate_uuid() password = 'password' - if self.keystone.version == "v3": - project = self.keystone.projects.create(project_name, - self.project_domain_id) - self.project_id = project.id - self.addCleanup(self.keystone.projects.delete, self.project_id) - - self.user_id = self.keystone.users.create( - name=user_name, password=password, - default_project=self.project_id).id - - for role in self.keystone.roles.list(): - if "member" in role.name.lower(): - self.keystone.roles.grant(role.id, user=self.user_id, - project=self.project_id) - break - else: - project = self.keystone.tenants.create(project_name) - self.project_id = project.id - self.addCleanup(self.keystone.tenants.delete, self.project_id) + project = self.openstack.identity.create_project( + name=project_name, + domain_id=self.project_domain_id) + self.project_id = project.id + self.addCleanup( + self.openstack.identity.delete_project, self.project_id) + + self.user_id = self.openstack.identity.create_user( + name=user_name, password=password, + default_project=self.project_id).id + + for role in self.openstack.identity.roles(): + if "member" in role.name.lower(): + self.openstack.identity.assign_project_role_to_user( + project=self.project_id, + user=self.user_id, + role=role.id) + break - self.user_id = self.keystone.users.create( - user_name, password, tenant_id=self.project_id).id + self.addCleanup( + self.openstack.identity.delete_user, self.user_id) - self.addCleanup(self.keystone.users.delete, self.user_id) self.cli_clients_2 = tempest.lib.cli.base.CLIClient( username=user_name, password=password, diff --git a/novaclient/v2/contrib/migrations.py b/novaclient/tests/functional/hooks/check_resources.py similarity index 62% rename from novaclient/v2/contrib/migrations.py rename to novaclient/tests/functional/hooks/check_resources.py index 909b569c6..1d2d1bdba 100644 --- a/novaclient/v2/contrib/migrations.py +++ b/novaclient/tests/functional/hooks/check_resources.py @@ -10,15 +10,21 @@ # License for the specific language governing permissions and limitations # under the License. -""" -migration interface -""" +from novaclient.tests.functional import base -from novaclient.v2 import contrib -from novaclient.v2 import migrations +class ResourceChecker(base.ClientTestBase): -Migration = migrations.Migration -MigrationManager = migrations.MigrationManager + def runTest(self): + pass -contrib.warn() + def check(self): + self.setUp() + + print("$ nova list --all-tenants") + print(self.nova("list", params="--all-tenants")) + print("\n") + + +if __name__ == "__main__": + ResourceChecker().check() diff --git a/novaclient/tests/functional/hooks/post_test_hook.sh b/novaclient/tests/functional/hooks/post_test_hook.sh deleted file mode 100755 index 766352933..000000000 --- a/novaclient/tests/functional/hooks/post_test_hook.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -xe - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# This script is executed inside post_test_hook function in devstack gate. - -function generate_testr_results { - if [ -f .testrepository/0 ]; then - sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit - sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit - sudo /usr/os-testr-env/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html - sudo gzip -9 $BASE/logs/testrepository.subunit - sudo gzip -9 $BASE/logs/testr_results.html - sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz - sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz - fi -} - -export NOVACLIENT_DIR="$BASE/new/python-novaclient" - -sudo chown -R jenkins:stack $NOVACLIENT_DIR - -# Go to the novaclient dir -cd $NOVACLIENT_DIR - -# Run tests -echo "Running novaclient functional test suite" -set +e -# Preserve env for OS_ credentials -sudo -E -H -u jenkins tox -e ${TOX_ENV:-functional} -EXIT_CODE=$? -set -e - -# Collect and parse result -generate_testr_results -exit $EXIT_CODE diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py index 9f645c334..77c3503ee 100644 --- a/novaclient/tests/functional/test_auth.py +++ b/novaclient/tests/functional/test_auth.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -from six.moves.urllib import parse +import os +from urllib import parse + import tempest.lib.cli.base from novaclient import client @@ -27,14 +29,19 @@ def _get_url(self, identity_api_version): url.fragment)) def nova_auth_with_password(self, action, identity_api_version): - flags = ('--os-username %s --os-tenant-name %s --os-password %s ' - '--os-auth-url %s --os-endpoint-type publicURL' % ( - self.cli_clients.username, - self.cli_clients.tenant_name, - self.cli_clients.password, - self._get_url(identity_api_version))) + flags = ( + f'--os-username {self.cli_clients.username} ' + f'--os-tenant-name {self.cli_clients.tenant_name} ' + f'--os-password {self.cli_clients.password} ' + f'--os-auth-url {self._get_url(identity_api_version)} ' + f'--os-endpoint-type publicURL' + ) + if self.cacert: + flags = f'{flags} --os-cacert {self.cacert}' + if self.cert: + flags = f'{flags} --os-cert {self.cert}' if self.cli_clients.insecure: - flags += ' --insecure ' + flags = f'{flags} --insecure' return tempest.lib.cli.base.execute( "nova", action, flags, cli_dir=self.cli_clients.cli_dir) @@ -48,35 +55,41 @@ def nova_auth_with_token(self, identity_api_version): if identity_api_version == "3": kw["project_domain_id"] = self.project_domain_id nova = client.Client("2", auth_token=token, auth_url=auth_url, - project_name=self.project_name, **kw) + project_name=self.project_name, + cacert=self.cacert, cert=self.cert, + **kw) nova.servers.list() - # NOTE(andreykurilin): token auth is completely broken in CLI - # flags = ('--os-username %s --os-tenant-name %s --os-auth-token %s ' - # '--os-auth-url %s --os-endpoint-type publicURL' % ( - # self.cli_clients.username, - # self.cli_clients.tenant_name, - # token, auth_url)) - # if self.cli_clients.insecure: - # flags += ' --insecure ' - # - # return tempest.lib.cli.base.execute( - # "nova", action, flags, cli_dir=self.cli_clients.cli_dir) + # NOTE(andreykurilin): tempest.lib.cli.base.execute doesn't allow to + # pass 'env' argument to subprocess.Popen for overriding the current + # process' environment. + # When one of OS_AUTH_TYPE or OS_AUTH_PLUGIN environment variables + # presents, keystoneauth1 can load the wrong auth plugin with wrong + # expected cli arguments. To avoid this case, we need to modify + # current environment. + # TODO(andreykurilin): tempest.lib.cli.base.execute is quite simple + # method that can be replaced by subprocess.check_output direct call + # with passing env argument to avoid modifying the current process + # environment. or we probably can propose a change to tempest. + os.environ.pop("OS_AUTH_TYPE", None) + os.environ.pop("OS_AUTH_PLUGIN", None) - def test_auth_via_keystone_v2(self): - session = self.keystone.session - version = (2, 0) - if not base.is_keystone_version_available(session, version): - self.skipTest("Identity API version 2.0 is not available.") + flags = ( + f'--os-tenant-name {self.project_name} ' + f'--os-token {token} ' + f'--os-auth-url {auth_url} ' + f'--os-endpoint-type publicURL' + ) + if self.cacert: + flags = f'{flags} --os-cacert {self.cacert}' + if self.cert: + flags = f'{flags} --os-cert {self.cert}' + if self.cli_clients.insecure: + flags = f'{flags} --insecure' - self.nova_auth_with_password("list", identity_api_version="2.0") - self.nova_auth_with_token(identity_api_version="2.0") + tempest.lib.cli.base.execute( + "nova", "list", flags, cli_dir=self.cli_clients.cli_dir) def test_auth_via_keystone_v3(self): - session = self.keystone.session - version = (3, 0) - if not base.is_keystone_version_available(session, version): - self.skipTest("Identity API version 3.0 is not available.") - self.nova_auth_with_password("list", identity_api_version="3") self.nova_auth_with_token(identity_api_version="3") diff --git a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py index 06f4008ee..2b2378485 100644 --- a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py +++ b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import json +from oslo_serialization import jsonutils from novaclient.tests.functional import base @@ -22,8 +22,8 @@ class TestExtAttrNovaClient(base.ClientTestBase): def _create_server_and_attach_volume(self): server = self._create_server() - volume = self.cinder.volumes.create(1) - self.addCleanup(volume.delete) + volume = self.openstack.block_storage.create_volume(size=1) + self.addCleanup(self.openstack.block_storage.delete_volume, volume) self.wait_for_volume_status(volume, 'available') self.nova('volume-attach', params="%s %s" % (server.name, volume.id)) self.addCleanup(self._release_volume, server, volume) @@ -50,4 +50,4 @@ def test_extended_server_attributes(self): volume_attr = self._get_value_from_the_table( table, 'os-extended-volumes:volumes_attached') # Check that 'id' exists as a key of volume_attr dict - self.assertIn('id', json.loads(volume_attr)[0]) + self.assertIn('id', jsonutils.loads(volume_attr)[0]) diff --git a/novaclient/tests/functional/v2/legacy/test_flavor_access.py b/novaclient/tests/functional/v2/legacy/test_flavor_access.py index 54671b286..aa8bb415c 100644 --- a/novaclient/tests/functional/v2/legacy/test_flavor_access.py +++ b/novaclient/tests/functional/v2/legacy/test_flavor_access.py @@ -13,7 +13,7 @@ from novaclient.tests.functional import base -class TestFlvAccessNovaClient(base.TenantTestBase): +class TestFlvAccessNovaClient(base.ProjectTestBase): """Functional tests for flavors with public and non-public access""" COMPUTE_API_VERSION = "2.1" @@ -29,7 +29,7 @@ def test_non_public_flavor_list(self): # Check that non-public flavor appears in flavor list # only for admin tenant and only with --all attribute # and doesn't appear for non-admin tenant - flv_name = self.name_generate(prefix='flv') + flv_name = self.name_generate() self.nova('flavor-create --is-public false %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) flavor_list1 = self.nova('flavor-list') @@ -42,7 +42,7 @@ def test_non_public_flavor_list(self): def test_add_access_non_public_flavor(self): # Check that it's allowed to grant an access to non-public flavor for # the given tenant - flv_name = self.name_generate(prefix='flv') + flv_name = self.name_generate() self.nova('flavor-create --is-public false %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) self.nova('flavor-access-add', params="%s %s" % @@ -55,7 +55,7 @@ def test_add_access_public_flavor(self): # successfully for public flavor, but the next operation, # 'flavor-access-list --flavor %(name_of_public_flavor)' returns # a CommandError - flv_name = self.name_generate(prefix='flv') + flv_name = self.name_generate() self.nova('flavor-create %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) self.nova('flavor-access-add %s %s' % (flv_name, self.project_id)) diff --git a/novaclient/tests/functional/v2/legacy/test_hypervisors.py b/novaclient/tests/functional/v2/legacy/test_hypervisors.py index ecc102dc0..621401f9b 100644 --- a/novaclient/tests/functional/v2/legacy/test_hypervisors.py +++ b/novaclient/tests/functional/v2/legacy/test_hypervisors.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six - from novaclient.tests.functional import base from novaclient import utils @@ -41,4 +39,4 @@ def _test_list(self, cpu_info_type, uuid_as_id=False): 'Expected hypervisor.service.id to be an integer.') def test_list(self): - self._test_list(six.text_type) + self._test_list(str) diff --git a/novaclient/tests/functional/v2/legacy/test_instances.py b/novaclient/tests/functional/v2/legacy/test_instances.py index 3ff07b44b..f933ec1e7 100644 --- a/novaclient/tests/functional/v2/legacy/test_instances.py +++ b/novaclient/tests/functional/v2/legacy/test_instances.py @@ -37,7 +37,7 @@ def test_attach_volume(self): destroy. """ - name = self.name_generate('Instance') + name = self.name_generate() # Boot via the cli, as we're primarily testing the cli in this test self.nova('boot', @@ -53,8 +53,8 @@ def test_attach_volume(self): self.addCleanup(server.delete) # create a volume for attachment - volume = self.cinder.volumes.create(1) - self.addCleanup(volume.delete) + volume = self.openstack.block_storage.create_volume(size=1) + self.addCleanup(self.openstack.block_storage.delete_volume, volume) # allow volume to become available self.wait_for_volume_status(volume, 'available') diff --git a/novaclient/tests/functional/v2/legacy/test_keypairs.py b/novaclient/tests/functional/v2/legacy/test_keypairs.py index 73e77b9cf..1e04781df 100644 --- a/novaclient/tests/functional/v2/legacy/test_keypairs.py +++ b/novaclient/tests/functional/v2/legacy/test_keypairs.py @@ -12,7 +12,6 @@ import tempfile -from oslo_utils import uuidutils from tempest.lib import exceptions from novaclient.tests.functional import base @@ -36,7 +35,7 @@ def _create_keypair(self, **kwargs): return key_name def _raw_create_keypair(self, **kwargs): - key_name = 'keypair-' + uuidutils.generate_uuid() + key_name = self.name_generate() kwargs_str = self._serialize_kwargs(kwargs) self.nova('keypair-add %s %s' % (kwargs_str, key_name)) return key_name diff --git a/novaclient/tests/functional/v2/legacy/test_os_services.py b/novaclient/tests/functional/v2/legacy/test_os_services.py index 8af71611d..92d426518 100644 --- a/novaclient/tests/functional/v2/legacy/test_os_services.py +++ b/novaclient/tests/functional/v2/legacy/test_os_services.py @@ -28,7 +28,7 @@ def test_os_service_disable_enable(self): # services returned by client # NOTE(sdague): service disable has the chance in racing # with other tests. Now functional tests for novaclient are launched - # in serial way (https://review.openstack.org/#/c/217768/), but + # in serial way (https://review.opendev.org/#/c/217768/), but # it's a potential issue for making these tests parallel in the future for serv in self.client.services.list(): # In Pike the os-services API was made multi-cell aware and it @@ -41,13 +41,12 @@ def test_os_service_disable_enable(self): continue host = self._get_column_value_from_single_row_table( self.nova('service-list --binary %s' % serv.binary), 'Host') - service = self.nova('service-disable %s %s' % (host, serv.binary)) - self.addCleanup(self.nova, 'service-enable', - params="%s %s" % (host, serv.binary)) + service = self.nova('service-disable %s' % host) + self.addCleanup(self.nova, 'service-enable', params=host) status = self._get_column_value_from_single_row_table( service, 'Status') self.assertEqual('disabled', status) - service = self.nova('service-enable %s %s' % (host, serv.binary)) + service = self.nova('service-enable %s' % host) status = self._get_column_value_from_single_row_table( service, 'Status') self.assertEqual('enabled', status) @@ -64,10 +63,9 @@ def test_os_service_disable_log_reason(self): continue host = self._get_column_value_from_single_row_table( self.nova('service-list --binary %s' % serv.binary), 'Host') - service = self.nova('service-disable --reason test_disable %s %s' - % (host, serv.binary)) - self.addCleanup(self.nova, 'service-enable', - params="%s %s" % (host, serv.binary)) + service = self.nova( + 'service-disable --reason test_disable %s' % host) + self.addCleanup(self.nova, 'service-enable', params=host) status = self._get_column_value_from_single_row_table( service, 'Status') log_reason = self._get_column_value_from_single_row_table( diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py index bf1ebe4c0..7dffba414 100644 --- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -38,7 +38,7 @@ def test_admin_aggregate_list(self): def test_admin_availability_zone_list(self): self.assertIn("internal", self.nova('availability-zone-list')) - def test_admin_flavor_acces_list(self): + def test_admin_flavor_access_list(self): self.assertRaises(exceptions.CommandFailed, self.nova, 'flavor-access-list') @@ -49,10 +49,7 @@ def test_admin_flavor_acces_list(self): params='--flavor m1.tiny') def test_admin_flavor_list(self): - self.assertIn("Memory_MB", self.nova('flavor-list')) - - def test_admin_host_list(self): - self.nova('host-list') + self.assertIn("Memory_MiB", self.nova('flavor-list')) def test_admin_hypervisor_list(self): self.nova('hypervisor-list') @@ -76,7 +73,7 @@ def test_admin_list(self): def test_admin_server_group_list(self): self.nova('server-group-list') - def test_admin_servce_list(self): + def test_admin_service_list(self): self.nova('service-list') def test_admin_usage(self): @@ -88,12 +85,25 @@ def test_admin_usage_list(self): def test_admin_help(self): self.nova('help') - def test_admin_list_extensions(self): - self.nova('list-extensions') - def test_agent_list(self): - self.nova('agent-list') - self.nova('agent-list', flags='--debug') + ex = self.assertRaises(exceptions.CommandFailed, + self.nova, 'agent-list') + self.assertIn( + "This resource is no longer available. " + "No forwarding address is given. (HTTP 410)", str(ex)) + self.assertIn( + "This command has been deprecated since 23.0.0 Wallaby Release " + "and will be removed in the first major release " + "after the Nova server 24.0.0 X release.", str(ex.stderr)) + ex = self.assertRaises(exceptions.CommandFailed, + self.nova, 'agent-list', flags='--debug') + self.assertIn( + "This resource is no longer available. " + "No forwarding address is given. (HTTP 410)", str(ex)) + self.assertIn( + "This command has been deprecated since 23.0.0 Wallaby Release " + "and will be removed in the first major release " + "after the Nova server 24.0.0 X release.", str(ex.stderr)) def test_migration_list(self): self.nova('migration-list') @@ -127,4 +137,4 @@ def test_admin_invalid_bypass_url(self): self.assertRaises(exceptions.CommandFailed, self.nova, 'list', - flags='--endpoint-override badurl') + flags='--os-endpoint-override badurl') diff --git a/novaclient/tests/functional/v2/legacy/test_server_groups.py b/novaclient/tests/functional/v2/legacy/test_server_groups.py index 5518ea979..a1ab151b4 100644 --- a/novaclient/tests/functional/v2/legacy/test_server_groups.py +++ b/novaclient/tests/functional/v2/legacy/test_server_groups.py @@ -11,8 +11,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_utils import uuidutils - from novaclient.tests.functional import base @@ -22,7 +20,7 @@ class TestServerGroupClient(base.ClientTestBase): COMPUTE_API_VERSION = "2.1" def _create_sg(self, policy): - sg_name = 'server_group-' + uuidutils.generate_uuid() + sg_name = self.name_generate() output = self.nova('server-group-create %s %s' % (sg_name, policy)) sg_id = self._get_column_value_from_single_row_table(output, "Id") return sg_id diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index c0852a1bf..ded7eb3a5 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -13,7 +13,6 @@ import datetime from oslo_utils import timeutils -from oslo_utils import uuidutils from novaclient.tests.functional import base @@ -25,10 +24,9 @@ class TestServersBootNovaClient(base.ClientTestBase): def _boot_server_with_legacy_bdm(self, bdm_params=()): volume_size = 1 - volume_name = uuidutils.generate_uuid() - volume = self.cinder.volumes.create(size=volume_size, - name=volume_name, - imageRef=self.image.id) + volume_name = self.name_generate() + volume = self.openstack.block_storage.create_volume( + size=volume_size, name=volume_name, image_id=self.image.id) self.wait_for_volume_status(volume, "available") if (len(bdm_params) >= 3 and bdm_params[2] == '1'): @@ -43,7 +41,7 @@ def _boot_server_with_legacy_bdm(self, bdm_params=()): params = ( "%(name)s --flavor %(flavor)s --poll " "--block-device-mapping vda=%(volume_id)s%(bdm_params)s" % { - "name": uuidutils.generate_uuid(), "flavor": + "name": self.name_generate(), "flavor": self.flavor.id, "volume_id": volume.id, "bdm_params": bdm_params}) @@ -57,12 +55,12 @@ def _boot_server_with_legacy_bdm(self, bdm_params=()): self.wait_for_resource_delete(server_id, self.client.servers) if delete_volume: - self.cinder.volumes.delete(volume.id) - self.wait_for_resource_delete(volume.id, self.cinder.volumes) + self.openstack.block_storage.delete_volume(volume) + self.openstack.block_storage.wait_for_delete(volume) def test_boot_server_with_legacy_bdm(self): # bdm v1 format - # ::: + # ::: # params = (type, size, delete-on-terminate) params = ('', '', '1') self._boot_server_with_legacy_bdm(bdm_params=params) @@ -73,7 +71,7 @@ def test_boot_server_with_legacy_bdm_volume_id_only(self): def test_boot_server_with_net_name(self): server_info = self.nova("boot", params=( "%(name)s --flavor %(flavor)s --image %(image)s --poll " - "--nic net-name=%(net-name)s" % {"name": uuidutils.generate_uuid(), + "--nic net-name=%(net-name)s" % {"name": self.name_generate(), "image": self.image.id, "flavor": self.flavor.id, "net-name": self.network.name})) @@ -90,11 +88,15 @@ def test_boot_server_using_image_with(self): 3. Create a second server using the --image-with option using the meta key stored in the snapshot image created in step 2. """ - # create the first server and wait for it to be active - server_info = self.nova('boot', params=( + params = ( '--flavor %(flavor)s --image %(image)s --poll ' 'image-with-server-1' % {'image': self.image.id, - 'flavor': self.flavor.id})) + 'flavor': self.flavor.id}) + # check to see if we have to pass in a network id + if self.multiple_networks: + params += ' --nic net-id=%s' % self.network.id + # create the first server and wait for it to be active + server_info = self.nova('boot', params=params) server_id = self._get_value_from_the_table(server_info, 'id') self.addCleanup(self._cleanup_server, server_id) @@ -108,18 +110,22 @@ def test_boot_server_using_image_with(self): # get the snapshot image id out of the output table for the second # server create request snapshot_id = self._get_value_from_the_table(snapshot_info, 'id') - self.addCleanup(self.glance.images.delete, snapshot_id) + self.addCleanup(self.openstack.image.delete_image, snapshot_id) # verify the metadata was set on the snapshot image meta_value = self._get_value_from_the_table( snapshot_info, 'image_with_meta') self.assertEqual(server_id, meta_value) - # create the second server using --image-with - server_info = self.nova('boot', params=( + params = ( '--flavor %(flavor)s --image-with image_with_meta=%(meta_value)s ' '--poll image-with-server-2' % {'meta_value': server_id, - 'flavor': self.flavor.id})) + 'flavor': self.flavor.id}) + # check to see if we have to pass in a network id + if self.multiple_networks: + params += ' --nic net-id=%s' % self.network.id + # create the second server using --image-with + server_info = self.nova('boot', params=params) server_id = self._get_value_from_the_table(server_info, 'id') self.addCleanup(self._cleanup_server, server_id) @@ -133,7 +139,7 @@ def _create_servers(self, name, number): return [self._create_server(name) for i in range(number)] def test_list_with_limit(self): - name = uuidutils.generate_uuid() + name = self.name_generate() self._create_servers(name, 2) output = self.nova("list", params="--limit 1 --name %s" % name) # Cut header and footer of the table @@ -142,7 +148,7 @@ def test_list_with_limit(self): def test_list_with_changes_since(self): now = datetime.datetime.isoformat(timeutils.utcnow()) - name = uuidutils.generate_uuid() + name = self.name_generate() self._create_servers(name, 1) output = self.nova("list", params="--changes-since %s" % now) self.assertIn(name, output, output) @@ -151,7 +157,7 @@ def test_list_with_changes_since(self): self.assertNotIn(name, output, output) def test_list_all_servers(self): - name = uuidutils.generate_uuid() + name = self.name_generate() precreated_servers = self._create_servers(name, 3) # there are no possibility to exceed the limit on API side, so just # check that "-1" limit processes by novaclient side @@ -161,13 +167,12 @@ def test_list_all_servers(self): self.assertIn(server.id, output) def test_list_minimal(self): - name = uuidutils.generate_uuid() - uuid = self._create_server(name).id + server = self._create_server() server_output = self.nova("list --minimal") # The only fields output are "ID" and "Name" output_uuid = self._get_column_value_from_single_row_table( server_output, 'ID') output_name = self._get_column_value_from_single_row_table( server_output, 'Name') - self.assertEqual(output_uuid, uuid) - self.assertEqual(output_name, name) + self.assertEqual(output_uuid, server.id) + self.assertEqual(output_name, server.name) diff --git a/novaclient/tests/functional/v2/legacy/test_usage.py b/novaclient/tests/functional/v2/legacy/test_usage.py index e37693d1b..11080c135 100644 --- a/novaclient/tests/functional/v2/legacy/test_usage.py +++ b/novaclient/tests/functional/v2/legacy/test_usage.py @@ -34,13 +34,13 @@ def _get_num_servers_by_tenant_from_usage_output(self): def test_usage(self): before = self._get_num_servers_from_usage_output() - self._create_server('some-server') + self._create_server() after = self._get_num_servers_from_usage_output() self.assertGreater(after, before) def test_usage_tenant(self): before = self._get_num_servers_by_tenant_from_usage_output() - self._create_server('some-server') + self._create_server() after = self._get_num_servers_by_tenant_from_usage_output() self.assertGreater(after, before) @@ -51,8 +51,8 @@ class TestUsageClient(base.ClientTestBase): def _create_servers_in_time_window(self): start = datetime.datetime.now() - self._create_server('some-server') - self._create_server('another-server') + self._create_server() + self._create_server() end = datetime.datetime.now() return start, end diff --git a/novaclient/tests/functional/v2/test_aggregates.py b/novaclient/tests/functional/v2/test_aggregates.py index b89e35664..f3f5f032f 100644 --- a/novaclient/tests/functional/v2/test_aggregates.py +++ b/novaclient/tests/functional/v2/test_aggregates.py @@ -11,8 +11,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_utils import uuidutils - from novaclient.tests.functional import base @@ -21,8 +19,8 @@ class TestAggregatesNovaClient(base.ClientTestBase): def setUp(self): super(TestAggregatesNovaClient, self).setUp() - self.agg1 = 'agg-%s' % uuidutils.generate_uuid() - self.agg2 = 'agg-%s' % uuidutils.generate_uuid() + self.agg1 = self.name_generate() + self.agg2 = self.name_generate() self.addCleanup(self._clean_aggregates) def _clean_aggregates(self): diff --git a/novaclient/tests/functional/v2/test_device_tagging.py b/novaclient/tests/functional/v2/test_device_tagging.py index c19c42403..5909137ea 100644 --- a/novaclient/tests/functional/v2/test_device_tagging.py +++ b/novaclient/tests/functional/v2/test_device_tagging.py @@ -12,8 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_utils import uuidutils -import six from tempest.lib import exceptions from novaclient.tests.functional import base @@ -33,7 +31,7 @@ def test_boot_server_with_tagged_block_devices_with_error(self): '--nic net-id=%(net-uuid)s ' '--block-device ' 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' - 'shutdown=remove,tag=bar' % {'name': uuidutils.generate_uuid(), + 'shutdown=remove,tag=bar' % {'name': self.name_generate(), 'flavor': self.flavor.id, 'net-uuid': self.network.id, 'image': self.image.id})) @@ -41,7 +39,7 @@ def test_boot_server_with_tagged_block_devices_with_error(self): self.assertIn("ERROR (CommandError): " "'tag' in block device mapping is not supported " "in API version %s." % self.COMPUTE_API_VERSION, - six.text_type(e)) + str(e)) else: server_id = self._get_value_from_the_table(output, 'id') self.client.servers.delete(server_id) @@ -63,12 +61,12 @@ def test_boot_server_with_tagged_nic_devices_with_error(self): '--nic net-id=%(net-uuid)s,tag=foo ' '--block-device ' 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' - 'shutdown=remove' % {'name': uuidutils.generate_uuid(), + 'shutdown=remove' % {'name': self.name_generate(), 'flavor': self.flavor.id, 'net-uuid': self.network.id, 'image': self.image.id})) except exceptions.CommandFailed as e: - self.assertIn("Invalid nic argument", six.text_type(e)) + self.assertIn('Invalid nic argument', str(e)) else: server_id = self._get_value_from_the_table(output, 'id') self.client.servers.delete(server_id) @@ -90,7 +88,7 @@ def test_boot_server_with_tagged_block_devices(self): '--nic net-id=%(net-uuid)s ' '--block-device ' 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' - 'shutdown=remove,tag=bar' % {'name': uuidutils.generate_uuid(), + 'shutdown=remove,tag=bar' % {'name': self.name_generate(), 'flavor': self.flavor.id, 'net-uuid': self.network.id, 'image': self.image.id})) @@ -112,7 +110,7 @@ def test_boot_server_with_tagged_nic_devices(self): '--nic net-id=%(net-uuid)s,tag=foo ' '--block-device ' 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' - 'shutdown=remove' % {'name': uuidutils.generate_uuid(), + 'shutdown=remove' % {'name': self.name_generate(), 'flavor': self.flavor.id, 'net-uuid': self.network.id, 'image': self.image.id})) diff --git a/novaclient/tests/functional/v2/test_extended_attributes.py b/novaclient/tests/functional/v2/test_extended_attributes.py index 6fddcc957..bf06875c6 100644 --- a/novaclient/tests/functional/v2/test_extended_attributes.py +++ b/novaclient/tests/functional/v2/test_extended_attributes.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. -import json +from oslo_serialization import jsonutils from novaclient.tests.functional.v2.legacy import test_extended_attributes @@ -32,8 +32,7 @@ def test_extended_server_attributes(self): 'OS-EXT-SRV-ATTR:ramdisk_id', 'OS-EXT-SRV-ATTR:kernel_id', 'OS-EXT-SRV-ATTR:hostname', - 'OS-EXT-SRV-ATTR:root_device_name', - 'OS-EXT-SRV-ATTR:user_data']: + 'OS-EXT-SRV-ATTR:root_device_name']: self._get_value_from_the_table(table, attr) # Check that attribute given below also exists in 'nova show' table # as a key (first column) of table dict @@ -41,4 +40,4 @@ def test_extended_server_attributes(self): table, 'os-extended-volumes:volumes_attached') # Check that 'delete_on_termination' exists as a key # of volume_attr dict - self.assertIn('delete_on_termination', json.loads(volume_attr)[0]) + self.assertIn('delete_on_termination', jsonutils.loads(volume_attr)[0]) diff --git a/novaclient/tests/functional/v2/test_flavor.py b/novaclient/tests/functional/v2/test_flavor.py new file mode 100644 index 000000000..cc25ad7fc --- /dev/null +++ b/novaclient/tests/functional/v2/test_flavor.py @@ -0,0 +1,72 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.functional import base + + +class TestFlavorNovaClientV274(base.ClientTestBase): + """Functional tests for flavors""" + + COMPUTE_API_VERSION = "2.74" + # NOTE(gmann): Before microversion 2.75, default value of 'swap' field is + # returned as empty string. + SWAP_DEFAULT = "" + + def _create_flavor(self, swap=None): + flv_name = self.name_generate() + cmd = 'flavor-create %s auto 512 1 1' + if swap: + cmd = cmd + (' --swap %s' % swap) + out = self.nova(cmd % flv_name) + self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) + return out, flv_name + + def test_create_flavor_with_no_swap(self): + out, _ = self._create_flavor() + self.assertEqual( + self.SWAP_DEFAULT, + self._get_column_value_from_single_row_table(out, "Swap")) + + def test_update_flavor_with_no_swap(self): + _, flv_name = self._create_flavor() + out = self.nova('flavor-update %s new-description' % flv_name) + self.assertEqual( + self.SWAP_DEFAULT, + self._get_column_value_from_single_row_table(out, "Swap")) + + def test_show_flavor_with_no_swap(self): + _, flv_name = self._create_flavor() + out = self.nova('flavor-show %s' % flv_name) + self.assertEqual(self.SWAP_DEFAULT, + self._get_value_from_the_table(out, "swap")) + + def test_list_flavor_with_no_swap(self): + self._create_flavor() + out = self.nova('flavor-list') + self.assertEqual( + self.SWAP_DEFAULT, + self._get_column_value_from_single_row_table(out, "Swap")) + + def test_create_flavor_with_swap(self): + out, _ = self._create_flavor(swap=10) + self.assertEqual( + '10', + self._get_column_value_from_single_row_table(out, "Swap")) + + +class TestFlavorNovaClientV275(TestFlavorNovaClientV274): + """Functional tests for flavors""" + + COMPUTE_API_VERSION = "2.75" + # NOTE(gmann): Since microversion 2.75, default value of 'swap' field is + # returned as 0. + SWAP_DEFAULT = '0' diff --git a/novaclient/tests/functional/v2/test_flavor_access.py b/novaclient/tests/functional/v2/test_flavor_access.py index 229aa3fc4..23ef030ec 100644 --- a/novaclient/tests/functional/v2/test_flavor_access.py +++ b/novaclient/tests/functional/v2/test_flavor_access.py @@ -22,7 +22,7 @@ class TestFlvAccessNovaClientV27(test_flavor_access.TestFlvAccessNovaClient): COMPUTE_API_VERSION = "2.7" def test_add_access_public_flavor(self): - flv_name = self.name_generate('v' + self.COMPUTE_API_VERSION) + flv_name = self.name_generate() self.nova('flavor-create %s auto 512 1 1' % flv_name) self.addCleanup(self.nova, 'flavor-delete %s' % flv_name) output = self.nova('flavor-access-add %s %s' % diff --git a/novaclient/tests/functional/v2/test_hypervisors.py b/novaclient/tests/functional/v2/test_hypervisors.py index ca66a44a2..bb58648b9 100644 --- a/novaclient/tests/functional/v2/test_hypervisors.py +++ b/novaclient/tests/functional/v2/test_hypervisors.py @@ -26,3 +26,16 @@ class TestHypervisorsV2_53(TestHypervisorsV28): def test_list(self): self._test_list(cpu_info_type=dict, uuid_as_id=True) + + def test_search_with_details(self): + # First find a hypervisor from the list to search on. + hypervisors = self.client.hypervisors.list() + # Now search for that hypervisor with details. + hypervisor = hypervisors[0] + hypervisors = self.client.hypervisors.search( + hypervisor.hypervisor_hostname, detailed=True) + self.assertEqual(1, len(hypervisors)) + hypervisor = hypervisors[0] + # We know we got details if service is in the response. + self.assertIsNotNone(hypervisor.service, + 'Expected service in hypervisor: %s' % hypervisor) diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index 728dbb0bc..1578d1012 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -10,8 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import time + +from oslo_utils import timeutils from oslo_utils import uuidutils -import six from tempest.lib import exceptions from novaclient.tests.functional import base @@ -25,7 +27,7 @@ def _test_cmd_with_not_existing_instance(self, cmd, args): try: self.nova("%s %s" % (cmd, args)) except exceptions.CommandFailed as e: - self.assertIn("ERROR (NotFound):", six.text_type(e)) + self.assertIn("ERROR (NotFound):", str(e)) else: self.fail("%s is not failed on non existing instance." % cmd) @@ -57,3 +59,157 @@ def test_show_and_list_actions_on_deleted_instance(self): # ensure that obtained action is "create". self.assertEqual("create", self._get_value_from_the_table(output, "action")) + + +class TestInstanceActionCLIV258(TestInstanceActionCLI): + """Instance action functional tests for v2.58 nova-api microversion.""" + + COMPUTE_API_VERSION = "2.58" + # Does this microversion return a hostId field in the event response? + expect_event_hostId_field = False + + def test_list_instance_action_with_marker_and_limit(self): + server = self._create_server() + server.stop() + # The actions are sorted by created_at in descending order, + # and now we have two actions: create and stop. + output = self.nova("instance-action-list %s --limit 1" % server.id) + marker_req = self._get_column_value_from_single_row_table( + output, "Request_ID") + action = self._get_list_of_values_from_single_column_table( + output, "Action") + # The stop action was most recently created so it's what + # we get back when limit=1. + self.assertEqual(action, ['stop']) + + output = self.nova("instance-action-list %s --limit 1 " + "--marker %s" % (server.id, marker_req)) + action = self._get_list_of_values_from_single_column_table( + output, "Action") + self.assertEqual(action, ['create']) + if not self.expect_event_hostId_field: + # Make sure host and hostId are not in the response when + # microversion is less than 2.62. + output = self.nova("instance-action %s %s" % ( + server.id, marker_req)) + self.assertNotIn("'host'", output) + self.assertNotIn("'hostId'", output) + + def test_list_instance_action_with_changes_since(self): + # Ignore microseconds to make this a deterministic test. + before_create = timeutils.utcnow().replace(microsecond=0).isoformat() + server = self._create_server() + time.sleep(2) + before_stop = timeutils.utcnow().replace(microsecond=0).isoformat() + server.stop() + + create_output = self.nova( + "instance-action-list %s --changes-since %s" % + (server.id, before_create)) + action = self._get_list_of_values_from_single_column_table( + create_output, "Action") + # The actions are sorted by created_at in descending order. + self.assertEqual(action, ['create', 'stop']) + + stop_output = self.nova("instance-action-list %s --changes-since %s" % + (server.id, before_stop)) + action = self._get_list_of_values_from_single_column_table( + stop_output, "Action") + # Provide detailed debug information if this fails. + self.assertEqual(action, ['stop'], + 'Expected to find the stop action with ' + '--changes-since=%s but got: %s\n\n' + 'First instance-action-list output: %s' % + (before_stop, stop_output, create_output)) + + +class TestInstanceActionCLIV262(TestInstanceActionCLIV258, + base.ProjectTestBase): + """Instance action functional tests for v2.62 nova-api microversion.""" + + COMPUTE_API_VERSION = "2.62" + expect_event_hostId_field = True + + def test_show_actions_with_host(self): + name = self.name_generate() + # Create server with non-admin user + server = self.another_nova('boot --flavor %s --image %s --poll %s' % + (self.flavor.name, self.image.name, name)) + server_id = self._get_value_from_the_table(server, 'id') + self.addCleanup(self.client.servers.delete, server_id) + + output = self.nova("instance-action-list %s" % server_id) + request_id = self._get_column_value_from_single_row_table( + output, "Request_ID") + + # Only the 'hostId' are exposed to non-admin + output = self.another_nova( + "instance-action %s %s" % (server_id, request_id)) + self.assertNotIn("'host'", output) + self.assertIn("'hostId'", output) + + # The 'host' and 'hostId' are exposed to admin + output = self.nova("instance-action %s %s" % (server_id, request_id)) + self.assertIn("'host'", output) + self.assertIn("'hostId'", output) + + +class TestInstanceActionCLIV266(TestInstanceActionCLIV258, + base.ProjectTestBase): + """Instance action functional tests for v2.66 nova-api microversion.""" + + COMPUTE_API_VERSION = "2.66" + expect_event_hostId_field = True + + def _wait_for_instance_actions(self, server, expected_num_of_actions): + start_time = time.time() + # Time out after 60 seconds + while time.time() - start_time < 60: + actions = self.client.instance_action.list(server) + if len(actions) == expected_num_of_actions: + break + # Sleep 1 second + time.sleep(1) + else: + self.fail("The number of instance actions for server %s " + "was not %d after 60 s" % + (server.id, expected_num_of_actions)) + # NOTE(takashin): In some DBMSs (e.g. MySQL 5.7), fractions + # (millisecond and microsecond) of DateTime column is not stored + # by default. So sleep an extra second. + time.sleep(1) + # Return time + return timeutils.utcnow().isoformat() + + def test_list_instance_action_with_changes_before(self): + server = self._create_server() + end_create = self._wait_for_instance_actions(server, 1) + # NOTE(takashin): In some DBMSs (e.g. MySQL 5.7), fractions + # (millisecond and microsecond) of DateTime column is not stored + # by default. So sleep a second. + time.sleep(1) + server.stop() + end_stop = self._wait_for_instance_actions(server, 2) + + stop_output = self.nova( + "instance-action-list %s --changes-before %s" % + (server.id, end_stop)) + action = self._get_list_of_values_from_single_column_table( + stop_output, "Action") + # The actions are sorted by created_at in descending order. + self.assertEqual(['create', 'stop'], action, + 'Expected to find the create and stop actions with ' + '--changes-before=%s but got: %s\n\n' % + (end_stop, stop_output)) + + create_output = self.nova( + "instance-action-list %s --changes-before %s" % + (server.id, end_create)) + action = self._get_list_of_values_from_single_column_table( + create_output, "Action") + # Provide detailed debug information if this fails. + self.assertEqual(['create'], action, + 'Expected to find the create action with ' + '--changes-before=%s but got: %s\n\n' + 'First instance-action-list output: %s' % + (end_create, create_output, stop_output)) diff --git a/novaclient/tests/functional/v2/test_instance_usage_audit_log.py b/novaclient/tests/functional/v2/test_instance_usage_audit_log.py new file mode 100644 index 000000000..9a57710cf --- /dev/null +++ b/novaclient/tests/functional/v2/test_instance_usage_audit_log.py @@ -0,0 +1,87 @@ +# 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 datetime + +from oslo_utils import timeutils + +from novaclient.tests.functional import base + + +class TestInstanceUsageAuditLogCLI(base.ClientTestBase): + COMPUTE_API_VERSION = '2.1' + + # NOTE(takashin): By default, 'instance_usage_audit' is False in nova. + # So the instance usage audit log is not recorded. + # Therefore an empty result can be got. + # But it is tested here to call APIs and get responses normally. + + @staticmethod + def _get_begin_end_time(): + current = timeutils.utcnow() + + end = datetime.datetime(day=1, month=current.month, year=current.year) + year = end.year + + if current.month == 1: + year -= 1 + month = 12 + else: + month = current.month - 1 + + begin = datetime.datetime(day=1, month=month, year=year) + + return (begin, end) + + def test_get_os_instance_usage_audit_log(self): + (begin, end) = self._get_begin_end_time() + expected = { + 'hosts_not_run': '[]', + 'log': '{}', + 'num_hosts': '0', + 'num_hosts_done': '0', + 'num_hosts_not_run': '0', + 'num_hosts_running': '0', + 'overall_status': 'ALL hosts done. 0 errors.', + 'total_errors': '0', + 'total_instances': '0', + 'period_beginning': str(begin), + 'period_ending': str(end) + } + + output = self.nova('instance-usage-audit-log') + + for key in expected.keys(): + self.assertEqual(expected[key], + self._get_value_from_the_table(output, key)) + + def test_get_os_instance_usage_audit_log_with_before(self): + expected = { + 'hosts_not_run': '[]', + 'log': '{}', + 'num_hosts': '0', + 'num_hosts_done': '0', + 'num_hosts_not_run': '0', + 'num_hosts_running': '0', + 'overall_status': 'ALL hosts done. 0 errors.', + 'total_errors': '0', + 'total_instances': '0', + 'period_beginning': '2016-11-01 00:00:00', + 'period_ending': '2016-12-01 00:00:00' + } + + output = self.nova( + 'instance-usage-audit-log --before "2016-12-10 13:59:59.999999"') + + for key in expected.keys(): + self.assertEqual(expected[key], + self._get_value_from_the_table(output, key)) diff --git a/novaclient/tests/functional/v2/test_keypairs.py b/novaclient/tests/functional/v2/test_keypairs.py index dba132c51..14133be12 100644 --- a/novaclient/tests/functional/v2/test_keypairs.py +++ b/novaclient/tests/functional/v2/test_keypairs.py @@ -44,13 +44,13 @@ def test_import_keypair_x509(self): self.assertIn('x509', keypair) -class TestKeypairsNovaClientV210(base.TenantTestBase): +class TestKeypairsNovaClientV210(base.ProjectTestBase): """Keypairs functional tests for v2.10 nova-api microversion.""" COMPUTE_API_VERSION = "2.10" def test_create_and_list_keypair(self): - name = self.name_generate("v2_10") + name = self.name_generate() self.nova("keypair-add %s --user %s" % (name, self.user_id)) self.addCleanup(self.another_nova, "keypair-delete %s" % name) output = self.nova("keypair-list") @@ -71,7 +71,7 @@ def test_create_and_list_keypair(self): self._get_value_from_the_table(output_1, "user_id")) def test_create_and_delete(self): - name = self.name_generate("v2_10") + name = self.name_generate() def cleanup(): # We should check keypair existence and remove it from correct user @@ -93,7 +93,7 @@ def cleanup(): self._get_column_value_from_single_row_table, output, "Name") -class TestKeypairsNovaClientV235(base.TenantTestBase): +class TestKeypairsNovaClientV235(base.ProjectTestBase): """Keypairs functional tests for v2.35 nova-api microversion.""" COMPUTE_API_VERSION = "2.35" @@ -101,7 +101,7 @@ class TestKeypairsNovaClientV235(base.TenantTestBase): def test_create_and_list_keypair_with_marker_and_limit(self): names = [] for i in range(3): - names.append(self.name_generate("v2_35")) + names.append(self.name_generate()) self.nova("keypair-add %s --user %s" % (names[i], self.user_id)) self.addCleanup(self.another_nova, "keypair-delete %s" % names[i]) diff --git a/novaclient/tests/functional/v2/test_migrations.py b/novaclient/tests/functional/v2/test_migrations.py new file mode 100644 index 000000000..34a036955 --- /dev/null +++ b/novaclient/tests/functional/v2/test_migrations.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. + +from oslo_utils import uuidutils + +from novaclient.tests.functional import base + + +class TestMigrationList(base.ClientTestBase): + """Tests the "nova migration-list" command.""" + + def _filter_migrations( + self, version, migration_type, source_compute): + """ + Filters migrations by --migration-type and --source-compute. + + :param version: The --os-compute-api-version to use. + :param migration_type: The type of migrations to filter. + :param source_compute: The source compute service hostname to filter. + :return: output of the nova migration-list command with filters applied + """ + return self.nova('migration-list', + flags='--os-compute-api-version %s' % version, + params='--migration-type %s --source-compute %s' % ( + migration_type, source_compute)) + + def test_migration_list(self): + """Tests creating a server, resizing it and then listing and filtering + migrations using various microversion milestones. + """ + server_id = self._create_server(flavor=self.flavor.id).id + # Find the source compute by getting OS-EXT-SRV-ATTR:host from the + # nova show output. + server = self.nova('show', params='%s' % server_id) + server_user_id = self._get_value_from_the_table(server, 'user_id') + tenant_id = self._get_value_from_the_table(server, 'tenant_id') + source_compute = self._get_value_from_the_table( + server, 'OS-EXT-SRV-ATTR:host') + # now resize up + alternate_flavor = self._pick_alternate_flavor() + self.nova('resize', + params='%s %s --poll' % (server_id, alternate_flavor)) + # now confirm the resize + self.nova('resize-confirm', params='%s' % server_id) + # wait for the server to be active and then check the migration list + self._wait_for_state_change(server_id, 'active') + # First, list migrations with v2.1 and our server id should be in the + # output. There should only be the one migration. + migrations = self.nova('migration-list', + flags='--os-compute-api-version 2.1') + instance_uuid = self._get_column_value_from_single_row_table( + migrations, 'Instance UUID') + self.assertEqual(server_id, instance_uuid) + # A successfully confirmed resize should have the migration status + # of "confirmed". + migration_status = self._get_column_value_from_single_row_table( + migrations, 'Status') + self.assertEqual('confirmed', migration_status) + # Now listing migrations with 2.23 should give us the Type column which + # should have a value of "resize". + migrations = self.nova('migration-list', + flags='--os-compute-api-version 2.23') + migration_type = self._get_column_value_from_single_row_table( + migrations, 'Type') + self.assertEqual('resize', migration_type) + # Filter migrations with v2.1. + migrations = self._filter_migrations('2.1', 'resize', source_compute) + # Make sure we got something back. + src_compute = self._get_column_value_from_single_row_table( + migrations, 'Source Compute') + self.assertEqual(source_compute, src_compute) + # Filter migrations with v2.59 and make sure there is a migration UUID + # value in the output. + migrations = self._filter_migrations('2.59', 'resize', source_compute) + # _get_column_value_from_single_row_table will raise ValueError if a + # value is not found for the given column. We don't actually care what + # the migration UUID value is just that the filter works and the UUID + # is shown. + self._get_column_value_from_single_row_table(migrations, 'UUID') + # Filter migrations with v2.66, same as 2.59. + migrations = self._filter_migrations('2.66', 'resize', source_compute) + self._get_column_value_from_single_row_table(migrations, 'UUID') + # Now do a negative test to show that filtering on a migration type + # that we don't have a migration for will not return anything. + migrations = self._filter_migrations( + '2.1', 'evacuation', source_compute) + self.assertNotIn(server_id, migrations) + # Similarly, make sure we don't get anything back when filtering on + # a --source-compute that doesn't exist. + migrations = self._filter_migrations( + '2.66', 'resize', uuidutils.generate_uuid()) + self.assertNotIn(server_id, migrations) + + # Listing migrations with v2.80 and make sure there are the User ID + # and Project ID values in the output. + migrations = self.nova('migration-list', + flags='--os-compute-api-version 2.80') + user_id = self._get_column_value_from_single_row_table( + migrations, 'User ID') + self.assertEqual(server_user_id, user_id) + project_id = self._get_column_value_from_single_row_table( + migrations, 'Project ID') + self.assertEqual(tenant_id, project_id) diff --git a/novaclient/tests/functional/v2/test_os_services.py b/novaclient/tests/functional/v2/test_os_services.py index 1dba1ad01..8550bba6c 100644 --- a/novaclient/tests/functional/v2/test_os_services.py +++ b/novaclient/tests/functional/v2/test_os_services.py @@ -38,15 +38,13 @@ def test_os_services_force_down_force_up(self): host = self._get_column_value_from_single_row_table(service_list, 'Host') - service = self.nova('service-force-down %s %s' - % (host, serv.binary)) + service = self.nova('service-force-down %s' % host) self.addCleanup(self.nova, 'service-force-down --unset', - params="%s %s" % (host, serv.binary)) + params=host) status = self._get_column_value_from_single_row_table( service, 'Forced down') self.assertEqual('True', status) - service = self.nova('service-force-down --unset %s %s' - % (host, serv.binary)) + service = self.nova('service-force-down --unset %s' % host) status = self._get_column_value_from_single_row_table( service, 'Forced down') self.assertEqual('False', status) @@ -74,7 +72,7 @@ def test_os_service_disable_enable(self): # services returned by client # NOTE(sdague): service disable has the chance in racing # with other tests. Now functional tests for novaclient are launched - # in serial way (https://review.openstack.org/#/c/217768/), but + # in serial way (https://review.opendev.org/#/c/217768/), but # it's a potential issue for making these tests parallel in the future for serv in self.client.services.list(): # In Pike the os-services API was made multi-cell aware and it diff --git a/novaclient/tests/functional/v2/test_quota_classes.py b/novaclient/tests/functional/v2/test_quota_classes.py index 399f53968..837759f7a 100644 --- a/novaclient/tests/functional/v2/test_quota_classes.py +++ b/novaclient/tests/functional/v2/test_quota_classes.py @@ -44,7 +44,7 @@ def _get_quota_class_name(self): """Returns a fake quota class name specific to this test class.""" return 'fake-class-%s' % self.COMPUTE_API_VERSION.replace('.', '-') - def _verify_qouta_class_show_output(self, output, expected_values): + def _verify_quota_class_show_output(self, output, expected_values): # Assert that the expected key/value pairs are in the output table for quota_name in self._included_resources: # First make sure the resource is actually in expected quota. @@ -74,7 +74,7 @@ def test_quota_class_show(self): } output = self.nova('quota-class-show %s' % self._get_quota_class_name()) - self._verify_qouta_class_show_output(output, default_values) + self._verify_quota_class_show_output(output, default_values) def test_quota_class_update(self): """Tests updating a fake quota class. The way this works in the API @@ -96,7 +96,7 @@ def test_quota_class_update(self): self.nova("quota-class-update", params=" ".join(params)) # Assert the results using the quota-class-show output. output = self.nova('quota-class-show %s' % class_name) - self._verify_qouta_class_show_output(output, expected_values) + self._verify_quota_class_show_output(output, expected_values) # Assert that attempting to update resources that are blocked will # result in a failure. @@ -115,7 +115,7 @@ class TestQuotasNovaClient2_50(TestQuotaClassesNovaClient): # The 2.50 microversion added the server_groups and server_group_members # to the response, and filtered out floating_ips, fixed_ips, # security_groups and security_group_members, similar to the 2.36 - # microversion in the os-qouta-sets API. + # microversion in the os-quota-sets API. _included_resources = ['instances', 'cores', 'ram', 'metadata_items', 'injected_files', 'injected_file_content_bytes', 'injected_file_path_bytes', 'key_pairs', diff --git a/novaclient/tests/functional/v2/test_quotas.py b/novaclient/tests/functional/v2/test_quotas.py index 1d4e6af31..effddf8a6 100644 --- a/novaclient/tests/functional/v2/test_quotas.py +++ b/novaclient/tests/functional/v2/test_quotas.py @@ -52,7 +52,7 @@ def test_quotas_update(self): class TestQuotasNovaClient2_36(TestQuotasNovaClient2_35): """Nova quotas functional tests.""" - COMPUTE_API_VERSION = "2.latest" + COMPUTE_API_VERSION = "2.36" # The 2.36 microversion stops proxying network quota resources like # floating/fixed IPs and security groups/rules. @@ -61,3 +61,14 @@ class TestQuotasNovaClient2_36(TestQuotasNovaClient2_35): 'injected_file_content_bytes', 'injected_file_path_bytes', 'key_pairs', 'server_groups', 'server_group_members'] + + +class TestQuotasNovaClient2_57(TestQuotasNovaClient2_35): + """Nova quotas functional tests.""" + + COMPUTE_API_VERSION = "2.latest" + + # The 2.57 microversion deprecates injected_file* quotas. + _quota_resources = ['instances', 'cores', 'ram', + 'metadata_items', 'key_pairs', + 'server_groups', 'server_group_members'] diff --git a/novaclient/tests/functional/v2/test_resize.py b/novaclient/tests/functional/v2/test_resize.py index 9277480ea..075a2f9a7 100644 --- a/novaclient/tests/functional/v2/test_resize.py +++ b/novaclient/tests/functional/v2/test_resize.py @@ -20,44 +20,6 @@ class TestServersResize(base.ClientTestBase): COMPUTE_API_VERSION = '2.1' - def _create_server(self, name, flavor): - """Boots a server with the given name and flavor and waits for it to - be ACTIVE. - """ - params = ( - "%(name)s --flavor %(flavor)s --image %(image)s --poll " % { - "name": self.name_generate(name), - "flavor": flavor, - "image": self.image.id}) - # check to see if we have to pass in a network id - if self.multiple_networks: - params += ' --nic net-id=%s' % self.network.id - server_info = self.nova("boot", params=params) - server_id = self._get_value_from_the_table(server_info, "id") - self.addCleanup(self._cleanup_server, server_id) - return server_id - - def _pick_alternate_flavor(self): - """Given the flavor picked in the base class setup, this finds the - opposite flavor to use for a resize test. For example, if m1.nano is - the flavor, then use m1.micro, but those are only available if Tempest - is configured. If m1.tiny, then use m1.small. - """ - flavor_name = self.flavor.name - if flavor_name == 'm1.nano': - # This is an upsize test. - return 'm1.micro' - if flavor_name == 'm1.micro': - # This is a downsize test. - return 'm1.nano' - if flavor_name == 'm1.tiny': - # This is an upsize test. - return 'm1.small' - if flavor_name == 'm1.small': - # This is a downsize test. - return 'm1.tiny' - self.fail('Unable to find alternate for flavor: %s' % flavor_name) - def _compare_quota_usage(self, old_usage, new_usage, expect_diff=True): """Compares the quota usage in the provided AbsoluteLimits.""" # For a resize, instance usage shouldn't change. @@ -83,7 +45,7 @@ def test_resize_up_confirm(self): """Tests creating a server and resizes up and confirms the resize. Compares quota before, during and after the resize. """ - server_id = self._create_server('resize-up-confirm', self.flavor.name) + server_id = self._create_server(flavor=self.flavor.id).id # get the starting quota now that we've created a server starting_usage = self._get_absolute_limits() # now resize up @@ -107,17 +69,20 @@ def _create_resize_down_flavors(self): """Creates two flavors with different size ram but same size vcpus and disk. - :returns: tuple of (larger_flavor_name, smaller_flavor_name) + :returns: tuple of 2 IDs which represents larger_flavor for resize and + smaller flavor. """ - self.nova('flavor-create', params='resize-larger-flavor auto 128 0 1') - self.addCleanup( - self.nova, 'flavor-delete', params='resize-larger-flavor') + output = self.nova('flavor-create', + params='%s auto 128 0 1' % self.name_generate()) + larger_id = self._get_column_value_from_single_row_table(output, "ID") + self.addCleanup(self.nova, 'flavor-delete', params=larger_id) - self.nova('flavor-create', params='resize-smaller-flavor auto 64 0 1') - self.addCleanup( - self.nova, 'flavor-delete', params='resize-smaller-flavor') + output = self.nova('flavor-create', + params='%s auto 64 0 1' % self.name_generate()) + smaller_id = self._get_column_value_from_single_row_table(output, "ID") + self.addCleanup(self.nova, 'flavor-delete', params=smaller_id) - return 'resize-larger-flavor', 'resize-smaller-flavor' + return larger_id, smaller_id def test_resize_down_revert(self): """Tests creating a server and resizes down and reverts the resize. @@ -128,7 +93,7 @@ def test_resize_down_revert(self): # create our own flavors. larger_flavor, smaller_flavor = self._create_resize_down_flavors() # Now create the server with the larger flavor. - server_id = self._create_server('resize-down-revert', larger_flavor) + server_id = self._create_server(flavor=larger_flavor).id # get the starting quota now that we've created a server starting_usage = self._get_absolute_limits() # now resize down diff --git a/novaclient/tests/functional/v2/test_server_groups.py b/novaclient/tests/functional/v2/test_server_groups.py index b2ad1262e..37ebaa66d 100644 --- a/novaclient/tests/functional/v2/test_server_groups.py +++ b/novaclient/tests/functional/v2/test_server_groups.py @@ -17,7 +17,9 @@ class TestServerGroupClientV213(test_server_groups.TestServerGroupClient): """Server groups v2.13 functional tests.""" - COMPUTE_API_VERSION = "2.latest" + COMPUTE_API_VERSION = "2.13" + expected_metadata = True + expected_policy_rules = False def test_create_server_group(self): sg_id = self._create_sg("affinity") @@ -29,6 +31,11 @@ def test_create_server_group(self): self._get_column_value_from_single_row_table( sg, "Project Id") self.assertEqual(sg_id, result) + self._get_column_value_from_single_row_table(sg, "Metadata") + self.assertIn( + 'affinity', + self._get_column_value_from_single_row_table(sg, 'Policies')) + self.assertNotIn('Rules', sg) def test_list_server_groups(self): sg_id = self._create_sg("affinity") @@ -40,6 +47,22 @@ def test_list_server_groups(self): self._get_column_value_from_single_row_table( sg, "Project Id") self.assertEqual(sg_id, result) + if self.expected_metadata: + self._get_column_value_from_single_row_table(sg, "Metadata") + else: + self.assertNotIn(sg, 'Metadata') + if self.expected_policy_rules: + self.assertEqual( + 'affinity', + self._get_column_value_from_single_row_table(sg, "Policy")) + self.assertEqual( + '{}', + self._get_column_value_from_single_row_table(sg, "Rules")) + else: + self.assertIn( + 'affinity', + self._get_column_value_from_single_row_table(sg, 'Policies')) + self.assertNotIn('Rules', sg) def test_get_server_group(self): sg_id = self._create_sg("affinity") @@ -51,3 +74,47 @@ def test_get_server_group(self): self._get_column_value_from_single_row_table( sg, "Project Id") self.assertEqual(sg_id, result) + if self.expected_metadata: + self._get_column_value_from_single_row_table(sg, "Metadata") + else: + self.assertNotIn(sg, 'Metadata') + if self.expected_policy_rules: + self.assertEqual( + 'affinity', + self._get_column_value_from_single_row_table(sg, "Policy")) + self.assertEqual( + '{}', + self._get_column_value_from_single_row_table(sg, "Rules")) + else: + self.assertIn( + 'affinity', + self._get_column_value_from_single_row_table(sg, 'Policies')) + self.assertNotIn('Rules', sg) + + +class TestServerGroupClientV264(TestServerGroupClientV213): + """Server groups v2.64 functional tests.""" + + COMPUTE_API_VERSION = "2.64" + expected_metadata = False + expected_policy_rules = True + + def test_create_server_group(self): + output = self.nova('server-group-create complex-anti-affinity-group ' + 'anti-affinity --rule max_server_per_host=3') + sg_id = self._get_column_value_from_single_row_table(output, "Id") + self.addCleanup(self.nova, 'server-group-delete %s' % sg_id) + sg = self.nova('server-group-get %s' % sg_id) + result = self._get_column_value_from_single_row_table(sg, "Id") + self.assertEqual(sg_id, result) + self._get_column_value_from_single_row_table( + sg, "User Id") + self._get_column_value_from_single_row_table( + sg, "Project Id") + self.assertNotIn('Metadata', sg) + self.assertEqual( + 'anti-affinity', + self._get_column_value_from_single_row_table(sg, "Policy")) + self.assertIn( + 'max_server_per_host', + self._get_column_value_from_single_row_table(sg, "Rules")) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index c2290eb22..3de668dd4 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -130,7 +130,7 @@ def test_update_with_description_longer_than_255_symbols(self): output = self.nova("update %s --description '%s'" % (server.id, descr), fail_ok=True, merge_stderr=True) self.assertIn("ERROR (BadRequest): Invalid input for field/attribute" - " description. Value: %s. u\'%s\' is too long (HTTP 400)" + " description. Value: %s. '%s' is too long (HTTP 400)" % (descr, descr), output) @@ -231,7 +231,7 @@ def test_boot_server_with_auto_network(self): self.skipTest('multiple networks available') server_info = self.nova('boot', params=( '%(name)s --flavor %(flavor)s --poll ' - '--image %(image)s ' % {'name': self.name_generate('server'), + '--image %(image)s ' % {'name': self.name_generate(), 'flavor': self.flavor.id, 'image': self.image.id})) server_id = self._get_value_from_the_table(server_info, 'id') @@ -251,7 +251,7 @@ def test_boot_server_with_no_network(self): server_info = self.nova('boot', params=( '%(name)s --flavor %(flavor)s --poll ' '--image %(image)s --nic none' % - {'name': self.name_generate('server'), + {'name': self.name_generate(), 'flavor': self.flavor.id, 'image': self.image.id})) server_id = self._get_value_from_the_table(server_info, 'id') @@ -288,7 +288,7 @@ def _validate_flavor_details(self, flavor_details, server_details): flavor_details, key) server_flavor_val = self._get_value_from_the_table( server_details, flavor_key_mapping[key]) - if key is "swap" and flavor_val is "": + if key == "swap" and flavor_val == "": # "flavor-show" displays zero swap as empty string. flavor_val = '0' self.assertEqual(flavor_val, server_flavor_val) @@ -326,3 +326,61 @@ def test_list(self): flavor_output = self.nova("flavor-show %s" % self.flavor.id) flavor_val = self._get_value_from_the_table(flavor_output, 'disk') self.assertEqual(flavor_val, server_flavor_val) + + +class TestInterfaceAttach(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.latest' + + def test_interface_attach(self): + server = self._create_server() + output = self.nova("interface-attach --net-id %s %s" % + (self.network.id, server.id)) + + for key in ('ip_address', 'mac_addr', 'port_id', 'port_state'): + self._get_value_from_the_table(output, key) + + self.assertEqual( + self.network.id, + self._get_value_from_the_table(output, 'net_id')) + + +class TestServeRebuildV274(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.74' + REBUILD_FIELDS = ["OS-DCF:diskConfig", "accessIPv4", "accessIPv6", + "adminPass", "created", "description", + "flavor", "hostId", "id", "image", "key_name", + "locked", "locked_reason", "metadata", "name", + "progress", "server_groups", "status", "tags", + "tenant_id", "trusted_image_certificates", "updated", + "user_data", "user_id"] + + def test_rebuild(self): + server = self._create_server() + output = self.nova("rebuild %s %s" % (server.id, self.image.name)) + for field in self.REBUILD_FIELDS: + self.assertIn(field, output) + + +class TestServeRebuildV275(TestServeRebuildV274): + + COMPUTE_API_VERSION = '2.75' + REBUILD_FIELDS_V275 = ['OS-EXT-AZ:availability_zone', 'config_drive', + 'OS-EXT-SRV-ATTR:host', + 'OS-EXT-SRV-ATTR:hypervisor_hostname', + 'OS-EXT-SRV-ATTR:instance_name', + 'OS-EXT-SRV-ATTR:hostname', + 'OS-EXT-SRV-ATTR:kernel_id', + 'OS-EXT-SRV-ATTR:launch_index', + 'OS-EXT-SRV-ATTR:ramdisk_id', + 'OS-EXT-SRV-ATTR:reservation_id', + 'OS-EXT-SRV-ATTR:root_device_name', + 'host_status', + 'OS-SRV-USG:launched_at', + 'OS-SRV-USG:terminated_at', + 'OS-EXT-STS:task_state', 'OS-EXT-STS:vm_state', + 'OS-EXT-STS:power_state', 'security_groups', + 'os-extended-volumes:volumes_attached'] + + REBUILD_FIELDS = TestServeRebuildV274.REBUILD_FIELDS + REBUILD_FIELDS_V275 diff --git a/novaclient/tests/functional/v2/test_trigger_crash_dump.py b/novaclient/tests/functional/v2/test_trigger_crash_dump.py index bf556eec8..19dcbd96e 100644 --- a/novaclient/tests/functional/v2/test_trigger_crash_dump.py +++ b/novaclient/tests/functional/v2/test_trigger_crash_dump.py @@ -19,7 +19,7 @@ @decorators.skip_because(bug="1675526") -class TestTriggerCrashDumpNovaClientV217(base.TenantTestBase): +class TestTriggerCrashDumpNovaClientV217(base.ProjectTestBase): """Functional tests for trigger crash dump""" COMPUTE_API_VERSION = "2.17" @@ -119,7 +119,7 @@ def test_trigger_crash_dump_in_locked_state_admin(self): self._assert_nmi(server.id) def test_trigger_crash_dump_in_locked_state_nonadmin(self): - name = self.name_generate(prefix='server') + name = self.name_generate() server = self.another_nova('boot --flavor %s --image %s --poll %s' % (self.flavor.name, self.image.name, name)) self.addCleanup(self.another_nova, 'delete', params=name) diff --git a/novaclient/tests/unit/fakes.py b/novaclient/tests/unit/fakes.py index e89370bc6..312cca4fb 100644 --- a/novaclient/tests/unit/fakes.py +++ b/novaclient/tests/unit/fakes.py @@ -42,7 +42,7 @@ def assert_has_keys(dict, required=None, optional=None): class FakeClient(object): def assert_called(self, method, url, body=None, pos=-1): - """Assert than an HTTP method was called at given order/position. + """Assert that an HTTP method was called at given order/position. :param method: HTTP method name which is expected to be called :param url: Expected request url to be called with given method @@ -59,10 +59,10 @@ def assert_called(self, method, url, body=None, pos=-1): self.assert_called('GET', '/flavors/aa1/os-extra_specs') 2. self.run_command(["boot", "--image", "1", - "--flavor", "512 MB Server", + "--flavor", "512 MiB Server", "--max-count", "3", "server"]) self.assert_called('GET', '/images/1', pos=0) - self.assert_called('GET', '/flavors/512 MB Server', pos=1) + self.assert_called('GET', '/flavors/512 MiB Server', pos=1) self.assert_called('GET', '/flavors?is_public=None', pos=2) self.assert_called('GET', '/flavors/2', pos=3) self.assert_called( @@ -98,7 +98,7 @@ def assert_called(self, method, url, body=None, pos=-1): (self.client.callstack[pos][2], body)) def assert_called_anytime(self, method, url, body=None): - """Assert than an HTTP method was called anytime in the test. + """Assert that an HTTP method was called anytime in the test. :param method: HTTP method name which is expected to be called :param url: Expected request url to be called with given method @@ -131,6 +131,19 @@ def assert_called_anytime(self, method, url, body=None): self.client.callstack = [] + def assert_not_called(self, method, url, body=None): + """Assert that an HTTP method was not called in the test. + + :param method: HTTP method name which is expected not to be called + :param url: Expected request url not to be called with given method + :param body: Expected request body not to be called with given method + and url. Default is None. + """ + not_expected = (method, url, body) + for entry in self.client.callstack: + assert not_expected != entry[0:3], ( + 'API %s %s body=%s was called.' % not_expected) + def clear_callstack(self): self.client.callstack = [] diff --git a/novaclient/tests/unit/fixture_data/aggregates.py b/novaclient/tests/unit/fixture_data/aggregates.py index 3ea64b8b7..b3ab88c5f 100644 --- a/novaclient/tests/unit/fixture_data/aggregates.py +++ b/novaclient/tests/unit/fixture_data/aggregates.py @@ -51,3 +51,10 @@ def setUp(self): self.requests_mock.delete(self.url(1), status_code=202, headers=self.json_headers) + + self.requests_mock.register_uri('POST', self.url(1), + json={}, + headers=self.json_headers) + self.requests_mock.post(self.url(1, 'images'), + json={}, + headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/base.py b/novaclient/tests/unit/fixture_data/base.py index 6580787a9..e08d82db6 100644 --- a/novaclient/tests/unit/fixture_data/base.py +++ b/novaclient/tests/unit/fixture_data/base.py @@ -10,8 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from urllib import parse + import fixtures -from six.moves.urllib import parse from novaclient.tests.unit.v2 import fakes diff --git a/novaclient/tests/unit/fixture_data/certs.py b/novaclient/tests/unit/fixture_data/certs.py deleted file mode 100644 index 5bc419e56..000000000 --- a/novaclient/tests/unit/fixture_data/certs.py +++ /dev/null @@ -1,55 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.tests.unit.fixture_data import base - - -class Fixture(base.Fixture): - - base_url = 'os-certificates' - - def get_os_certificates_root(self, **kw): - return ( - 200, - {}, - {'certificate': {'private_key': None, 'data': 'foo'}} - ) - - def post_os_certificates(self, **kw): - return ( - 200, - {}, - {'certificate': {'private_key': 'foo', 'data': 'bar'}} - ) - - def setUp(self): - super(Fixture, self).setUp() - - get_os_certificate = { - 'certificate': { - 'private_key': None, - 'data': 'foo' - } - } - self.requests_mock.get(self.url('root'), - json=get_os_certificate, - headers=self.json_headers) - - post_os_certificates = { - 'certificate': { - 'private_key': 'foo', - 'data': 'bar' - } - } - self.requests_mock.post(self.url(), - json=post_os_certificates, - headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/client.py b/novaclient/tests/unit/fixture_data/client.py index d0ce3cc52..d72ed6a35 100644 --- a/novaclient/tests/unit/fixture_data/client.py +++ b/novaclient/tests/unit/fixture_data/client.py @@ -17,7 +17,7 @@ from novaclient import client -IDENTITY_URL = 'http://identityserver:5000/v2.0' +IDENTITY_URL = 'http://identity.host/v3' COMPUTE_URL = 'http://compute.host' diff --git a/novaclient/tests/unit/fixture_data/cloudpipe.py b/novaclient/tests/unit/fixture_data/cloudpipe.py deleted file mode 100644 index b19e24342..000000000 --- a/novaclient/tests/unit/fixture_data/cloudpipe.py +++ /dev/null @@ -1,37 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.tests.unit.fixture_data import base - - -class Fixture(base.Fixture): - - base_url = 'os-cloudpipe' - - def setUp(self): - super(Fixture, self).setUp() - - get_os_cloudpipe = {'cloudpipes': [{'project_id': 1}]} - self.requests_mock.get(self.url(), - json=get_os_cloudpipe, - headers=self.json_headers) - - instance_id = '9d5824aa-20e6-4b9f-b967-76a699fc51fd' - post_os_cloudpipe = {'instance_id': instance_id} - self.requests_mock.post(self.url(), - json=post_os_cloudpipe, - headers=self.json_headers, - status_code=202) - - self.requests_mock.put(self.url('configure-project'), - headers=self.json_headers, - status_code=202) diff --git a/novaclient/tests/unit/fixture_data/hosts.py b/novaclient/tests/unit/fixture_data/hosts.py deleted file mode 100644 index c95cddeba..000000000 --- a/novaclient/tests/unit/fixture_data/hosts.py +++ /dev/null @@ -1,144 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.tests.unit.fixture_data import base - - -class BaseFixture(base.Fixture): - - base_url = 'os-hosts' - - def setUp(self): - super(BaseFixture, self).setUp() - - get_os_hosts_host = { - 'host': [ - {'resource': {'project': '(total)', 'host': 'dummy', - 'cpu': 16, 'memory_mb': 32234, 'disk_gb': 128}}, - {'resource': {'project': '(used_now)', 'host': 'dummy', - 'cpu': 1, 'memory_mb': 2075, 'disk_gb': 45}}, - {'resource': {'project': '(used_max)', 'host': 'dummy', - 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}}, - {'resource': {'project': 'admin', 'host': 'dummy', - 'cpu': 1, 'memory_mb': 2048, 'disk_gb': 30}} - ] - } - - headers = self.json_headers - - self.requests_mock.get(self.url('host'), - json=get_os_hosts_host, - headers=headers) - - def get_os_hosts(request, context): - zone = 'nova1' - service = None - - if request.query: - try: - zone = request.qs['zone'][0] - except Exception: - pass - - try: - service = request.qs['service'][0] - except Exception: - pass - - return { - 'hosts': [ - { - 'host_name': 'host1', - 'service': service or 'nova-compute', - 'zone': zone - }, - { - 'host_name': 'host1', - 'service': service or 'nova-cert', - 'zone': zone - } - ] - } - - self.requests_mock.get(self.url(), - json=get_os_hosts, - headers=headers) - - get_os_hosts_sample_host = { - 'host': [ - {'resource': {'host': 'sample_host'}} - ], - } - self.requests_mock.get(self.url('sample_host'), - json=get_os_hosts_sample_host, - headers=headers) - - self.requests_mock.put(self.url('sample_host', 1), - json=self.put_host_1(), - headers=headers) - - self.requests_mock.put(self.url('sample_host', 2), - json=self.put_host_2(), - headers=headers) - - self.requests_mock.put(self.url('sample_host', 3), - json=self.put_host_3(), - headers=headers) - - self.requests_mock.get(self.url('sample_host', 'reboot'), - json=self.get_host_reboot(), - headers=headers) - - self.requests_mock.get(self.url('sample_host', 'startup'), - json=self.get_host_startup(), - headers=headers) - - self.requests_mock.get(self.url('sample_host', 'shutdown'), - json=self.get_host_shutdown(), - headers=headers) - - def put_os_hosts_sample_host(request, context): - result = {'host': 'dummy'} - result.update(request.json()) - return result - - self.requests_mock.put(self.url('sample_host'), - json=put_os_hosts_sample_host, - headers=headers) - - -class V1(BaseFixture): - - def put_host_1(self): - return {'host': 'sample-host_1', - 'status': 'enabled'} - - def put_host_2(self): - return {'host': 'sample-host_2', - 'maintenance_mode': 'on_maintenance'} - - def put_host_3(self): - return {'host': 'sample-host_3', - 'status': 'enabled', - 'maintenance_mode': 'on_maintenance'} - - def get_host_reboot(self): - return {'host': 'sample_host', - 'power_action': 'reboot'} - - def get_host_startup(self): - return {'host': 'sample_host', - 'power_action': 'startup'} - - def get_host_shutdown(self): - return {'host': 'sample_host', - 'power_action': 'shutdown'} diff --git a/novaclient/tests/unit/fixture_data/hypervisors.py b/novaclient/tests/unit/fixture_data/hypervisors.py index 1e706786b..3c04daa74 100644 --- a/novaclient/tests/unit/fixture_data/hypervisors.py +++ b/novaclient/tests/unit/fixture_data/hypervisors.py @@ -10,6 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +from urllib import parse + +from oslo_utils import encodeutils + from novaclient import api_versions from novaclient.tests.unit.fixture_data import base @@ -23,15 +27,41 @@ class V1(base.Fixture): service_id_1 = 1 service_id_2 = 2 + @staticmethod + def _transform_hypervisor_details(hypervisor): + """Transform a detailed hypervisor view from 2.53 to 2.88.""" + del hypervisor['current_workload'] + del hypervisor['disk_available_least'] + del hypervisor['free_ram_mb'] + del hypervisor['free_disk_gb'] + del hypervisor['local_gb'] + del hypervisor['local_gb_used'] + del hypervisor['memory_mb'] + del hypervisor['memory_mb_used'] + del hypervisor['running_vms'] + del hypervisor['vcpus'] + del hypervisor['vcpus_used'] + hypervisor['uptime'] = 'fake uptime' + def setUp(self): super(V1, self).setUp() - uuid_as_id = (api_versions.APIVersion(self.api_version) >= - api_versions.APIVersion('2.53')) + + api_version = api_versions.APIVersion(self.api_version) get_os_hypervisors = { 'hypervisors': [ - {'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1'}, - {'id': self.hyper_id_2, 'hypervisor_hostname': 'hyper2'}, + { + 'id': self.hyper_id_1, + 'hypervisor_hostname': 'hyper1', + 'state': 'up', + 'status': 'enabled', + }, + { + 'id': self.hyper_id_2, + 'hypervisor_hostname': 'hyper2', + 'state': 'up', + 'status': 'enabled', + }, ] } @@ -63,7 +93,9 @@ def setUp(self): 'current_workload': 2, 'running_vms': 2, 'cpu_info': 'cpu_info', - 'disk_available_least': 100 + 'disk_available_least': 100, + 'state': 'up', + 'status': 'enabled', }, { 'id': self.hyper_id_2, @@ -85,11 +117,17 @@ def setUp(self): 'current_workload': 2, 'running_vms': 2, 'cpu_info': 'cpu_info', - 'disk_available_least': 100 + 'disk_available_least': 100, + 'state': 'up', + 'status': 'enabled', } ] } + if api_version >= api_versions.APIVersion('2.88'): + for hypervisor in get_os_hypervisors_detail['hypervisors']: + self._transform_hypervisor_details(hypervisor) + self.requests_mock.get(self.url('detail'), json=get_os_hypervisors_detail, headers=self.headers) @@ -117,12 +155,22 @@ def setUp(self): get_os_hypervisors_search = { 'hypervisors': [ - {'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1'}, - {'id': self.hyper_id_2, 'hypervisor_hostname': 'hyper2'} + { + 'id': self.hyper_id_1, + 'hypervisor_hostname': 'hyper1', + 'state': 'up', + 'status': 'enabled', + }, + { + 'id': self.hyper_id_2, + 'hypervisor_hostname': 'hyper2', + 'state': 'up', + 'status': 'enabled', + }, ] } - if uuid_as_id: + if api_version >= api_versions.APIVersion('2.53'): url = self.url(hypervisor_hostname_pattern='hyper') else: url = self.url('hyper', 'search') @@ -130,11 +178,38 @@ def setUp(self): json=get_os_hypervisors_search, headers=self.headers) + if api_version >= api_versions.APIVersion('2.53'): + get_os_hypervisors_search_u_v2_53 = { + 'error_name': 'BadRequest', + 'message': 'Invalid input for query parameters ' + 'hypervisor_hostname_pattern.', + 'code': 400} + # hypervisor_hostname_pattern is encoded in the url method + url = self.url(hypervisor_hostname_pattern='\\u5de5\\u4f5c') + self.requests_mock.get(url, + json=get_os_hypervisors_search_u_v2_53, + headers=self.headers, status_code=400) + else: + get_os_hypervisors_search_unicode = { + 'error_name': 'NotFound', + 'message': "No hypervisor matching " + "'\\u5de5\\u4f5c' could be found.", + 'code': 404 + } + hypervisor_hostname_pattern = parse.quote(encodeutils.safe_encode( + '\\u5de5\\u4f5c')) + url = self.url(hypervisor_hostname_pattern, 'search') + self.requests_mock.get(url, + json=get_os_hypervisors_search_unicode, + headers=self.headers, status_code=404) + get_hyper_server = { 'hypervisors': [ { 'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1', + 'state': 'up', + 'status': 'enabled', 'servers': [ {'name': 'inst1', 'uuid': 'uuid1'}, {'name': 'inst2', 'uuid': 'uuid2'} @@ -143,6 +218,8 @@ def setUp(self): { 'id': self.hyper_id_2, 'hypervisor_hostname': 'hyper2', + 'state': 'up', + 'status': 'enabled', 'servers': [ {'name': 'inst3', 'uuid': 'uuid3'}, {'name': 'inst4', 'uuid': 'uuid4'} @@ -151,7 +228,7 @@ def setUp(self): ] } - if uuid_as_id: + if api_version >= api_versions.APIVersion('2.53'): url = self.url(hypervisor_hostname_pattern='hyper', with_servers=True) else: @@ -178,10 +255,16 @@ def setUp(self): 'current_workload': 2, 'running_vms': 2, 'cpu_info': 'cpu_info', - 'disk_available_least': 100 + 'disk_available_least': 100, + 'state': 'up', + 'status': 'enabled', } } + if api_version >= api_versions.APIVersion('2.88'): + self._transform_hypervisor_details( + get_os_hypervisors_hyper1['hypervisor']) + self.requests_mock.get(self.url(self.hyper_id_1), json=get_os_hypervisors_hyper1, headers=self.headers) @@ -190,7 +273,9 @@ def setUp(self): 'hypervisor': { 'id': self.hyper_id_1, 'hypervisor_hostname': 'hyper1', - 'uptime': 'fake uptime' + 'uptime': 'fake uptime', + 'state': 'up', + 'status': 'enabled', } } @@ -199,10 +284,15 @@ def setUp(self): headers=self.headers) -class V2_53(V1): +class V253(V1): """Fixture data for the os-hypervisors 2.53 API.""" api_version = '2.53' hyper_id_1 = 'd480b1b6-2255-43c2-b2c2-d60d42c2c074' hyper_id_2 = '43a8214d-f36a-4fc0-a25c-3cf35c17522d' service_id_1 = 'a87743ff-9c29-42ff-805d-2444659b5fc0' service_id_2 = '0486ab8b-1cfc-4ccb-9d94-9f22ec8bbd6b' + + +class V288(V253): + """Fixture data for the os-hypervisors 2.88 API.""" + api_version = '2.88' diff --git a/novaclient/tests/unit/fixture_data/keypairs.py b/novaclient/tests/unit/fixture_data/keypairs.py index 1d73d2528..5ac1b4ad7 100644 --- a/novaclient/tests/unit/fixture_data/keypairs.py +++ b/novaclient/tests/unit/fixture_data/keypairs.py @@ -10,16 +10,20 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit import fakes from novaclient.tests.unit.fixture_data import base class V1(base.Fixture): + api_version = '2.1' base_url = 'os-keypairs' def setUp(self): super(V1, self).setUp() + api_version = api_versions.APIVersion(self.api_version) + keypair = {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'} headers = self.json_headers @@ -39,7 +43,13 @@ def setUp(self): def post_os_keypairs(request, context): body = request.json() assert list(body) == ['keypair'] - fakes.assert_has_keys(body['keypair'], required=['name']) + if api_version >= api_versions.APIVersion("2.92"): + # In 2.92, public_key becomes mandatory + required = ['name', 'public_key'] + else: + required = ['name'] + fakes.assert_has_keys(body['keypair'], + required=required) return {'keypair': keypair} self.requests_mock.post(self.url(), diff --git a/novaclient/tests/unit/fixture_data/limits.py b/novaclient/tests/unit/fixture_data/limits.py index f9a86ff79..0e7986580 100644 --- a/novaclient/tests/unit/fixture_data/limits.py +++ b/novaclient/tests/unit/fixture_data/limits.py @@ -16,6 +16,13 @@ class Fixture(base.Fixture): base_url = 'limits' + absolute = { + "maxTotalRAMSize": 51200, + "maxServerMeta": 5, + "maxImageMeta": 5, + "maxPersonality": 5, + "maxPersonalitySize": 10240 + } def setUp(self): super(Fixture, self).setUp() @@ -64,13 +71,7 @@ def setUp(self): ] } ], - "absolute": { - "maxTotalRAMSize": 51200, - "maxServerMeta": 5, - "maxImageMeta": 5, - "maxPersonality": 5, - "maxPersonalitySize": 10240 - }, + "absolute": self.absolute, }, } @@ -78,3 +79,13 @@ def setUp(self): self.requests_mock.get(self.url(), json=get_limits, headers=headers) + + +class Fixture2_57(Fixture): + """Fixture data for the 2.57 microversion where personality files are + deprecated. + """ + absolute = { + "maxTotalRAMSize": 51200, + "maxServerMeta": 5 + } diff --git a/novaclient/tests/unit/fixture_data/quotas.py b/novaclient/tests/unit/fixture_data/quotas.py index a6931fcc6..1ffa8c265 100644 --- a/novaclient/tests/unit/fixture_data/quotas.py +++ b/novaclient/tests/unit/fixture_data/quotas.py @@ -52,16 +52,36 @@ def setUp(self): def test_quota(self, tenant_id='test'): return { - 'tenant_id': tenant_id, - 'metadata_items': [], + 'id': tenant_id, + 'metadata_items': 1, 'injected_file_content_bytes': 1, 'injected_file_path_bytes': 1, 'ram': 1, + 'fixed_ips': -1, 'floating_ips': 1, 'instances': 1, 'injected_files': 1, 'cores': 1, - 'keypairs': 1, + 'key_pairs': 1, 'security_groups': 1, - 'security_group_rules': 1 + 'security_group_rules': 1, + 'server_groups': 1, + 'server_group_members': 1 + } + + +class V2_57(V1): + """2.57 fixture data where there are no injected file or network resources + """ + + def test_quota(self, tenant_id='test'): + return { + 'id': tenant_id, + 'metadata_items': 1, + 'ram': 1, + 'instances': 1, + 'cores': 1, + 'key_pairs': 1, + 'server_groups': 1, + 'server_group_members': 1 } diff --git a/novaclient/tests/unit/fixture_data/server_groups.py b/novaclient/tests/unit/fixture_data/server_groups.py index 5c4c88c6f..d2fd43c7d 100644 --- a/novaclient/tests/unit/fixture_data/server_groups.py +++ b/novaclient/tests/unit/fixture_data/server_groups.py @@ -87,8 +87,8 @@ def setUp(self): headers=headers) self.requests_mock.get(self.url(all_projects=True), - json={'server_groups': server_groups - + other_project_server_groups}, + json={'server_groups': server_groups + + other_project_server_groups}, headers=headers) self.requests_mock.get(self.url(limit=2, offset=1), diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 9822fa5b0..1635e33dc 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -43,7 +43,7 @@ def setUp(self): }, "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", "status": "BUILD", @@ -85,7 +85,7 @@ def setUp(self): }, "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", @@ -127,7 +127,7 @@ def setUp(self): "image": "", "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", @@ -205,6 +205,19 @@ def setUp(self): self.requests_mock.delete(self.url(u, 'metadata', 'key1'), json=self.diagnostic, headers=self.json_headers) + metadata3 = {'meta': { + 'Server Label': 'Web Head 1' + }} + self.requests_mock.get(self.url(1234, 'metadata', 'Server Label'), + json=metadata3, + headers=self.json_headers) + metadata4 = {'metadata': { + 'Server Label': 'Web Head 1', + 'Image Version': '2.1' + }} + self.requests_mock.get(self.url(1234, 'metadata'), + json=metadata4, + headers=self.json_headers) get_security_groups = { "security_groups": [{ @@ -271,37 +284,6 @@ def put_servers_1234(request, context): status_code=204, headers=self.json_headers) - def post_os_volumes_boot(request, context): - body = request.json() - assert (set(body.keys()) <= - set(['server', 'os:scheduler_hints'])) - - fakes.assert_has_keys(body['server'], - required=['name', 'flavorRef'], - optional=['imageRef']) - - data = body['server'] - - # Require one, and only one, of the keys for bdm - if 'block_device_mapping' not in data: - if 'block_device_mapping_v2' not in data: - msg = "missing required keys: 'block_device_mapping'" - raise AssertionError(msg) - elif 'block_device_mapping_v2' in data: - msg = "found extra keys: 'block_device_mapping'" - raise AssertionError(msg) - - return {'server': self.server_9012} - - # NOTE(jamielennox): hack to make os_volumes mock go to the right place - base_url = self.base_url - self.base_url = None - self.requests_mock.post(self.url('os-volumes_boot'), - json=post_os_volumes_boot, - status_code=202, - headers=self.json_headers) - self.base_url = base_url - # # Server password # @@ -380,6 +362,10 @@ def setUp(self): self.requests_mock.delete(self.url('1234', 'os-interface', 'port-id'), headers=self.json_headers) + self.requests_mock.get(self.url('1234', 'topology'), + json=v2_fakes.SERVER_TOPOLOGY, + headers=self.json_headers) + # Testing with the following password and key # # Clear password: FooBar123 @@ -461,6 +447,23 @@ def post_servers_1234_action(self, request, context): # but we can not specify version in data_fixture now and this is # V1 data, so just let it pass pass + elif action == 'migrate': + return None + elif action == 'lock': + return None + elif action == 'unshelve': + if api_version >= api_versions.APIVersion("2.91"): + # In 2.91 and above, we allow body to be one of these: + # {'unshelve': None} + # {'unshelve': {'availability_zone': }} + # {'unshelve': {'availability_zone': None}} (Unpin az) + # {'unshelve': {'host': }} + # {'unshelve': {'availability_zone': , 'host': }} + # {'unshelve': {'availability_zone': None, 'host': }} + if body[action] is not None: + for key in body[action].keys(): + key in ['availability_zone', 'host'] + return None elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py index 89d806072..a5c8cb1c2 100644 --- a/novaclient/tests/unit/test_api_versions.py +++ b/novaclient/tests/unit/test_api_versions.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock import novaclient from novaclient import api_versions @@ -343,6 +343,27 @@ def fake_func(): self.assertEqual(expected_name, fake_func.__id__) +class CheckVersionTestCase(utils.TestCase): + def test_version_unsupported(self): + for version in ('1.0', '1.5', '1.100'): + with self.subTest('version too old', version=version): + self.assertRaises( + exceptions.UnsupportedVersion, + api_versions.check_version, + api_versions.APIVersion(version)) + + for version in ('2.97', '2.101', '3.0'): + with self.subTest('version too new', version=version): + self.assertRaises( + exceptions.UnsupportedVersion, + api_versions.check_version, + api_versions.APIVersion(version)) + + for version in ('2.1', '2.57', '2.96'): + with self.subTest('version just right', version=version): + api_versions.check_version(api_versions.APIVersion(version)) + + class DiscoverVersionTestCase(utils.TestCase): def setUp(self): super(DiscoverVersionTestCase, self).setUp() diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py index eb70dff99..634cc93b4 100644 --- a/novaclient/tests/unit/test_base.py +++ b/novaclient/tests/unit/test_base.py @@ -11,8 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -from requests import Response -import six +import requests from novaclient import api_versions from novaclient import base @@ -23,13 +22,13 @@ def create_response_obj_with_header(): - resp = Response() + resp = requests.Response() resp.headers['x-openstack-request-id'] = fakes.FAKE_REQUEST_ID return resp def create_response_obj_with_compute_header(): - resp = Response() + resp = requests.Response() resp.headers['x-compute-request-id'] = fakes.FAKE_REQUEST_ID return resp @@ -49,7 +48,7 @@ class TmpObject(object): def test_resource_lazy_getattr(self): cs = fakes.FakeClient(api_versions.APIVersion("2.0")) f = flavors.Flavor(cs.flavors, {'id': 1}) - self.assertEqual('256 MB Server', f.name) + self.assertEqual('256 MiB Server', f.name) cs.assert_called('GET', '/flavors/1') # Missing stuff still fails after a second get @@ -148,14 +147,3 @@ def test_bytes_with_meta(self): # Check request_ids attribute is added to obj self.assertTrue(hasattr(obj, 'request_ids')) self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids) - - -if six.PY2: - class UnicodeWithMetaTest(utils.TestCase): - def test_unicode_with_meta(self): - resp = create_response_obj_with_header() - obj = base.UnicodeWithMeta(u'test-unicode', resp) - self.assertEqual(u'test-unicode', obj) - # Check request_ids attribute is added to obj - self.assertTrue(hasattr(obj, 'request_ids')) - self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids) diff --git a/novaclient/tests/unit/test_client.py b/novaclient/tests/unit/test_client.py index f5f677608..f0154ab78 100644 --- a/novaclient/tests/unit/test_client.py +++ b/novaclient/tests/unit/test_client.py @@ -14,10 +14,10 @@ # under the License. import copy -import uuid +from unittest import mock from keystoneauth1 import session -import mock +from oslo_utils import uuidutils import novaclient.api_versions import novaclient.client @@ -26,26 +26,6 @@ import novaclient.v2.client -class ClientTest(utils.TestCase): - def test_get_client_class_v2(self): - output = novaclient.client.get_client_class('2') - self.assertEqual(output, novaclient.v2.client.Client) - - def test_get_client_class_v2_int(self): - output = novaclient.client.get_client_class(2) - self.assertEqual(output, novaclient.v2.client.Client) - - def test_get_client_class_unknown(self): - self.assertRaises(novaclient.exceptions.UnsupportedVersion, - novaclient.client.get_client_class, '0') - - def test_get_client_class_latest(self): - self.assertRaises(novaclient.exceptions.UnsupportedVersion, - novaclient.client.get_client_class, 'latest') - self.assertRaises(novaclient.exceptions.UnsupportedVersion, - novaclient.client.get_client_class, '2.latest') - - class SessionClientTest(utils.TestCase): def test_timings(self): @@ -72,7 +52,7 @@ def test_client_get_reset_timings_v2(self): self.assertEqual(0, len(cs.get_timings())) def test_global_id(self): - global_id = "req-%s" % uuid.uuid4() + global_id = "req-%s" % uuidutils.generate_uuid() self.requests_mock.get('http://no.where') client = novaclient.client.SessionClient(session=session.Session(), @@ -109,23 +89,6 @@ def f(*args, **kwargs): mock_discover_via_entry_points.assert_called_once_with() self.assertEqual([mock_extension()] * 4, result) - @mock.patch('novaclient.client.warnings') - @mock.patch("novaclient.client._discover_via_entry_points") - @mock.patch("novaclient.client._discover_via_python_path") - @mock.patch("novaclient.extension.Extension") - def test_discover_extensions_only_contrib( - self, mock_extension, mock_discover_via_python_path, - mock_discover_via_entry_points, mock_warnings): - - version = novaclient.api_versions.APIVersion("2.0") - - self.assertEqual([], novaclient.client.discover_extensions( - version, only_contrib=True)) - self.assertFalse(mock_discover_via_python_path.called) - self.assertFalse(mock_discover_via_entry_points.called) - self.assertFalse(mock_extension.called) - self.assertTrue(mock_warnings.warn.called) - @mock.patch("novaclient.client.warnings") def test__check_arguments(self, mock_warnings): release = "Coolest" @@ -161,3 +124,15 @@ def test__check_arguments(self, mock_warnings): self.assertNotEqual(original_kwargs, actual_kwargs) self.assertEqual({}, actual_kwargs) self.assertTrue(mock_warnings.warn.called) + + +class ClientTest(utils.TestCase): + + def test_logger(self): + client = novaclient.client.Client('2.1', logger=mock.sentinel.logger) + self.assertEqual(mock.sentinel.logger, client.logger) + self.assertEqual(mock.sentinel.logger, client.client.logger) + client = novaclient.client.Client('2.1') + self.assertEqual('novaclient.v2.client', client.logger.name) + self.assertIsNotNone(client.client.logger) + self.assertEqual('novaclient.v2.client', client.client.logger.name) diff --git a/novaclient/tests/unit/test_crypto.py b/novaclient/tests/unit/test_crypto.py new file mode 100644 index 000000000..3b4a80ed8 --- /dev/null +++ b/novaclient/tests/unit/test_crypto.py @@ -0,0 +1,73 @@ +# Copyright 2018 NTT Corporation +# +# 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 base64 +import subprocess +from unittest import mock + +from novaclient import crypto +from novaclient.tests.unit import utils + + +class CryptoTest(utils.TestCase): + + def setUp(self): + super(CryptoTest, self).setUp() + # The password string that passed as the method argument + self.password_string = 'Test Password' + # The return value of Popen.communicate + self.decrypt_password = b'Decrypt Password' + self.private_key = 'Test Private Key' + + @mock.patch('subprocess.Popen') + def test_decrypt_password(self, mock_open): + mocked_proc = mock.Mock() + mock_open.return_value = mocked_proc + mocked_proc.returncode = 0 + mocked_proc.communicate.return_value = (self.decrypt_password, '') + + decrypt_password = crypto.decrypt_password(self.private_key, + self.password_string) + + # The return value is 'str' in both python 2 and python 3 + self.assertIsInstance(decrypt_password, str) + self.assertEqual('Decrypt Password', decrypt_password) + + mock_open.assert_called_once_with( + ['openssl', 'rsautl', '-decrypt', '-inkey', self.private_key], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + mocked_proc.communicate.assert_called_once_with( + base64.b64decode(self.password_string)) + mocked_proc.stdin.close.assert_called_once_with() + + @mock.patch('subprocess.Popen') + def test_decrypt_password_failure(self, mock_open): + mocked_proc = mock.Mock() + mock_open.return_value = mocked_proc + mocked_proc.returncode = 1 # Error case + mocked_proc.communicate.return_value = (self.decrypt_password, '') + + self.assertRaises(crypto.DecryptionFailure, crypto.decrypt_password, + self.private_key, self.password_string) + + mock_open.assert_called_once_with( + ['openssl', 'rsautl', '-decrypt', '-inkey', self.private_key], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + mocked_proc.communicate.assert_called_once_with( + base64.b64decode(self.password_string)) + mocked_proc.stdin.close.assert_called_once_with() diff --git a/novaclient/tests/unit/test_discover.py b/novaclient/tests/unit/test_discover.py index c03c1d0fd..f294e2e81 100644 --- a/novaclient/tests/unit/test_discover.py +++ b/novaclient/tests/unit/test_discover.py @@ -13,11 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. -import imp +import importlib import inspect +from unittest import mock -import mock -import pkg_resources +import stevedore +from stevedore import extension from novaclient import client from novaclient.tests.unit import utils @@ -27,16 +28,21 @@ class DiscoverTest(utils.TestCase): def test_discover_via_entry_points(self): - def mock_iter_entry_points(group): - if group == 'novaclient.extension': - fake_ep = mock.Mock() - fake_ep.name = 'foo' - fake_ep.module = imp.new_module('foo') - fake_ep.load.return_value = fake_ep.module - return [fake_ep] + def mock_mgr(): + fake_ep = mock.Mock() + fake_ep.name = 'foo' + module_spec = importlib.machinery.ModuleSpec('foo', None) + fake_ep.module = importlib.util.module_from_spec(module_spec) + fake_ep.load.return_value = fake_ep.module + fake_ext = extension.Extension( + name='foo', + entry_point=fake_ep, + plugin=fake_ep.module, + obj=None, + ) + return stevedore.ExtensionManager.make_test_instance([fake_ext]) - @mock.patch.object(pkg_resources, 'iter_entry_points', - mock_iter_entry_points) + @mock.patch.object(client, '_make_discovery_manager', mock_mgr) def test(): for name, module in client._discover_via_entry_points(): self.assertEqual('foo', name) @@ -47,10 +53,14 @@ def test(): def test_discover_extensions(self): def mock_discover_via_python_path(): - yield 'foo', imp.new_module('foo') + module_spec = importlib.machinery.ModuleSpec('foo', None) + module = importlib.util.module_from_spec(module_spec) + yield 'foo', module def mock_discover_via_entry_points(): - yield 'baz', imp.new_module('baz') + module_spec = importlib.machinery.ModuleSpec('baz', None) + module = importlib.util.module_from_spec(module_spec) + yield 'baz', module @mock.patch.object(client, '_discover_via_python_path', diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 10de208f3..efad2ac9f 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -12,16 +12,14 @@ # under the License. import argparse -import distutils.version as dist_version +import io import re import sys +from unittest import mock import fixtures from keystoneauth1 import fixture -import mock -import prettytable import requests_mock -import six from testtools import matchers from novaclient import api_versions @@ -34,19 +32,23 @@ FAKE_ENV = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where/v2.0', - 'OS_COMPUTE_API_VERSION': '2'} + 'OS_AUTH_URL': 'http://no.where/v3', + 'OS_COMPUTE_API_VERSION': '2', + 'OS_PROJECT_DOMAIN_ID': 'default', + 'OS_PROJECT_DOMAIN_NAME': 'default', + 'OS_USER_DOMAIN_ID': 'default', + 'OS_USER_DOMAIN_NAME': 'default'} FAKE_ENV2 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', - 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_AUTH_URL': 'http://no.where/v3', 'OS_COMPUTE_API_VERSION': '2'} FAKE_ENV3 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', - 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_AUTH_URL': 'http://no.where/v3', 'NOVA_ENDPOINT_TYPE': 'novaURL', 'OS_ENDPOINT_TYPE': 'osURL', 'OS_COMPUTE_API_VERSION': '2'} @@ -54,7 +56,7 @@ FAKE_ENV4 = {'OS_USER_ID': 'user_id', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', - 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_AUTH_URL': 'http://no.where/v3', 'NOVA_ENDPOINT_TYPE': 'internal', 'OS_ENDPOINT_TYPE': 'osURL', 'OS_COMPUTE_API_VERSION': '2'} @@ -62,7 +64,7 @@ FAKE_ENV5 = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where/v2.0'} + 'OS_AUTH_URL': 'http://no.where/v3'} def _create_ver_list(versions): @@ -207,7 +209,7 @@ def test_init_action_other(self, mock_init): action.assert_called_once_with( 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) def test_get_action_nolookup(self): action_class = mock.Mock() parser = mock.Mock(**{ @@ -225,7 +227,7 @@ def test_get_action_nolookup(self): self.assertFalse(action_class.called) self.assertEqual(sys.stderr.getvalue(), '') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) def test_get_action_lookup_noresult(self): parser = mock.Mock(**{ '_registry_get.return_value': None, @@ -243,7 +245,7 @@ def test_get_action_lookup_noresult(self): 'WARNING: Programming error: Unknown real action ' '"store"\n') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) def test_get_action_lookup_withresult(self): action_class = mock.Mock() parser = mock.Mock(**{ @@ -262,7 +264,7 @@ def test_get_action_lookup_withresult(self): 'option_strings', 'dest', help='Deprecated', const=1) self.assertEqual(sys.stderr.getvalue(), '') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') def test_call_unemitted_nouse(self, mock_get_action): obj = novaclient.shell.DeprecatedAction( @@ -277,7 +279,7 @@ def test_call_unemitted_nouse(self, mock_get_action): self.assertEqual(sys.stderr.getvalue(), 'WARNING: Option "option_string" is deprecated\n') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') def test_call_unemitted_withuse(self, mock_get_action): obj = novaclient.shell.DeprecatedAction( @@ -293,7 +295,7 @@ def test_call_unemitted_withuse(self, mock_get_action): 'WARNING: Option "option_string" is deprecated; ' 'use this instead\n') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') def test_call_emitted_nouse(self, mock_get_action): obj = novaclient.shell.DeprecatedAction( @@ -308,7 +310,7 @@ def test_call_emitted_nouse(self, mock_get_action): 'parser', 'namespace', 'values', 'option_string') self.assertEqual(sys.stderr.getvalue(), '') - @mock.patch.object(sys, 'stderr', six.StringIO()) + @mock.patch.object(sys, 'stderr', io.StringIO()) @mock.patch.object(novaclient.shell.DeprecatedAction, '_get_action') def test_call_emitted_withuse(self, mock_get_action): obj = novaclient.shell.DeprecatedAction( @@ -387,8 +389,8 @@ def shell(self, argstr, exitcodes=(0,)): orig = sys.stdout orig_stderr = sys.stderr try: - sys.stdout = six.StringIO() - sys.stderr = six.StringIO() + sys.stdout = io.StringIO() + sys.stderr = io.StringIO() _shell = novaclient.shell.OpenStackComputeShell() _shell.main(argstr.split()) except SystemExit: @@ -404,10 +406,10 @@ def shell(self, argstr, exitcodes=(0,)): return (stdout, stderr) def register_keystone_discovery_fixture(self, mreq): - v2_url = "http://no.where/v2.0" - v2_version = fixture.V2Discovery(v2_url) + v3_url = "http://no.where/v3" + v3_version = fixture.V3Discovery(v3_url) mreq.register_uri( - 'GET', v2_url, json=_create_ver_list([v2_version]), + 'GET', v3_url, json=_create_ver_list([v3_version]), status_code=200) def test_help_unknown_command(self): @@ -427,7 +429,7 @@ def _test_help(self, command, required=None): if required is None: required = [ '.*?^usage: ', - '.*?^\s+set-password\s+Change the admin password', + '.*?^\\s+set-password\\s+Change the admin password', '.*?^See "nova help COMMAND" for help on a specific command', ] stdout, stderr = self.shell(command) @@ -445,6 +447,9 @@ def test_help_option(self): def test_help_no_options(self): self._test_help('') + def test_help_no_subcommand(self): + self._test_help('--os-compute-api-version 2.87') + def test_help_on_subcommand(self): required = [ '.*?^usage: nova set-password', @@ -467,9 +472,9 @@ def test_bash_completion(self): matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_no_username(self): - required = ('You must provide a username or user ID' - ' via --os-username, --os-user-id,' - ' env[OS_USERNAME] or env[OS_USER_ID]') + required = ('You must provide a user name/id (via --os-username, ' + '--os-user-id, env[OS_USERNAME] or env[OS_USER_ID]) or ' + 'an auth token (via --os-token).') self.make_env(exclude='OS_USERNAME') try: self.shell('list') @@ -479,9 +484,9 @@ def test_no_username(self): self.fail('CommandError not raised') def test_no_user_id(self): - required = ('You must provide a username or user ID' - ' via --os-username, --os-user-id,' - ' env[OS_USERNAME] or env[OS_USER_ID]') + required = ('You must provide a user name/id (via --os-username, ' + '--os-user-id, env[OS_USERNAME] or env[OS_USER_ID]) or ' + 'an auth token (via --os-token).') self.make_env(exclude='OS_USER_ID', fake_env=FAKE_ENV2) try: self.shell('list') @@ -521,6 +526,24 @@ def test_no_auth_url(self): else: self.fail('CommandError not raised') + def test_basic_attributes(self): + for exclude, client_arg, env_var in ( + (None, 'project_domain_id', FAKE_ENV['OS_PROJECT_DOMAIN_ID']), + ('OS_PROJECT_DOMAIN_ID', 'project_domain_id', ''), + (None, 'project_domain_name', FAKE_ENV['OS_PROJECT_DOMAIN_NAME']), + ('OS_PROJECT_DOMAIN_NAME', 'project_domain_name', ''), + (None, 'user_domain_id', FAKE_ENV['OS_USER_DOMAIN_ID']), + ('OS_USER_DOMAIN_ID', 'user_domain_id', ''), + (None, 'user_domain_name', FAKE_ENV['OS_USER_DOMAIN_NAME']), + ('OS_USER_DOMAIN_NAME', 'user_domain_name', '') + ): + with self.subTest(f'{exclude},{client_arg},{env_var}'): + self.mock_client.reset_mock() + self.make_env(exclude=exclude, fake_env=FAKE_ENV) + self.shell('list') + client_kwargs = self.mock_client.call_args_list[0][1] + self.assertEqual(env_var, client_kwargs[client_arg]) + @requests_mock.Mocker() def test_nova_endpoint_type(self, m_requests): self.make_env(fake_env=FAKE_ENV3) @@ -557,39 +580,18 @@ def test_default_endpoint_type(self): def test_password(self, mock_getpass, mock_stdin, m_requests): mock_stdin.encoding = "utf-8" - # default output of empty tables differs depending between prettytable - # versions - if (hasattr(prettytable, '__version__') and - dist_version.StrictVersion(prettytable.__version__) < - dist_version.StrictVersion('0.7.2')): - ex = '\n' - else: - ex = '\n'.join([ - '+----+------+--------+------------+-------------+----------+', - '| ID | Name | Status | Task State | Power State | Networks |', - '+----+------+--------+------------+-------------+----------+', - '+----+------+--------+------------+-------------+----------+', - '' - ]) + ex = '\n'.join([ + '+----+------+--------+------------+-------------+----------+', + '| ID | Name | Status | Task State | Power State | Networks |', + '+----+------+--------+------------+-------------+----------+', + '+----+------+--------+------------+-------------+----------+', + '' + ]) self.make_env(exclude='OS_PASSWORD') self.register_keystone_discovery_fixture(m_requests) stdout, stderr = self.shell('list') self.assertEqual((stdout + stderr), ex) - @mock.patch('sys.stdin', side_effect=mock.MagicMock) - @mock.patch('getpass.getpass', side_effect=EOFError) - def test_no_password(self, mock_getpass, mock_stdin): - required = ('Expecting a password provided' - ' via either --os-password, env[OS_PASSWORD],' - ' or prompted response',) - self.make_env(exclude='OS_PASSWORD') - try: - self.shell('list') - except exceptions.CommandError as message: - self.assertEqual(required, message.args) - else: - self.fail('CommandError not raised') - def _test_service_type(self, version, service_type, mock_client): if version is None: cmd = 'list' @@ -612,26 +614,32 @@ def test_v_unknown_service_type(self): self._test_service_type, 'unknown', 'compute', self.mock_client) - @mock.patch('sys.argv', ['nova']) - @mock.patch('sys.stdout', six.StringIO()) - @mock.patch('sys.stderr', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) + @mock.patch('sys.stderr', io.StringIO()) def test_main_noargs(self): # Ensure that main works with no command-line arguments try: - novaclient.shell.main() + novaclient.shell.main([]) except SystemExit: self.fail('Unexpected SystemExit') # We expect the normal usage as a result - self.assertIn('Command-line interface to the OpenStack Nova API', - sys.stdout.getvalue()) + self.assertIn( + 'Command-line interface to the OpenStack Nova API', + sys.stdout.getvalue(), + ) + # We also expect to see the deprecation warning + self.assertIn( + 'nova CLI is deprecated and will be removed in a future release', + sys.stderr.getvalue(), + ) @mock.patch.object(novaclient.shell.OpenStackComputeShell, 'main') def test_main_keyboard_interrupt(self, mock_compute_shell): # Ensure that exit code is 130 for KeyboardInterrupt mock_compute_shell.side_effect = KeyboardInterrupt() try: - novaclient.shell.main() + novaclient.shell.main([]) except SystemExit as ex: self.assertEqual(ex.code, 130) @@ -666,25 +674,6 @@ def test_osprofiler_not_installed(self, m_requests): self.assertIn('unrecognized arguments: --profile swordfish', stderr) - @mock.patch('novaclient.shell.SecretsHelper.tenant_id', - return_value=True) - @mock.patch('novaclient.shell.SecretsHelper.auth_token', - return_value=True) - @mock.patch('novaclient.shell.SecretsHelper.management_url', - return_value=True) - @requests_mock.Mocker() - def test_keyring_saver_helper(self, - sh_management_url_function, - sh_auth_token_function, - sh_tenant_id_function, - m_requests): - self.make_env(fake_env=FAKE_ENV) - self.register_keystone_discovery_fixture(m_requests) - self.shell('list') - mock_client_instance = self.mock_client.return_value - keyring_saver = mock_client_instance.client.keyring_saver - self.assertIsInstance(keyring_saver, novaclient.shell.SecretsHelper) - def test_microversion_with_default_behaviour(self): self.make_env(fake_env=FAKE_ENV5) self.mock_server_version_range.return_value = ( @@ -771,16 +760,24 @@ def test_microversion_with_specific_version_without_microversions(self): def test_main_error_handling(self, mock_compute_shell): class MyException(Exception): pass - with mock.patch('sys.stderr', six.StringIO()): + with mock.patch('sys.stderr', io.StringIO()): mock_compute_shell.side_effect = MyException('message') - self.assertRaises(SystemExit, novaclient.shell.main) + self.assertRaises(SystemExit, novaclient.shell.main, []) err = sys.stderr.getvalue() - self.assertEqual(err, 'ERROR (MyException): message\n') + # We expect to see the error propagated + self.assertIn('ERROR (MyException): message\n', err) + # We also expect to see the deprecation warning + self.assertIn( + 'nova CLI is deprecated and will be removed in a future release', + err, + ) class TestLoadVersionedActions(utils.TestCase): def test_load_versioned_actions(self): + # first load with API version 2.15, ensuring we use the 2.15 version of + # the underlying function (which returns 1) parser = novaclient.shell.NovaClientArgumentParser() subparsers = parser.add_subparsers(metavar='') shell = novaclient.shell.OpenStackComputeShell() @@ -791,6 +788,12 @@ def test_load_versioned_actions(self): self.assertEqual( 1, shell.subcommands['fake-action'].get_default('func')()) + # now load with API version 2.25, ensuring we now use the + # correspponding version of the underlying function (which now returns + # 2) + parser = novaclient.shell.NovaClientArgumentParser() + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("2.25"), False) @@ -798,10 +801,6 @@ def test_load_versioned_actions(self): self.assertEqual( 2, shell.subcommands['fake-action'].get_default('func')()) - self.assertIn('fake-action2', shell.subcommands.keys()) - self.assertEqual( - 3, shell.subcommands['fake-action2'].get_default('func')()) - def test_load_versioned_actions_not_in_version_range(self): parser = novaclient.shell.NovaClientArgumentParser() subparsers = parser.add_subparsers(metavar='') @@ -912,6 +911,10 @@ def test_load_actions_with_versioned_args(self, mock_add_arg): mock_add_arg.reset_mock() + parser = novaclient.shell.NovaClientArgumentParser(add_help=False) + subparsers = parser.add_subparsers(metavar='') + shell = novaclient.shell.OpenStackComputeShell() + shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("2.21"), False) self.assertNotIn(mock.call('--foo', help="first foo"), diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py index 919d4cb2b..8411f3a40 100644 --- a/novaclient/tests/unit/test_utils.py +++ b/novaclient/tests/unit/test_utils.py @@ -11,18 +11,17 @@ # License for the specific language governing permissions and limitations # under the License. +import io import sys - -import mock -from oslo_utils import encodeutils -import six -from six.moves.urllib import parse +from unittest import mock +from urllib import parse from novaclient import base from novaclient import exceptions from novaclient.tests.unit import fakes from novaclient.tests.unit import utils as test_utils from novaclient import utils +from novaclient.v2 import servers UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' @@ -176,7 +175,7 @@ def __init__(self, name, value): class PrintResultTestCase(test_utils.TestCase): - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_dict(self): dict = {'key': 'value'} utils.print_dict(dict) @@ -187,7 +186,7 @@ def test_print_dict(self): '+----------+-------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_dict_wrap(self): dict = {'key1': 'not wrapped', 'key2': 'this will be wrapped'} @@ -201,7 +200,7 @@ def test_print_dict_wrap(self): '+----------+--------------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_list_sort_by_str(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 2), @@ -218,7 +217,7 @@ def test_print_list_sort_by_str(self): '+------+-------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_list_sort_by_integer(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 2), @@ -235,14 +234,11 @@ def test_print_list_sort_by_integer(self): '+------+-------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_unicode_list(self): - objs = [_FakeResult("k", u'\u2026')] + objs = [_FakeResult("k", '\u2026')] utils.print_list(objs, ["Name", "Value"]) - if six.PY3: - s = u'\u2026' - else: - s = encodeutils.safe_encode(u'\u2026') + s = '\u2026' self.assertEqual('+------+-------+\n' '| Name | Value |\n' '+------+-------+\n' @@ -251,7 +247,7 @@ def test_print_unicode_list(self): sys.stdout.getvalue()) # without sorting - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_list_sort_by_none(self): objs = [_FakeResult("k1", 1), _FakeResult("k3", 3), @@ -268,7 +264,7 @@ def test_print_list_sort_by_none(self): '+------+-------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_dict_dictionary(self): dict = {'k': {'foo': 'bar'}} utils.print_dict(dict) @@ -279,7 +275,7 @@ def test_print_dict_dictionary(self): '+----------+----------------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_dict_list_dictionary(self): dict = {'k': [{'foo': 'bar'}]} utils.print_dict(dict) @@ -290,7 +286,7 @@ def test_print_dict_list_dictionary(self): '+----------+------------------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_dict_list(self): dict = {'k': ['foo', 'bar']} utils.print_dict(dict) @@ -301,7 +297,7 @@ def test_print_dict_list(self): '+----------+----------------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_large_dict_list(self): dict = {'k': ['foo1', 'bar1', 'foo2', 'bar2', 'foo3', 'bar3', 'foo4', 'bar4']} @@ -315,14 +311,11 @@ def test_print_large_dict_list(self): '+----------+------------------------------------------+\n', sys.stdout.getvalue()) - @mock.patch('sys.stdout', six.StringIO()) + @mock.patch('sys.stdout', io.StringIO()) def test_print_unicode_dict(self): - dict = {'k': u'\u2026'} + dict = {'k': '\u2026'} utils.print_dict(dict) - if six.PY3: - s = u'\u2026' - else: - s = encodeutils.safe_encode(u'\u2026') + s = '\u2026' self.assertEqual('+----------+-------+\n' '| Property | Value |\n' '+----------+-------+\n' @@ -380,26 +373,6 @@ def test_validate_flavor_metadata_keys_with_invalid_keys(self): self.assertIn(key, str(ce)) -class ResourceManagerExtraKwargsHookTestCase(test_utils.TestCase): - def test_get_resource_manager_extra_kwargs_hook_test(self): - do_foo = mock.MagicMock() - - def hook1(args): - return {'kwarg1': 'v_hook1'} - - def hook2(args): - return {'kwarg1': 'v_hook2'} - do_foo.resource_manager_kwargs_hooks = [hook1, hook2] - args = {} - exc = self.assertRaises(exceptions.NoUniqueMatch, - utils.get_resource_manager_extra_kwargs, - do_foo, - args) - except_error = ("Hook 'hook2' is attempting to redefine " - "attributes") - self.assertIn(except_error, six.text_type(exc)) - - class DoActionOnManyTestCase(test_utils.TestCase): def _test_do_action_on_many(self, side_effect, fail): @@ -422,6 +395,30 @@ def test_do_action_on_many_first_fails(self): def test_do_action_on_many_last_fails(self): self._test_do_action_on_many([None, Exception()], fail=True) + @mock.patch('sys.stdout', new_callable=io.StringIO) + def _test_do_action_on_many_resource_string( + self, resource, expected_string, mock_stdout): + utils.do_action_on_many(mock.Mock(), [resource], 'success with %s', + 'error') + self.assertIn('success with %s' % expected_string, + mock_stdout.getvalue()) + + def test_do_action_on_many_resource_string_with_str(self): + self._test_do_action_on_many_resource_string('resource1', 'resource1') + + def test_do_action_on_many_resource_string_with_human_id(self): + resource = servers.Server(None, {'name': 'resource1'}) + self._test_do_action_on_many_resource_string(resource, 'resource1') + + def test_do_action_on_many_resource_string_with_id(self): + resource = servers.Server(None, {'id': UUID}) + self._test_do_action_on_many_resource_string(resource, UUID) + + def test_do_action_on_many_resource_string_with_id_and_human_id(self): + resource = servers.Server(None, {'name': 'resource1', 'id': UUID}) + self._test_do_action_on_many_resource_string(resource, + 'resource1 (%s)' % UUID) + class RecordTimeTestCase(test_utils.TestCase): @@ -443,19 +440,30 @@ def test_record_time(self): class PrepareQueryStringTestCase(test_utils.TestCase): - def test_convert_dict_to_string(self): - ustr = b'?\xd0\xbf=1&\xd1\x80=2' - if six.PY3: - # in py3 real unicode symbols will be urlencoded - ustr = ustr.decode('utf8') - cases = ( + + def setUp(self): + super(PrepareQueryStringTestCase, self).setUp() + self.ustr = b'?\xd0\xbf=1&\xd1\x80=2' + # in py3 real unicode symbols will be urlencoded + self.ustr = self.ustr.decode('utf8') + self.cases = ( ({}, ''), + (None, ''), ({'2': 2, '10': 1}, '?10=1&2=2'), ({'abc': 1, 'abc1': 2}, '?abc=1&abc1=2'), - ({b'\xd0\xbf': 1, b'\xd1\x80': 2}, ustr), + ({b'\xd0\xbf': 1, b'\xd1\x80': 2}, self.ustr), ({(1, 2): '1', (3, 4): '2'}, '?(1, 2)=1&(3, 4)=2') ) - for case in cases: + + def test_convert_dict_to_string(self): + for case in self.cases: self.assertEqual( case[1], parse.unquote_plus(utils.prepare_query_string(case[0]))) + + def test_get_url_with_filter(self): + url = '/fake' + for case in self.cases: + self.assertEqual( + '%s%s' % (url, case[1]), + parse.unquote_plus(utils.get_url_with_filter(url, case[0]))) diff --git a/novaclient/tests/unit/utils.py b/novaclient/tests/unit/utils.py index 49d3631f3..c3c77b39f 100644 --- a/novaclient/tests/unit/utils.py +++ b/novaclient/tests/unit/utils.py @@ -12,20 +12,15 @@ # under the License. import os +from unittest import mock import fixtures -import mock from oslo_serialization import jsonutils import requests from requests_mock.contrib import fixture as requests_mock_fixture -import six import testscenarios import testtools -AUTH_URL = "http://localhost:5002/auth_url" -AUTH_URL_V1 = "http://localhost:5002/auth_url/v1.0" -AUTH_URL_V2 = "http://localhost:5002/auth_url/v2.0" - def _patch_mock_to_raise_for_invalid_assert_calls(): def raise_for_invalid_assert_calls(wrapped): @@ -45,6 +40,7 @@ def wrapper(_self, name): mock.Mock.__getattr__ = raise_for_invalid_assert_calls( mock.Mock.__getattr__) + # NOTE(gibi): needs to be called only once at import time # to patch the mock lib _patch_mock_to_raise_for_invalid_assert_calls() @@ -99,12 +95,12 @@ def assert_called(self, method, path, body=None): if body: req_data = self.requests_mock.last_request.body - if isinstance(req_data, six.binary_type): + if isinstance(req_data, bytes): req_data = req_data.decode('utf-8') - if not isinstance(body, six.string_types): + if not isinstance(body, str): # json load if the input body to match against is not a string req_data = jsonutils.loads(req_data) - self.assertEqual(req_data, body) + self.assertEqual(body, req_data) class TestResponse(requests.Response): diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 4138e8a98..f3e6d9908 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -14,13 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy import datetime import re +from unittest import mock +from urllib import parse -import mock from oslo_utils import strutils -import six -from six.moves.urllib import parse import novaclient from novaclient import api_versions @@ -57,6 +57,48 @@ FAKE_SERVICE_UUID_1 = '75e9eabc-ed3b-4f11-8bba-add1e7e7e2de' FAKE_SERVICE_UUID_2 = '1f140183-c914-4ddf-8757-6df73028aa86' +SERVER_TOPOLOGY = { + "nodes": [ + { + "cpu_pinning": { + "0": 0, + "1": 5 + }, + "host_node": 0, + "memory_mb": 1024, + "siblings": [ + [ + 0, + 1 + ] + ], + "vcpu_set": [ + 0, + 1 + ] + }, + { + "cpu_pinning": { + "2": 1, + "3": 8 + }, + "host_node": 1, + "memory_mb": 2048, + "siblings": [ + [ + 2, + 3 + ] + ], + "vcpu_set": [ + 2, + 3 + ] + } + ], + "pagesize_kb": 4 +} + class FakeClient(fakes.FakeClient, client.Client): @@ -123,6 +165,9 @@ def _cs_request(self, url, method, **kwargs): munged_url = munged_url.replace(' ', '_') munged_url = munged_url.replace('!', '_') munged_url = munged_url.replace('@', '_') + munged_url = munged_url.replace('%20', '_') + munged_url = munged_url.replace('%3A', '_') + munged_url = munged_url.replace('%', '_') callback = "%s_%s" % (method.lower(), munged_url) if url is None or callback == "get_http:__nova_api:8774": @@ -173,6 +218,10 @@ def _cs_request(self, url, method, **kwargs): "text": body, "headers": headers, }) + + if status >= 400: + raise exceptions.from_response(r, body, url, method) + return r, body def get_versions(self): @@ -278,58 +327,19 @@ def put_os_agents_1(self, body, **kw): "md5hash": "add6bb58e139be103324d04d82d8f546", 'id': 1}}) - # - # List all extensions - # - - def get_extensions(self, **kw): - exts = [ - { - "alias": "NMN", - "description": "Multiple network support", - "links": [], - "name": "Multinic", - "namespace": ("http://docs.openstack.org/" - "compute/ext/multinic/api/v1.1"), - "updated": "2011-06-09T00:00:00+00:00" - }, - { - "alias": "OS-DCF", - "description": "Disk Management Extension", - "links": [], - "name": "DiskConfig", - "namespace": ("http://docs.openstack.org/" - "compute/ext/disk_config/api/v1.1"), - "updated": "2011-09-27T00:00:00+00:00" - }, - { - "alias": "OS-EXT-SRV-ATTR", - "description": "Extended Server Attributes support.", - "links": [], - "name": "ExtendedServerAttributes", - "namespace": ("http://docs.openstack.org/" - "compute/ext/extended_status/api/v1.1"), - "updated": "2011-11-03T00:00:00+00:00" - }, - { - "alias": "OS-EXT-STS", - "description": "Extended Status support", - "links": [], - "name": "ExtendedStatus", - "namespace": ("http://docs.openstack.org/" - "compute/ext/extended_status/api/v1.1"), - "updated": "2011-11-03T00:00:00+00:00" - }, - ] - return (200, FAKE_RESPONSE_HEADERS, { - "extensions": exts, - }) - # # Limits # def get_limits(self, **kw): + absolute = { + "maxTotalRAMSize": 51200, + "maxServerMeta": 5, + "maxImageMeta": 5 + } + # 2.57 removes injected_file* entries from the response. + if self.api_version < api_versions.APIVersion('2.57'): + absolute.update({"maxPersonality": 5, "maxPersonalitySize": 10240}) return (200, {}, {"limits": { "rate": [ { @@ -373,13 +383,7 @@ def get_limits(self, **kw): ] } ], - "absolute": { - "maxTotalRAMSize": 51200, - "maxServerMeta": 5, - "maxImageMeta": 5, - "maxPersonality": 5, - "maxPersonalitySize": 10240 - }, + "absolute": absolute, }}) # @@ -387,14 +391,32 @@ def get_limits(self, **kw): # def get_servers(self, **kw): - return (200, {}, {"servers": [ + servers = {"servers": [ {'id': '1234', 'name': 'sample-server'}, {'id': '5678', 'name': 'sample-server2'}, {'id': '9014', 'name': 'help'} - ]}) + ]} + if self.api_version >= api_versions.APIVersion('2.69'): + # include "partial results" from non-responsive part of + # infrastructure. + servers['servers'].append( + {'id': '9015', + 'status': "UNKNOWN", + "links": [ + { + "href": "http://fake/v2.1/", + "rel": "self" + }, + { + "href": "http://fake", + "rel": "bookmark" + } + ]} + ) + return (200, {}, servers) def get_servers_detail(self, **kw): - return (200, {}, {"servers": [ + servers = {"servers": [ { "id": '1234', "name": "sample-server", @@ -404,7 +426,7 @@ def get_servers_detail(self, **kw): }, "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", "status": "BUILD", @@ -445,7 +467,7 @@ def get_servers_detail(self, **kw): }, "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", @@ -486,7 +508,7 @@ def get_servers_detail(self, **kw): "image": "", "flavor": { "id": 1, - "name": "256 MB Server", + "name": "256 MiB Server", }, "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", @@ -533,7 +555,29 @@ def get_servers_detail(self, **kw): "hostId": "9e107d9d372bb6826bd81d3542a419d6", "status": "ACTIVE", }, - ]}) + ]} + if self.api_version >= api_versions.APIVersion('2.69'): + # include "partial results" from non-responsive part of + # infrastructure. + servers['servers'].append( + { + "id": "9015", + "status": "UNKNOWN", + "tenant_id": "6f70656e737461636b20342065766572", + "created": "2018-12-03T21:06:18Z", + "links": [ + { + "href": "http://fake/v2.1/", + "rel": "self" + }, + { + "href": "http://fake", + "rel": "bookmark" + } + ] + } + ) + return (200, {}, servers) def post_servers(self, body, **kw): assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) @@ -549,27 +593,12 @@ def post_servers(self, body, **kw): else: return (202, {}, self.get_servers_1234()[2]) - def post_os_volumes_boot(self, body, **kw): - assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) - fakes.assert_has_keys( - body['server'], - required=['name', 'flavorRef'], - optional=['imageRef']) - - # Require one, and only one, of the keys for bdm - if 'block_device_mapping' not in body['server']: - if 'block_device_mapping_v2' not in body['server']: - raise AssertionError( - "missing required keys: 'block_device_mapping'" - ) - elif 'block_device_mapping_v2' in body['server']: - raise AssertionError("found extra keys: 'block_device_mapping'") - - return (202, {}, self.get_servers_9012()[2]) - def get_servers_1234(self, **kw): - r = {'server': self.get_servers_detail()[2]['servers'][0]} - return (200, {}, r) + server = self.get_servers_detail()[2]['servers'][0] + if self.api_version >= api_versions.APIVersion('2.71'): + server.update( + {'server_groups': ['a67359fb-d397-4697-88f1-f55e3ee7c499']}) + return (200, {}, {'server': server}) def get_servers_1235(self, **kw): r = {'server': self.get_servers_detail()[2]['servers'][0]} @@ -594,6 +623,27 @@ def get_servers_9014(self, **kw): r = {'server': self.get_servers_detail()[2]['servers'][4]} return (200, {}, r) + def get_servers_9015(self, **kw): + r = {'server': self.get_servers_detail()[2]['servers'][5]} + r['server']["OS-EXT-AZ:availability_zone"] = 'geneva' + r['server']["OS-EXT-STS:power_state"] = 0 + flavor = { + "disk": 1, + "ephemeral": 0, + "original_name": "m1.tiny", + "ram": 512, + "swap": 0, + "vcpus": 1, + "extra_specs": {} + } + image = { + "id": "c99d7632-bd66-4be9-aed5-3dd14b223a76", + } + r['server']['image'] = image + r['server']['flavor'] = flavor + r['server']['user_id'] = "fake" + return (200, {}, r) + def delete_os_server_groups_12345(self, **kw): return (202, {}, None) @@ -630,6 +680,12 @@ def post_servers_uuid3_metadata(self, **kw): def post_servers_uuid4_metadata(self, **kw): return (204, {}, {'metadata': {'key1': 'val1'}}) + def post_servers_uuid5_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def post_servers_uuid6_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + def delete_servers_uuid1_metadata_key1(self, **kw): return (200, {}, {'data': 'Fake diagnostics'}) @@ -642,6 +698,12 @@ def delete_servers_uuid3_metadata_key1(self, **kw): def delete_servers_uuid4_metadata_key1(self, **kw): return (200, {}, {'data': 'Fake diagnostics'}) + def delete_servers_uuid5_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def delete_servers_uuid6_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + def get_servers_1234_os_security_groups(self, **kw): return (200, {}, { "security_groups": [{ @@ -652,6 +714,9 @@ def get_servers_1234_os_security_groups(self, **kw): 'rules': []}] }) + def get_servers_1234_topology(self, **kw): + return 200, {}, SERVER_TOPOLOGY + # # Server password # @@ -686,10 +751,10 @@ def delete_servers_1234_os_server_password(self, **kw): # Server actions # - none_actions = ['revertResize', 'migrate', 'os-stop', 'os-start', + none_actions = ['revertResize', 'os-stop', 'os-start', 'forceDelete', 'restore', 'pause', 'unpause', 'unlock', - 'unrescue', 'resume', 'suspend', 'lock', 'shelve', - 'shelveOffload', 'unshelve', 'resetNetwork'] + 'unrescue', 'resume', 'suspend', 'shelve', + 'shelveOffload', 'resetNetwork'] type_actions = ['os-getVNCConsole', 'os-getSPICEConsole', 'os-getRDPConsole'] @@ -703,13 +768,6 @@ def check_server_actions(cls, body): assert 'flavorRef' in body[action] elif action in cls.none_actions: assert body[action] is None - elif action == 'addFixedIp': - assert list(body[action]) == ['networkId'] - elif action in ['removeFixedIp', 'removeFloatingIp']: - assert list(body[action]) == ['address'] - elif action == 'addFloatingIp': - assert (list(body[action]) == ['address'] or - sorted(list(body[action])) == ['address', 'fixed_address']) elif action == 'changePassword': assert list(body[action]) == ['adminPass'] elif action in cls.type_actions: @@ -748,6 +806,41 @@ def post_servers_1234_action(self, body, **kw): if self.api_version < api_versions.APIVersion("2.25"): expected.add('disk_over_commit') assert set(body[action].keys()) == expected + elif action == 'migrate': + if self.api_version < api_versions.APIVersion("2.56"): + assert body[action] is None + else: + expected = set() + if 'host' in body[action].keys(): + # host can be optional + expected.add('host') + assert set(body[action].keys()) == expected + elif action == 'lock': + if self.api_version < api_versions.APIVersion("2.73"): + assert body[action] is None + else: + # In 2.73 and above, we allow body to be one of these: + # a) {'lock': None} + # b) {'lock': {}} + # c) {'lock': {locked_reason': 'blah'}} + if body[action] is not None: + expected = set() + if 'locked_reason' in body[action].keys(): + # reason can be optional + expected.add('locked_reason') + assert set(body[action].keys()) == expected + else: + assert body[action] is None + elif action == 'unshelve': + if self.api_version < api_versions.APIVersion("2.77"): + assert body[action] is None + else: + # In 2.77 to 2.91, we allow body to be one of these: + # {'unshelve': None} + # {'unshelve': {'availability_zone': 'foo-az'}} + if body[action] is not None: + assert set(body[action].keys()) == set( + ['availability_zone']) elif action == 'rebuild': body = body[action] adminPass = body.get('adminPass', 'randompassword') @@ -805,55 +898,39 @@ def post_servers_1234_action(self, body, **kw): def post_servers_5678_action(self, body, **kw): return self.post_servers_1234_action(body, **kw) - # - # Cloudpipe - # - - def get_os_cloudpipe(self, **kw): - return ( - 200, - {}, - {'cloudpipes': [{'project_id': 1}]} - ) - - def post_os_cloudpipe(self, **ks): - return ( - 202, - {}, - {'instance_id': '9d5824aa-20e6-4b9f-b967-76a699fc51fd'} - ) - - def put_os_cloudpipe_configure_project(self, **kw): - return (202, {}, None) - # # Flavors # def get_flavors(self, **kw): status, header, flavors = self.get_flavors_detail(**kw) + included_fields = ['id', 'name'] + if self.api_version >= api_versions.APIVersion('2.55'): + included_fields.append('description') + if self.api_version >= api_versions.APIVersion('2.61'): + included_fields.append('extra_specs') for flavor in flavors['flavors']: for k in list(flavor): - if k not in ['id', 'name']: + if k not in included_fields: del flavor[k] return (200, FAKE_RESPONSE_HEADERS, flavors) def get_flavors_detail(self, **kw): flavors = {'flavors': [ - {'id': 1, 'name': '256 MB Server', 'ram': 256, 'disk': 10, + {'id': 1, 'name': '256 MiB Server', 'ram': 256, 'disk': 10, 'OS-FLV-EXT-DATA:ephemeral': 10, 'os-flavor-access:is_public': True, 'links': {}}, - {'id': 2, 'name': '512 MB Server', 'ram': 512, 'disk': 20, + {'id': 2, 'name': '512 MiB Server', 'ram': 512, 'disk': 20, 'OS-FLV-EXT-DATA:ephemeral': 20, 'os-flavor-access:is_public': False, 'links': {}}, - {'id': 4, 'name': '1024 MB Server', 'ram': 1024, 'disk': 10, + {'id': 4, 'name': '1024 MiB Server', 'ram': 1024, 'disk': 10, 'OS-FLV-EXT-DATA:ephemeral': 10, 'os-flavor-access:is_public': True, 'links': {}}, - {'id': 'aa1', 'name': '128 MB Server', 'ram': 128, 'disk': 0, + {'id': 'aa1', 'name': '128 MiB Server', 'ram': 128, 'disk': 0, 'OS-FLV-EXT-DATA:ephemeral': 0, 'os-flavor-access:is_public': True, 'links': {}} @@ -880,6 +957,23 @@ def get_flavors_detail(self, **kw): if not v['os-flavor-access:is_public'] ] + # Add description in the response for all flavors. + if self.api_version >= api_versions.APIVersion('2.55'): + for flavor in flavors['flavors']: + flavor['description'] = None + # Add a new flavor that is a copy of the first but with a different + # name, flavorid and a description set. + new_flavor = copy.deepcopy(flavors['flavors'][0]) + new_flavor['id'] = 'with-description' + new_flavor['name'] = 'with-description' + new_flavor['description'] = 'test description' + flavors['flavors'].append(new_flavor) + + # Add extra_specs in the response for all flavors. + if self.api_version >= api_versions.APIVersion('2.61'): + for flavor in flavors['flavors']: + flavor['extra_specs'] = {'test': 'value'} + return (200, FAKE_RESPONSE_HEADERS, flavors) def get_flavors_1(self, **kw): @@ -905,16 +999,16 @@ def get_flavors_3(self, **kw): FAKE_RESPONSE_HEADERS, {'flavor': { 'id': 3, - 'name': '256 MB Server', + 'name': '256 MiB Server', 'ram': 256, 'disk': 10, }}, ) - def get_flavors_512_MB_Server(self, **kw): + def get_flavors_512_MiB_Server(self, **kw): raise exceptions.NotFound('404') - def get_flavors_128_MB_Server(self, **kw): + def get_flavors_128_MiB_Server(self, **kw): raise exceptions.NotFound('404') def get_flavors_80645cf4_6ad3_410a_bbc8_6f3e1e291f51(self, **kw): @@ -937,6 +1031,14 @@ def get_flavors_4(self, **kw): self.get_flavors_detail(is_public='None')[2]['flavors'][2]} ) + def get_flavors_with_description(self, **kw): + return ( + 200, + {}, + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][-1]} + ) + def delete_flavors_flavordelete(self, **kw): return (202, FAKE_RESPONSE_HEADERS, None) @@ -951,6 +1053,14 @@ def post_flavors(self, body, **kw): self.get_flavors_detail(is_public='None')[2]['flavors'][0]} ) + def put_flavors_with_description(self, body, **kw): + assert 'flavor' in body + assert 'description' in body['flavor'] + flavor = self.get_flavors_with_description(**kw)[2] + # Fake out the actual update of the flavor description for the response + flavor['description'] = body['flavor']['description'] + return (200, {}, {'flavor': flavor}) + def get_flavors_1_os_extra_specs(self, **kw): return ( 200, @@ -1012,7 +1122,7 @@ def post_flavors_2_action(self, body, **kw): # Images # def get_images(self, **kw): - return (200, {}, {'images': [ + images = [ { "id": FAKE_IMAGE_UUID_SNAPSHOT, "name": "My Server Backup", @@ -1062,7 +1172,16 @@ def get_images(self, **kw): "progress": 80, "links": {}, }, - ]}) + ] + + if 'id' in kw: + requested = kw['id'].replace('in:', '').split(',') + images = [i for i in images if i['id'] in requested] + if 'names' in kw: + requested = kw['names'].replace('in:', '').split(',') + images = [i for i in images if i['name'] in requested] + + return (200, {}, {'images': images}) def get_images_555cae93_fb41_4145_9c52_f5b923538a26(self, **kw): return (200, {}, {'image': self.get_images()[2]['images'][0]}) @@ -1099,7 +1218,7 @@ def get_os_keypairs(self, user_id=None, limit=None, marker=None, *kw): "deleted": False, "created_at": "2014-04-19T02:16:44.000000", "updated_at": "2014-04-19T10:12:3.000000", - "figerprint": "FAKE_KEYPAIR", + "fingerprint": "FAKE_KEYPAIR", "deleted_at": None, "id": 4}} ]}) @@ -1114,14 +1233,6 @@ def post_os_keypairs(self, body, **kw): r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]['keypair']} return (202, {}, r) - # - # Virtual Interfaces - # - def get_servers_1234_os_virtual_interfaces(self, **kw): - return (200, {}, {"virtual_interfaces": [ - {'id': 'fakeid', 'mac_address': 'fakemac'} - ]}) - # # Quotas # @@ -1292,6 +1403,19 @@ def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): # def get_os_quota_class_sets_test(self, **kw): + # 2.57 removes injected_file* entries from the response. + if self.api_version >= api_versions.APIVersion('2.57'): + return (200, FAKE_RESPONSE_HEADERS, { + 'quota_class_set': { + 'id': 'test', + 'metadata_items': 1, + 'ram': 1, + 'instances': 1, + 'cores': 1, + 'key_pairs': 1, + 'server_groups': 1, + 'server_group_members': 1}}) + if self.api_version >= api_versions.APIVersion('2.50'): return (200, FAKE_RESPONSE_HEADERS, { 'quota_class_set': { @@ -1324,6 +1448,18 @@ def get_os_quota_class_sets_test(self, **kw): def put_os_quota_class_sets_test(self, body, **kw): assert list(body) == ['quota_class_set'] + # 2.57 removes injected_file* entries from the response. + if self.api_version >= api_versions.APIVersion('2.57'): + return (200, {}, { + 'quota_class_set': { + 'metadata_items': 1, + 'ram': 1, + 'instances': 1, + 'cores': 1, + 'key_pairs': 1, + 'server_groups': 1, + 'server_group_members': 1}}) + if self.api_version >= api_versions.APIVersion('2.50'): return (200, {}, { 'quota_class_set': { @@ -1373,158 +1509,146 @@ def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, # Tenant Usage # def get_os_simple_tenant_usage(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, - {six.u('tenant_usages'): [{ - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, - six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, - six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): - six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}]}) + return (200, FAKE_RESPONSE_HEADERS, {'tenant_usages': [{ + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, + 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'vcpus': 1, + 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}]}) def get_os_simple_tenant_usage_next(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, - {six.u('tenant_usages'): [{ - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, - six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-2222-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, - six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): - six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}]}) + return (200, FAKE_RESPONSE_HEADERS, {'tenant_usages': [{ + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, + 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-2222-457b-b350-bb5ecc685cdd', + 'vcpus': 1, + 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}]}) def get_os_simple_tenant_usage_next_next(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, {six.u('tenant_usages'): []}) + return (200, FAKE_RESPONSE_HEADERS, {'tenant_usages': []}) def get_os_simple_tenant_usage_tenantfoo(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, - {six.u('tenant_usage'): { - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): - six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): - six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}}) + return (200, FAKE_RESPONSE_HEADERS, {'tenant_usage': { + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'vcpus': 1, 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}}) def get_os_simple_tenant_usage_test(self, **kw): - return (200, {}, {six.u('tenant_usage'): { - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}}) + return (200, {}, {'tenant_usage': { + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'vcpus': 1, 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}}) def get_os_simple_tenant_usage_tenant_id(self, **kw): - return (200, {}, {six.u('tenant_usage'): { - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-1111-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}}) + return (200, {}, {'tenant_usage': { + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'vcpus': 1, 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}}) def get_os_simple_tenant_usage_tenant_id_next(self, **kw): - return (200, {}, {six.u('tenant_usage'): { - six.u('total_memory_mb_usage'): 25451.762807466665, - six.u('total_vcpus_usage'): 49.71047423333333, - six.u('total_hours'): 49.71047423333333, - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('stop'): six.u('2012-01-22 19:48:41.750722'), - six.u('server_usages'): [{ - six.u('hours'): 49.71047423333333, - six.u('uptime'): 27035, six.u('local_gb'): 0, - six.u('ended_at'): None, - six.u('name'): six.u('f15image1'), - six.u('tenant_id'): six.u('7b0a1d73f8fb41718f3343c207597869'), - six.u('instance_id'): - six.u('f079e394-2222-457b-b350-bb5ecc685cdd'), - six.u('vcpus'): 1, six.u('memory_mb'): 512, - six.u('state'): six.u('active'), - six.u('flavor'): six.u('m1.tiny'), - six.u('started_at'): six.u('2012-01-20 18:06:06.479998')}], - six.u('start'): six.u('2011-12-25 19:48:41.750687'), - six.u('total_local_gb_usage'): 0.0}}) + return (200, {}, {'tenant_usage': { + 'total_memory_mb_usage': 25451.762807466665, + 'total_vcpus_usage': 49.71047423333333, + 'total_hours': 49.71047423333333, + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'stop': '2012-01-22 19:48:41.750722', + 'server_usages': [{ + 'hours': 49.71047423333333, + 'uptime': 27035, 'local_gb': 0, + 'ended_at': None, + 'name': 'f15image1', + 'tenant_id': '7b0a1d73f8fb41718f3343c207597869', + 'instance_id': 'f079e394-2222-457b-b350-bb5ecc685cdd', + 'vcpus': 1, 'memory_mb': 512, + 'state': 'active', + 'flavor': 'm1.tiny', + 'started_at': '2012-01-20 18:06:06.479998', + }], + 'start': '2011-12-25 19:48:41.750687', + 'total_local_gb_usage': 0.0}}) def get_os_simple_tenant_usage_tenant_id_next_next(self, **kw): - return (200, {}, {six.u('tenant_usage'): {}}) + return (200, {}, {'tenant_usage': {}}) # # Aggregates @@ -1579,6 +1703,9 @@ def post_os_aggregates_3_action(self, body, **kw): def delete_os_aggregates_1(self, **kw): return (202, {}, None) + def post_os_aggregates_1_images(self, body, **kw): + return (202, {}, None) + # # Services # @@ -1591,24 +1718,35 @@ def get_os_services(self, **kw): else: service_id_1 = 1 service_id_2 = 2 - return (200, FAKE_RESPONSE_HEADERS, - {'services': [{'binary': binary, - 'host': host, - 'zone': 'nova', - 'status': 'enabled', - 'state': 'up', - 'updated_at': datetime.datetime( - 2012, 10, 29, 13, 42, 2), - 'id': service_id_1}, - {'binary': binary, - 'host': host, - 'zone': 'nova', - 'status': 'disabled', - 'state': 'down', - 'updated_at': datetime.datetime( - 2012, 9, 18, 8, 3, 38), - 'id': service_id_2}, - ]}) + services = { + 'services': [ + {'binary': binary, + 'host': host, + 'zone': 'nova', + 'status': 'enabled', + 'state': 'up', + 'updated_at': datetime.datetime( + 2012, 10, 29, 13, 42, 2), + 'id': service_id_1}, + {'binary': binary, + 'host': host, + 'zone': 'nova', + 'status': 'disabled', + 'state': 'down', + 'updated_at': datetime.datetime( + 2012, 9, 18, 8, 3, 38), + 'id': service_id_2}, + ] + } + if self.api_version >= api_versions.APIVersion('2.69'): + services['services'].append( + { + "binary": "nova-compute", + "host": "host-down", + "status": "UNKNOWN" + } + ) + return (200, FAKE_RESPONSE_HEADERS, services) def put_os_services_enable(self, body, **kw): return (200, FAKE_RESPONSE_HEADERS, @@ -1652,43 +1790,9 @@ def put_os_services_force_down(self, body, **kw): 'forced_down': False}}) # - # Hosts + # Hypervisors # - def get_os_hosts(self, **kw): - zone = kw.get('zone', 'nova1') - return (200, {}, {'hosts': [{'host': 'host1', - 'service': 'nova-compute', - 'zone': zone}, - {'host': 'host1', - 'service': 'nova-cert', - 'zone': zone}]}) - - def put_os_hosts_sample_host_1(self, body, **kw): - return (200, {}, {'host': 'sample-host_1', - 'status': 'enabled'}) - - def put_os_hosts_sample_host_2(self, body, **kw): - return (200, {}, {'host': 'sample-host_2', - 'maintenance_mode': 'on_maintenance'}) - - def put_os_hosts_sample_host_3(self, body, **kw): - return (200, {}, {'host': 'sample-host_3', - 'status': 'enabled', - 'maintenance_mode': 'on_maintenance'}) - - def get_os_hosts_sample_host_reboot(self, **kw): - return (200, {}, {'host': 'sample_host', - 'power_action': 'reboot'}) - - def get_os_hosts_sample_host_startup(self, **kw): - return (200, {}, {'host': 'sample_host', - 'power_action': 'startup'}) - - def get_os_hosts_sample_host_shutdown(self, **kw): - return (200, {}, {'host': 'sample_host', - 'power_action': 'shutdown'}) - def get_os_hypervisors(self, **kw): return (200, {}, { "hypervisors": [ @@ -1775,6 +1879,26 @@ def get_os_hypervisors_hyper_servers(self, **kw): {'name': 'inst4', 'uuid': 'uuid4'}]}] }) + def get_os_hypervisors_hyper1_servers(self, **kw): + return (200, {}, { + 'hypervisors': [ + {'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'servers': [ + {'name': 'inst1', 'uuid': 'uuid1'}, + {'name': 'inst2', 'uuid': 'uuid2'}]}] + }) + + def get_os_hypervisors_hyper2_servers(self, **kw): + return (200, {}, { + 'hypervisors': [ + {'id': 5678, + 'hypervisor_hostname': 'hyper2', + 'servers': [ + {'name': 'inst3', 'uuid': 'uuid3'}, + {'name': 'inst4', 'uuid': 'uuid4'}]}] + }) + def get_os_hypervisors_hyper_no_servers_servers(self, **kw): return (200, {}, {'hypervisors': [{'id': 1234, 'hypervisor_hostname': 'hyper1'}]}) @@ -1888,7 +2012,7 @@ def get_os_availability_zone_detail(self, **kw): "hosts": None}]}) def get_servers_1234_os_interface(self, **kw): - return (200, {}, { + attachments = { "interfaceAttachments": [ {"port_state": "ACTIVE", "net_id": "net-id-1", @@ -1901,27 +2025,43 @@ def get_servers_1234_os_interface(self, **kw): "port_id": "port-id-1", "mac_address": "aa:bb:cc:dd:ee:ff", "fixed_ips": [{"ip_address": "1.2.3.4"}], - }] - }) + } + ] + } + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in each attachment. + for attachment in attachments['interfaceAttachments']: + attachment['tag'] = 'test-tag' + return (200, {}, attachments) def post_servers_1234_os_interface(self, **kw): - return (200, {}, {'interfaceAttachment': {}}) + attachment = {} + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in the response. + attachment['tag'] = 'test-tag' + return (200, {}, {'interfaceAttachment': attachment}) def delete_servers_1234_os_interface_port_id(self, **kw): return (200, {}, None) def post_servers_1234_os_volume_attachments(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { - "volumeAttachment": - {"device": "/dev/vdb", - "volumeId": 2}}) + attachment = {"device": "/dev/vdb", "volumeId": 2} + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in the response. + attachment['tag'] = 'test-tag' + + if self.api_version >= api_versions.APIVersion('2.79'): + # Include the "delete_on_termination" field in the + # response. + attachment['delete_on_termination'] = True + return (200, FAKE_RESPONSE_HEADERS, {"volumeAttachment": attachment}) def put_servers_1234_os_volume_attachments_Work(self, **kw): return (200, FAKE_RESPONSE_HEADERS, {"volumeAttachment": {"volumeId": 2}}) def get_servers_1234_os_volume_attachments(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { + attachments = { "volumeAttachments": [ {"display_name": "Work", "display_description": "volume for work", @@ -1931,7 +2071,20 @@ def get_servers_1234_os_volume_attachments(self, **kw): "attached": "2011-11-11T00:00:00Z", "size": 1024, "attachments": [{"id": "3333", "links": ''}], - "metadata": {}}]}) + "metadata": {}} + ] + } + if self.api_version >= api_versions.APIVersion('2.70'): + # Include the "tag" field in each attachment. + for attachment in attachments['volumeAttachments']: + attachment['tag'] = 'test-tag' + + if self.api_version >= api_versions.APIVersion('2.79'): + # Include the "delete_on_termination" field in each + # attachment. + for attachment in attachments['volumeAttachments']: + attachment['delete_on_termination'] = True + return (200, FAKE_RESPONSE_HEADERS, attachments) def get_servers_1234_os_volume_attachments_Work(self, **kw): return (200, FAKE_RESPONSE_HEADERS, { @@ -1950,26 +2103,114 @@ def delete_servers_1234_os_volume_attachments_Work(self, **kw): return (200, FAKE_RESPONSE_HEADERS, {}) def get_servers_1234_os_instance_actions(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { - "instanceActions": - [{"instance_uuid": "1234", + action = {"instance_uuid": "1234", "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", "start_time": "2013-03-25T13:45:09.000000", "request_id": "req-abcde12345", "action": "create", "message": None, - "project_id": "04019601fe3648c0abd4f4abfb9e6106"}]}) + "project_id": "04019601fe3648c0abd4f4abfb9e6106"} + if self.api_version >= api_versions.APIVersion('2.58'): + # This is intentionally different from the start_time. + action['updated_at'] = '2013-03-25T13:50:09.000000' + return (200, FAKE_RESPONSE_HEADERS, { + "instanceActions": [action]}) def get_servers_1234_os_instance_actions_req_abcde12345(self, **kw): + action = {"instance_uuid": "1234", + "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", + "start_time": "2013-03-25T13:45:09.000000", + "request_id": "req-abcde12345", + "action": "create", + "message": None, + "project_id": "04019601fe3648c0abd4f4abfb9e6106"} + if self.api_version >= api_versions.APIVersion('2.58'): + action['updated_at'] = '2013-03-25T13:45:09.000000' return (200, FAKE_RESPONSE_HEADERS, { - "instanceAction": - {"instance_uuid": "1234", - "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", - "start_time": "2013-03-25T13:45:09.000000", - "request_id": "req-abcde12345", - "action": "create", - "message": None, - "project_id": "04019601fe3648c0abd4f4abfb9e6106"}}) + "instanceAction": action}) + + def get_os_instance_usage_audit_log(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, { + "instance_usage_audit_logs": { + "hosts_not_run": ["samplehost3"], + "log": { + "samplehost0": { + "errors": 1, + "instances": 1, + "message": ("Instance usage audit ran for host " + "samplehost0, 1 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + "samplehost1": { + "errors": 1, + "instances": 2, + "message": ("Instance usage audit ran for host " + "samplehost1, 2 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + "samplehost2": { + "errors": 1, + "instances": 3, + "message": ("Instance usage audit ran for host " + "samplehost2, 3 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + }, + "num_hosts": 4, + "num_hosts_done": 3, + "num_hosts_not_run": 1, + "num_hosts_running": 0, + "overall_status": "3 of 4 hosts done. 3 errors.", + "period_beginning": "2012-06-01 00:00:00", + "period_ending": "2012-07-01 00:00:00", + "total_errors": 3, + "total_instances": 6}}) + + def get_os_instance_usage_audit_log_2016_12_10_13_59_59_999999(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, { + "instance_usage_audit_log": { + "hosts_not_run": ["samplehost3"], + "log": { + "samplehost0": { + "errors": 1, + "instances": 1, + "message": ("Instance usage audit ran for host " + "samplehost0, 1 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + "samplehost1": { + "errors": 1, + "instances": 2, + "message": ("Instance usage audit ran for host " + "samplehost1, 2 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + "samplehost2": { + "errors": 1, + "instances": 3, + "message": ("Instance usage audit ran for host " + "samplehost2, 3 instances in 0.01 " + "seconds."), + "state": "DONE" + }, + }, + "num_hosts": 4, + "num_hosts_done": 3, + "num_hosts_not_run": 1, + "num_hosts_running": 0, + "overall_status": "3 of 4 hosts done. 3 errors.", + "period_beginning": "2012-06-01 00:00:00", + "period_ending": "2012-07-01 00:00:00", + "total_errors": 3, + "total_instances": 6}}) + + def get_os_instance_usage_audit_log__5Cu5de5_5Cu4f5c(self, **kw): + return (400, {}, {"Invalid timestamp for date \u6f22\u5b57"}) def post_servers_uuid1_action(self, **kw): return 202, {}, {} @@ -1983,33 +2224,11 @@ def post_servers_uuid3_action(self, **kw): def post_servers_uuid4_action(self, **kw): return 202, {}, {} - def get_os_cells_child_cell(self, **kw): - cell = {'cell': { - 'username': 'cell1_user', - 'name': 'cell1', - 'rpc_host': '10.0.1.10', - 'info': { - 'username': 'cell1_user', - 'rpc_host': '10.0.1.10', - 'type': 'child', - 'name': 'cell1', - 'rpc_port': 5673}, - 'type': 'child', - 'rpc_port': 5673, - 'loaded': True - }} - return (200, FAKE_RESPONSE_HEADERS, cell) - - def get_os_cells_capacities(self, **kw): - cell_capacities_response = {"cell": {"capacities": {"ram_free": { - "units_by_mb": {"8192": 0, "512": 13, "4096": 1, "2048": 3, - "16384": 0}, "total_mb": 7680}, "disk_free": { - "units_by_mb": {"81920": 11, "20480": 46, "40960": 23, "163840": 5, - "0": 0}, "total_mb": 1052672}}}} - return (200, FAKE_RESPONSE_HEADERS, cell_capacities_response) + def post_servers_uuid5_action(self, **kw): + return 202, {}, {} - def get_os_cells_child_cell_capacities(self, **kw): - return self.get_os_cells_capacities() + def post_servers_uuid6_action(self, **kw): + return 202, {}, {} def get_os_migrations(self, **kw): migration1 = { @@ -2046,6 +2265,18 @@ def get_os_migrations(self, **kw): migration1.update({"migration_type": "live-migration"}) migration2.update({"migration_type": "live-migration"}) + if self.api_version >= api_versions.APIVersion("2.59"): + migration1.update({"uuid": "11111111-07d5-11e1-90e3-e3dffe0c5983"}) + migration2.update({"uuid": "22222222-07d5-11e1-90e3-e3dffe0c5983"}) + + if self.api_version >= api_versions.APIVersion("2.80"): + migration1.update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + migration2.update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + migration_list = [] instance_uuid = kw.get('instance_uuid', None) if instance_uuid == migration1['instance_uuid']: @@ -2102,8 +2333,13 @@ def get_os_server_groups(self, **kw): return (200, {}, {"server_groups": server_groups}) def _return_server_group(self): - r = {'server_group': - self.get_os_server_groups()[2]['server_groups'][0]} + if self.api_version < api_versions.APIVersion("2.64"): + r = {'server_group': + self.get_os_server_groups()[2]['server_groups'][0]} + else: + r = {"members": [], "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", + 'server_group': {'name': 'ig1', 'policy': 'anti-affinity', + 'rules': {'max_server_per_host': 3}}} return (200, {}, r) def post_os_server_groups(self, body, **kw): @@ -2132,6 +2368,12 @@ def get_servers_1234_migrations_1(self, **kw): "disk_remaining_bytes": 230000, "updated_at": "2016-01-29T13:42:02.000000" }} + + if self.api_version >= api_versions.APIVersion("2.80"): + migration['migration'].update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + return (200, FAKE_RESPONSE_HEADERS, migration) @api_versions.wraps(start_version="2.23") @@ -2155,11 +2397,20 @@ def get_servers_1234_migrations(self, **kw): "disk_remaining_bytes": 230000, "updated_at": "2016-01-29T13:42:02.000000" }]} + + if self.api_version >= api_versions.APIVersion("2.80"): + migrations['migrations'][0].update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + return (200, FAKE_RESPONSE_HEADERS, migrations) def delete_servers_1234_migrations_1(self): return (202, {}, None) + def put_servers_1234(self, **kw): + return (201, {}, None) + def put_servers_1234_tags_tag(self, **kw): return (201, {}, None) diff --git a/novaclient/tests/unit/v2/test_aggregates.py b/novaclient/tests/unit/v2/test_aggregates.py index 1de128238..4f3eecdf5 100644 --- a/novaclient/tests/unit/v2/test_aggregates.py +++ b/novaclient/tests/unit/v2/test_aggregates.py @@ -13,11 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions +from novaclient import exceptions from novaclient.tests.unit.fixture_data import aggregates as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import aggregates +from novaclient.v2 import images class AggregatesTest(utils.FixturedTestCase): @@ -161,3 +164,40 @@ def test_delete_aggregate(self): result3 = self.cs.aggregates.delete(aggregate) self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-aggregates/1') + + +class AggregatesV281Test(utils.FixturedTestCase): + api_version = "2.81" + data_fixture_class = data.Fixture + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def setUp(self): + super(AggregatesV281Test, self).setUp() + self.cs.api_version = api_versions.APIVersion(self.api_version) + + def test_cache_images(self): + aggregate = self.cs.aggregates.list()[0] + _images = [images.Image(self.cs.aggregates, {'id': '1'}), + images.Image(self.cs.aggregates, {'id': '2'})] + aggregate.cache_images(_images) + expected_body = {'cache': [{'id': image.id} + for image in _images]} + self.assert_called('POST', '/os-aggregates/1/images', + expected_body) + + def test_cache_images_just_ids(self): + aggregate = self.cs.aggregates.list()[0] + _images = ['1'] + aggregate.cache_images(_images) + expected_body = {'cache': [{'id': '1'}]} + self.assert_called('POST', '/os-aggregates/1/images', + expected_body) + + def test_cache_images_pre281(self): + self.cs.api_version = api_versions.APIVersion('2.80') + aggregate = self.cs.aggregates.list()[0] + _images = [images.Image(self.cs.aggregates, {'id': '1'})] + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + aggregate.cache_images, _images) diff --git a/novaclient/tests/unit/v2/test_availability_zone.py b/novaclient/tests/unit/v2/test_availability_zone.py index ac2bf0f2b..c3ac6f07e 100644 --- a/novaclient/tests/unit/v2/test_availability_zone.py +++ b/novaclient/tests/unit/v2/test_availability_zone.py @@ -14,8 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -import six - from novaclient.tests.unit.fixture_data import availability_zones as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils @@ -54,8 +52,8 @@ def test_list_availability_zone(self): self.assertEqual(2, len(zones)) - l0 = [six.u('zone-1'), six.u('available')] - l1 = [six.u('zone-2'), six.u('not available')] + l0 = ['zone-1', 'available'] + l1 = ['zone-2', 'not available'] z0 = self.shell._treeizeAvailabilityZone(zones[0]) z1 = self.shell._treeizeAvailabilityZone(zones[1]) @@ -75,18 +73,15 @@ def test_detail_availability_zone(self): self.assertEqual(3, len(zones)) - l0 = [six.u('zone-1'), six.u('available')] - l1 = [six.u('|- fake_host-1'), six.u('')] - l2 = [six.u('| |- nova-compute'), - six.u('enabled :-) 2012-12-26 14:45:25')] - l3 = [six.u('internal'), six.u('available')] - l4 = [six.u('|- fake_host-1'), six.u('')] - l5 = [six.u('| |- nova-sched'), - six.u('enabled :-) 2012-12-26 14:45:25')] - l6 = [six.u('|- fake_host-2'), six.u('')] - l7 = [six.u('| |- nova-network'), - six.u('enabled XXX 2012-12-26 14:45:24')] - l8 = [six.u('zone-2'), six.u('not available')] + l0 = ['zone-1', 'available'] + l1 = ['|- fake_host-1', ''] + l2 = ['| |- nova-compute', 'enabled :-) 2012-12-26 14:45:25'] + l3 = ['internal', 'available'] + l4 = ['|- fake_host-1', ''] + l5 = ['| |- nova-sched', 'enabled :-) 2012-12-26 14:45:25'] + l6 = ['|- fake_host-2', ''] + l7 = ['| |- nova-network', 'enabled XXX 2012-12-26 14:45:24'] + l8 = ['zone-2', 'not available'] z0 = self.shell._treeizeAvailabilityZone(zones[0]) z1 = self.shell._treeizeAvailabilityZone(zones[1]) diff --git a/novaclient/tests/unit/v2/test_cells.py b/novaclient/tests/unit/v2/test_cells.py deleted file mode 100644 index 5a47e1c6b..000000000 --- a/novaclient/tests/unit/v2/test_cells.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient import api_versions -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes - - -class CellsExtensionTests(utils.TestCase): - def setUp(self): - super(CellsExtensionTests, self).setUp() - self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) - - def test_get_cells(self): - cell_name = 'child_cell' - cell = self.cs.cells.get(cell_name) - self.assert_request_id(cell, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-cells/%s' % cell_name) - - def test_get_capacities_for_a_given_cell(self): - cell_name = 'child_cell' - ca = self.cs.cells.capacities(cell_name) - self.assert_request_id(ca, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-cells/%s/capacities' % cell_name) - - def test_get_capacities_for_all_cells(self): - ca = self.cs.cells.capacities() - self.assert_request_id(ca, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-cells/capacities') diff --git a/novaclient/tests/unit/v2/test_certs.py b/novaclient/tests/unit/v2/test_certs.py deleted file mode 100644 index d7a3afa85..000000000 --- a/novaclient/tests/unit/v2/test_certs.py +++ /dev/null @@ -1,45 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import mock - -from novaclient.tests.unit.fixture_data import certs as data -from novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import certs - - -class CertsTest(utils.FixturedTestCase): - - data_fixture_class = data.Fixture - cert_type = certs.Certificate - - scenarios = [('original', {'client_fixture_class': client.V1}), - ('session', {'client_fixture_class': client.SessionV1})] - - @mock.patch('warnings.warn') - def test_create_cert(self, mock_warn): - cert = self.cs.certs.create() - self.assert_request_id(cert, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-certificates') - self.assertIsInstance(cert, self.cert_type) - self.assertEqual(1, mock_warn.call_count) - - @mock.patch('warnings.warn') - def test_get_root_cert(self, mock_warn): - cert = self.cs.certs.get() - self.assert_request_id(cert, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-certificates/root') - self.assertIsInstance(cert, self.cert_type) - self.assertEqual(1, mock_warn.call_count) diff --git a/novaclient/tests/unit/v2/test_cloudpipe.py b/novaclient/tests/unit/v2/test_cloudpipe.py deleted file mode 100644 index 2e48d848e..000000000 --- a/novaclient/tests/unit/v2/test_cloudpipe.py +++ /dev/null @@ -1,57 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import mock -import six - -from novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import cloudpipe as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import cloudpipe - - -class CloudpipeTest(utils.FixturedTestCase): - - data_fixture_class = data.Fixture - - scenarios = [('original', {'client_fixture_class': client.V1}), - ('session', {'client_fixture_class': client.SessionV1})] - - @mock.patch('warnings.warn') - def test_list_cloudpipes(self, mock_warn): - cp = self.cs.cloudpipe.list() - self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-cloudpipe') - for c in cp: - self.assertIsInstance(c, cloudpipe.Cloudpipe) - mock_warn.assert_called_once_with(mock.ANY) - - @mock.patch('warnings.warn') - def test_create(self, mock_warn): - project = "test" - cp = self.cs.cloudpipe.create(project) - self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) - body = {'cloudpipe': {'project_id': project}} - self.assert_called('POST', '/os-cloudpipe', body) - self.assertIsInstance(cp, six.string_types) - mock_warn.assert_called_once_with(mock.ANY) - - @mock.patch('warnings.warn') - def test_update(self, mock_warn): - cp = self.cs.cloudpipe.update("192.168.1.1", 2345) - self.assert_request_id(cp, fakes.FAKE_REQUEST_ID_LIST) - body = {'configure_project': {'vpn_ip': "192.168.1.1", - 'vpn_port': 2345}} - self.assert_called('PUT', '/os-cloudpipe/configure-project', body) - mock_warn.assert_called_once_with(mock.ANY) diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py index c882ddb38..fccfc8fdb 100644 --- a/novaclient/tests/unit/v2/test_flavors.py +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from novaclient import api_versions from novaclient import base @@ -122,16 +122,17 @@ def test_find(self): f = self.cs.flavors.find(ram=256) self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called('GET', '/flavors/detail') - self.assertEqual('256 MB Server', f.name) + self.assertEqual('256 MiB Server', f.name) f = self.cs.flavors.find(disk=0) self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) - self.assertEqual('128 MB Server', f.name) + self.assertEqual('128 MiB Server', f.name) self.assertRaises(exceptions.NotFound, self.cs.flavors.find, disk=12345) - def _create_body(self, name, ram, vcpus, disk, ephemeral, id, swap, + @staticmethod + def _create_body(name, ram, vcpus, disk, ephemeral, id, swap, rxtx_factor, is_public): return { "flavor": { @@ -258,3 +259,65 @@ def test_unset_keys(self, mock_delete): mock.call("/flavors/1/os-extra_specs/k1"), mock.call("/flavors/1/os-extra_specs/k2") ]) + + +class FlavorsTest_v2_55(utils.TestCase): + """Tests creating/showing/updating a flavor with a description.""" + def setUp(self): + super(FlavorsTest_v2_55, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion('2.55')) + + def test_list_flavors(self): + fl = self.cs.flavors.list() + self.cs.assert_called('GET', '/flavors/detail') + for flavor in fl: + self.assertTrue(hasattr(flavor, 'description'), + "%s does not have a description set." % flavor) + + def test_list_flavors_undetailed(self): + fl = self.cs.flavors.list(detailed=False) + self.cs.assert_called('GET', '/flavors') + for flavor in fl: + self.assertTrue(hasattr(flavor, 'description'), + "%s does not have a description set." % flavor) + + def test_get_flavor_details(self): + f = self.cs.flavors.get('with-description') + self.cs.assert_called('GET', '/flavors/with-description') + self.assertEqual('test description', f.description) + + def test_create(self): + self.cs.flavors.create( + 'with-description', 512, 1, 10, 'with-description', ephemeral=10, + is_public=False, description='test description') + + body = FlavorsTest._create_body( + "with-description", 512, 1, 10, 10, 'with-description', + 0, 1.0, False) + body['flavor']['description'] = 'test description' + self.cs.assert_called('POST', '/flavors', body) + + def test_create_bad_version(self): + """Tests trying to create a flavor with a description before 2.55.""" + self.cs.api_version = api_versions.APIVersion('2.54') + self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.flavors.create, + 'with-description', 512, 1, 10, 'with-description', + description='test description') + + def test_update(self): + updated_flavor = self.cs.flavors.update( + 'with-description', 'new description') + body = { + 'flavor': { + 'description': 'new description' + } + } + self.cs.assert_called('PUT', '/flavors/with-description', body) + self.assertEqual('new description', updated_flavor.description) + + def test_update_bad_version(self): + """Tests trying to update a flavor with a description before 2.55.""" + self.cs.api_version = api_versions.APIVersion('2.54') + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + self.cs.flavors.update, 'foo', 'bar') diff --git a/novaclient/tests/unit/v2/test_hosts.py b/novaclient/tests/unit/v2/test_hosts.py deleted file mode 100644 index a44a5e01e..000000000 --- a/novaclient/tests/unit/v2/test_hosts.py +++ /dev/null @@ -1,148 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import mock - -from novaclient import api_versions -from novaclient import exceptions -from novaclient.tests.unit.fixture_data import client -from novaclient.tests.unit.fixture_data import hosts as data -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes -from novaclient.v2 import hosts - - -class HostsTest(utils.FixturedTestCase): - - client_fixture_class = client.V1 - data_fixture_class = data.V1 - - def setUp(self): - super(HostsTest, self).setUp() - self.warning_mock = mock.patch('warnings.warn').start() - self.addCleanup(self.warning_mock.stop) - - def test_describe_resource(self): - hs = self.cs.hosts.get('host') - self.warning_mock.assert_called_once() - self.assert_request_id(hs, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hosts/host') - for h in hs: - self.assertIsInstance(h, hosts.Host) - - def test_list_host(self): - hs = self.cs.hosts.list() - self.warning_mock.assert_called_once() - self.assert_request_id(hs, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hosts') - for h in hs: - self.assertIsInstance(h, hosts.Host) - self.assertEqual(h.zone, 'nova1') - - def test_list_host_with_zone(self): - hs = self.cs.hosts.list('nova') - self.assert_request_id(hs, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hosts?zone=nova') - for h in hs: - self.assertIsInstance(h, hosts.Host) - self.assertEqual(h.zone, 'nova') - - def test_update_enable(self): - host = self.cs.hosts.get('sample_host')[0] - values = {"status": "enabled"} - result = host.update(values) - # one warning for the get, one warning for the update - self.assertEqual(2, self.warning_mock.call_count) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertIsInstance(result, hosts.Host) - - def test_update_maintenance(self): - host = self.cs.hosts.get('sample_host')[0] - values = {"maintenance_mode": "enable"} - result = host.update(values) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertIsInstance(result, hosts.Host) - - def test_update_both(self): - host = self.cs.hosts.get('sample_host')[0] - values = {"status": "enabled", - "maintenance_mode": "enable"} - result = host.update(values) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertIsInstance(result, hosts.Host) - - def test_host_startup(self): - host = self.cs.hosts.get('sample_host')[0] - result = host.startup() - # one warning for the get, one warning for the action - self.assertEqual(2, self.warning_mock.call_count) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called( - 'GET', '/os-hosts/sample_host/startup') - - def test_host_reboot(self): - host = self.cs.hosts.get('sample_host')[0] - result = host.reboot() - # one warning for the get, one warning for the action - self.assertEqual(2, self.warning_mock.call_count) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called( - 'GET', '/os-hosts/sample_host/reboot') - - def test_host_shutdown(self): - host = self.cs.hosts.get('sample_host')[0] - result = host.shutdown() - # one warning for the get, one warning for the action - self.assertEqual(2, self.warning_mock.call_count) - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called( - 'GET', '/os-hosts/sample_host/shutdown') - - def test_hosts_repr(self): - hs = self.cs.hosts.get('host') - self.assertEqual('', repr(hs[0])) - - def test_hosts_list_repr(self): - hs = self.cs.hosts.list() - for h in hs: - self.assertEqual('' % h.host_name, repr(h)) - - -class DeprecatedHostsTestv2_43(utils.FixturedTestCase): - """Tests the os-hosts API bindings at microversion 2.43 to ensure - they fail with a 404 error. - """ - client_fixture_class = client.V1 - - def setUp(self): - super(DeprecatedHostsTestv2_43, self).setUp() - self.cs.api_version = api_versions.APIVersion('2.43') - - def test_get(self): - self.assertRaises(exceptions.VersionNotFoundForAPIMethod, - self.cs.hosts.get, 'host') - - def test_list(self): - self.assertRaises(exceptions.VersionNotFoundForAPIMethod, - self.cs.hosts.list) - - def test_update(self): - self.assertRaises(exceptions.VersionNotFoundForAPIMethod, - self.cs.hosts.update, 'host', {"status": "enabled"}) - - def test_host_action(self): - self.assertRaises(exceptions.VersionNotFoundForAPIMethod, - self.cs.hosts.host_action, 'host', 'reboot') diff --git a/novaclient/tests/unit/v2/test_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py index 0a3a2514f..be48914fc 100644 --- a/novaclient/tests/unit/v2/test_hypervisors.py +++ b/novaclient/tests/unit/v2/test_hypervisors.py @@ -14,6 +14,7 @@ # under the License. from novaclient import api_versions +from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import hypervisors as data from novaclient.tests.unit import utils @@ -62,7 +63,9 @@ def test_hypervisor_detail(self): current_workload=2, running_vms=2, cpu_info='cpu_info', - disk_available_least=100), + disk_available_least=100, + state='up', + status='enabled'), dict(id=self.data_fixture.hyper_id_2, service=dict(id=self.data_fixture.service_id_2, host="compute2"), @@ -80,7 +83,24 @@ def test_hypervisor_detail(self): current_workload=2, running_vms=2, cpu_info='cpu_info', - disk_available_least=100)] + disk_available_least=100, + state='up', + status='enabled')] + + if self.cs.api_version >= api_versions.APIVersion('2.88'): + for hypervisor in expected: + del hypervisor['current_workload'] + del hypervisor['disk_available_least'] + del hypervisor['free_ram_mb'] + del hypervisor['free_disk_gb'] + del hypervisor['local_gb'] + del hypervisor['local_gb_used'] + del hypervisor['memory_mb'] + del hypervisor['memory_mb_used'] + del hypervisor['running_vms'] + del hypervisor['vcpus'] + del hypervisor['vcpus_used'] + hypervisor['uptime'] = 'fake uptime' result = self.cs.hypervisors.list() self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) @@ -92,9 +112,13 @@ def test_hypervisor_detail(self): def test_hypervisor_search(self): expected = [ dict(id=self.data_fixture.hyper_id_1, - hypervisor_hostname='hyper1'), + hypervisor_hostname='hyper1', + state='up', + status='enabled'), dict(id=self.data_fixture.hyper_id_2, - hypervisor_hostname='hyper2')] + hypervisor_hostname='hyper2', + state='up', + status='enabled')] result = self.cs.hypervisors.search('hyper') self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) @@ -107,15 +131,38 @@ def test_hypervisor_search(self): for idx, hyper in enumerate(result): self.compare_to_expected(expected[idx], hyper) + def test_hypervisor_search_unicode(self): + hypervisor_match = '\\u5de5\\u4f5c' + if self.cs.api_version >= api_versions.APIVersion('2.53'): + self.assertRaises(exceptions.BadRequest, + self.cs.hypervisors.search, + hypervisor_match) + else: + self.assertRaises(exceptions.NotFound, + self.cs.hypervisors.search, + hypervisor_match) + + def test_hypervisor_search_detailed(self): + # detailed=True is not supported before 2.53 + ex = self.assertRaises(exceptions.UnsupportedVersion, + self.cs.hypervisors.search, 'hyper', + detailed=True) + self.assertIn('Parameter "detailed" requires API version 2.53 or ' + 'greater.', str(ex)) + def test_hypervisor_servers(self): expected = [ dict(id=self.data_fixture.hyper_id_1, hypervisor_hostname='hyper1', + state='up', + status='enabled', servers=[ dict(name='inst1', uuid='uuid1'), dict(name='inst2', uuid='uuid2')]), dict(id=self.data_fixture.hyper_id_2, hypervisor_hostname='hyper2', + state='up', + status='enabled', servers=[ dict(name='inst3', uuid='uuid3'), dict(name='inst4', uuid='uuid4')]), @@ -151,7 +198,23 @@ def test_hypervisor_get(self): current_workload=2, running_vms=2, cpu_info='cpu_info', - disk_available_least=100) + disk_available_least=100, + state='up', + status='enabled') + + if self.cs.api_version >= api_versions.APIVersion('2.88'): + del expected['current_workload'] + del expected['disk_available_least'] + del expected['free_ram_mb'] + del expected['free_disk_gb'] + del expected['local_gb'] + del expected['local_gb_used'] + del expected['memory_mb'] + del expected['memory_mb_used'] + del expected['running_vms'] + del expected['vcpus'] + del expected['vcpus_used'] + expected['uptime'] = 'fake uptime' result = self.cs.hypervisors.get(self.data_fixture.hyper_id_1) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) @@ -164,7 +227,9 @@ def test_hypervisor_uptime(self): expected = dict( id=self.data_fixture.hyper_id_1, hypervisor_hostname="hyper1", - uptime="fake uptime") + uptime="fake uptime", + state='up', + status='enabled') result = self.cs.hypervisors.uptime(self.data_fixture.hyper_id_1) self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) @@ -195,11 +260,6 @@ def test_hypervisor_statistics(self): self.compare_to_expected(expected, result) - def test_hypervisor_statistics_data_model(self): - result = self.cs.hypervisor_stats.statistics() - self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('GET', '/os-hypervisors/statistics') - # Test for Bug #1370415, the line below used to raise AttributeError self.assertEqual("", result.__repr__()) @@ -217,10 +277,59 @@ def test_use_limit_marker_params(self): self.assertEqual([v], self.requests_mock.last_request.qs[k]) -class HypervisorsV2_53Test(HypervisorsV233Test): +class HypervisorsV253Test(HypervisorsV233Test): """Tests the os-hypervisors 2.53 API bindings.""" - data_fixture_class = data.V2_53 + data_fixture_class = data.V253 def setUp(self): - super(HypervisorsV2_53Test, self).setUp() + super(HypervisorsV253Test, self).setUp() self.cs.api_version = api_versions.APIVersion("2.53") + + def test_hypervisor_search_detailed(self): + expected = [ + dict(id=self.data_fixture.hyper_id_1, + state='up', + status='enabled', + hypervisor_hostname='hyper1'), + dict(id=self.data_fixture.hyper_id_2, + state='up', + status='enabled', + hypervisor_hostname='hyper2')] + result = self.cs.hypervisors.search('hyper', detailed=True) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'GET', '/os-hypervisors/detail?hypervisor_hostname_pattern=hyper') + for idx, hyper in enumerate(result): + self.compare_to_expected(expected[idx], hyper) + + +class HypervisorsV288Test(HypervisorsV253Test): + data_fixture_class = data.V288 + + def setUp(self): + super().setUp() + self.cs.api_version = api_versions.APIVersion('2.88') + + def test_hypervisor_uptime(self): + expected = { + 'id': self.data_fixture.hyper_id_1, + 'hypervisor_hostname': 'hyper1', + 'uptime': 'fake uptime', + 'state': 'up', + 'status': 'enabled', + } + + result = self.cs.hypervisors.uptime(self.data_fixture.hyper_id_1) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'GET', '/os-hypervisors/%s' % self.data_fixture.hyper_id_1) + + self.compare_to_expected(expected, result) + + def test_hypervisor_statistics(self): + exc = self.assertRaises( + exceptions.UnsupportedVersion, + self.cs.hypervisor_stats.statistics) + self.assertIn( + "The 'statistics' API is removed in API version 2.88 or later.", + str(exc)) diff --git a/novaclient/tests/unit/v2/test_images.py b/novaclient/tests/unit/v2/test_images.py index a3177a9cb..5fa448e01 100644 --- a/novaclient/tests/unit/v2/test_images.py +++ b/novaclient/tests/unit/v2/test_images.py @@ -11,7 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import images as data diff --git a/novaclient/tests/unit/v2/test_instance_actions.py b/novaclient/tests/unit/v2/test_instance_actions.py index 8eed17197..e2da9d03a 100644 --- a/novaclient/tests/unit/v2/test_instance_actions.py +++ b/novaclient/tests/unit/v2/test_instance_actions.py @@ -16,6 +16,7 @@ from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import instance_action class InstanceActionExtensionTests(utils.TestCase): @@ -39,3 +40,44 @@ def test_get_instance_action(self): self.cs.assert_called( 'GET', '/servers/%s/os-instance-actions/%s' % (server_uuid, request_id)) + + +class InstanceActionExtensionV258Tests(InstanceActionExtensionTests): + def setUp(self): + super(InstanceActionExtensionV258Tests, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.58") + + def test_list_instance_actions_with_limit_marker_params(self): + server_uuid = '1234' + marker = '12140183-c814-4ddf-8453-6df43028aa67' + + ias = self.cs.instance_action.list( + server_uuid, marker=marker, limit=10, + changes_since='2016-02-29T06:23:22') + self.assert_request_id(ias, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'GET', + '/servers/%s/os-instance-actions?changes-since=%s&limit=10&' + 'marker=%s' % (server_uuid, '2016-02-29T06%3A23%3A22', marker)) + for ia in ias: + self.assertIsInstance(ia, instance_action.InstanceAction) + + +class InstanceActionExtensionV266Tests(InstanceActionExtensionV258Tests): + def setUp(self): + super(InstanceActionExtensionV266Tests, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.66") + + def test_list_instance_actions_with_changes_before(self): + server_uuid = '1234' + + ias = self.cs.instance_action.list( + server_uuid, marker=None, limit=None, changes_since=None, + changes_before='2016-02-29T06:23:22') + self.assert_request_id(ias, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'GET', + '/servers/%s/os-instance-actions?changes-before=%s' % + (server_uuid, '2016-02-29T06%3A23%3A22')) + for ia in ias: + self.assertIsInstance(ia, instance_action.InstanceAction) diff --git a/novaclient/tests/unit/v2/test_instance_usage_audit_log.py b/novaclient/tests/unit/v2/test_instance_usage_audit_log.py new file mode 100644 index 000000000..61b4ce734 --- /dev/null +++ b/novaclient/tests/unit/v2/test_instance_usage_audit_log.py @@ -0,0 +1,43 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import api_versions +from novaclient import exceptions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes + + +class InstanceUsageAuditLogTests(utils.TestCase): + def setUp(self): + super(InstanceUsageAuditLogTests, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) + + def test_instance_usage_audit_log(self): + audit_log = self.cs.instance_usage_audit_log.get() + self.assert_request_id(audit_log, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-instance_usage_audit_log') + + def test_instance_usage_audit_log_with_before(self): + audit_log = self.cs.instance_usage_audit_log.get( + before='2016-12-10 13:59:59.999999') + self.assert_request_id(audit_log, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'GET', + '/os-instance_usage_audit_log/2016-12-10%2013%3A59%3A59.999999') + + def test_instance_usage_audit_log_with_before_unicode(self): + before = '\\u5de5\\u4f5c' + self.assertRaises(exceptions.BadRequest, + self.cs.instance_usage_audit_log.get, before) diff --git a/novaclient/tests/unit/v2/test_keypairs.py b/novaclient/tests/unit/v2/test_keypairs.py index cf310b0ef..7e16438d8 100644 --- a/novaclient/tests/unit/v2/test_keypairs.py +++ b/novaclient/tests/unit/v2/test_keypairs.py @@ -127,3 +127,28 @@ def test_list_keypairs(self): % self.keypair_prefix) for kp in kps: self.assertIsInstance(kp, keypairs.Keypair) + + +class KeypairsV92TestCase(KeypairsTest): + def setUp(self): + super(KeypairsV92TestCase, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.92") + + def test_create_keypair(self): + name = "foo" + key_type = "some_type" + public_key = "fake-public-key" + kp = self.cs.keypairs.create(name, public_key=public_key, + key_type=key_type) + self.assert_request_id(kp, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name, + 'public_key': public_key, + 'type': key_type}}) + self.assertIsInstance(kp, keypairs.Keypair) + + def test_create_keypair_without_pubkey(self): + name = "foo" + key_type = "some_type" + self.assertRaises(TypeError, + self.cs.keypairs.create, name, key_type=key_type) diff --git a/novaclient/tests/unit/v2/test_limits.py b/novaclient/tests/unit/v2/test_limits.py index a0b8bcf2a..1abcd1a5d 100644 --- a/novaclient/tests/unit/v2/test_limits.py +++ b/novaclient/tests/unit/v2/test_limits.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import limits as data from novaclient.tests.unit import utils @@ -22,6 +23,8 @@ class LimitsTest(utils.FixturedTestCase): client_fixture_class = client.V1 data_fixture_class = data.Fixture + supports_image_meta = True # 2.39 deprecates maxImageMeta + supports_personality = True # 2.57 deprecates maxPersonality* def test_get_limits(self): obj = self.cs.limits.get() @@ -39,13 +42,16 @@ def test_absolute_limits_reserved(self): obj = self.cs.limits.get(reserved=True) self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST) - expected = ( + expected = [ limits.AbsoluteLimit("maxTotalRAMSize", 51200), - limits.AbsoluteLimit("maxServerMeta", 5), - limits.AbsoluteLimit("maxImageMeta", 5), - limits.AbsoluteLimit("maxPersonality", 5), - limits.AbsoluteLimit("maxPersonalitySize", 10240), - ) + limits.AbsoluteLimit("maxServerMeta", 5) + ] + if self.supports_image_meta: + expected.append(limits.AbsoluteLimit("maxImageMeta", 5)) + if self.supports_personality: + expected.extend([ + limits.AbsoluteLimit("maxPersonality", 5), + limits.AbsoluteLimit("maxPersonalitySize", 10240)]) self.assert_called('GET', '/limits?reserved=1') abs_limits = list(obj.absolute) @@ -75,16 +81,29 @@ def test_rate_absolute_limits(self): for limit in rate_limits: self.assertIn(limit, expected) - expected = ( + expected = [ limits.AbsoluteLimit("maxTotalRAMSize", 51200), - limits.AbsoluteLimit("maxServerMeta", 5), - limits.AbsoluteLimit("maxImageMeta", 5), - limits.AbsoluteLimit("maxPersonality", 5), - limits.AbsoluteLimit("maxPersonalitySize", 10240), - ) + limits.AbsoluteLimit("maxServerMeta", 5) + ] + if self.supports_image_meta: + expected.append(limits.AbsoluteLimit("maxImageMeta", 5)) + if self.supports_personality: + expected.extend([ + limits.AbsoluteLimit("maxPersonality", 5), + limits.AbsoluteLimit("maxPersonalitySize", 10240)]) abs_limits = list(obj.absolute) self.assertEqual(len(abs_limits), len(expected)) for limit in abs_limits: self.assertIn(limit, expected) + + +class LimitsTest2_57(LimitsTest): + data_fixture_class = data.Fixture2_57 + supports_image_meta = False + supports_personality = False + + def setUp(self): + super(LimitsTest2_57, self).setUp() + self.cs.api_version = api_versions.APIVersion('2.57') diff --git a/novaclient/tests/unit/v2/test_list_extensions.py b/novaclient/tests/unit/v2/test_list_extensions.py deleted file mode 100644 index 60783b31a..000000000 --- a/novaclient/tests/unit/v2/test_list_extensions.py +++ /dev/null @@ -1,30 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient import api_versions -from novaclient.tests.unit import utils -from novaclient.tests.unit.v2 import fakes - - -class ListExtensionsTests(utils.TestCase): - def setUp(self): - super(ListExtensionsTests, self).setUp() - self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) - - def test_list_extensions(self): - all_exts = self.cs.list_extensions.show_all() - self.assert_request_id(all_exts, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/extensions') - self.assertGreater(len(all_exts), 0) - for r in all_exts: - self.assertGreater(len(r.summary), 0) diff --git a/novaclient/tests/unit/v2/test_migrations.py b/novaclient/tests/unit/v2/test_migrations.py index 408909cda..0b5ebb84e 100644 --- a/novaclient/tests/unit/v2/test_migrations.py +++ b/novaclient/tests/unit/v2/test_migrations.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import mock - from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -30,43 +28,140 @@ def test_list_migrations(self): for m in ml: self.assertIsInstance(m, migrations.Migration) self.assertRaises(AttributeError, getattr, m, "migration_type") - - def test_list_migrations_v223(self): - cs = fakes.FakeClient(api_versions.APIVersion("2.23")) - ml = cs.migrations.list() - self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) - cs.assert_called('GET', '/os-migrations') - for m in ml: - self.assertIsInstance(m, migrations.Migration) - self.assertEqual(m.migration_type, 'live-migration') - - @mock.patch('novaclient.v2.migrations.warnings.warn') - def test_list_migrations_with_cell_name(self, mock_warn): - ml = self.cs.migrations.list(cell_name="abc") - self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) - self.cs.assert_called('GET', '/os-migrations?cell_name=abc') - for m in ml: - self.assertIsInstance(m, migrations.Migration) - self.assertTrue(mock_warn.called) + self.assertRaises(AttributeError, getattr, m, "uuid") def test_list_migrations_with_filters(self): - ml = self.cs.migrations.list('host1', 'finished', 'child1') + ml = self.cs.migrations.list('host1', 'finished') self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called( 'GET', - '/os-migrations?cell_name=child1&host=host1&status=finished') + '/os-migrations?host=host1&status=finished') for m in ml: self.assertIsInstance(m, migrations.Migration) def test_list_migrations_with_instance_uuid_filter(self): - ml = self.cs.migrations.list('host1', 'finished', 'child1', - 'instance_id_456') + ml = self.cs.migrations.list('host1', 'finished', 'instance_id_456') self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) self.cs.assert_called( 'GET', - ('/os-migrations?cell_name=child1&host=host1&' + ('/os-migrations?host=host1&' 'instance_uuid=instance_id_456&status=finished')) self.assertEqual(1, len(ml)) self.assertEqual('instance_id_456', ml[0].instance_uuid) + + +class MigrationsV223Test(MigrationsTest): + def setUp(self): + super(MigrationsV223Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.23") + + def test_list_migrations(self): + ml = self.cs.migrations.list() + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-migrations') + for m in ml: + self.assertIsInstance(m, migrations.Migration) + self.assertEqual(m.migration_type, 'live-migration') + self.assertRaises(AttributeError, getattr, m, "uuid") + + +class MigrationsV259Test(MigrationsV223Test): + def setUp(self): + super(MigrationsV259Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.59") + + def test_list_migrations(self): + ml = self.cs.migrations.list() + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-migrations') + for m in ml: + self.assertIsInstance(m, migrations.Migration) + self.assertEqual(m.migration_type, 'live-migration') + self.assertTrue(hasattr(m, 'uuid')) + + def test_list_migrations_with_limit_marker_params(self): + marker = '12140183-c814-4ddf-8453-6df43028aa67' + params = {'limit': 10, + 'marker': marker, + 'changes_since': '2012-02-29T06:23:22'} + + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', + '/os-migrations?' + 'changes-since=%s&limit=10&marker=%s' + % ('2012-02-29T06%3A23%3A22', marker)) + for m in ms: + self.assertIsInstance(m, migrations.Migration) + + +class MigrationsV266Test(MigrationsV259Test): + def setUp(self): + super(MigrationsV266Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.66") + + def test_list_migrations_with_changes_before(self): + params = {'changes_before': '2012-02-29T06:23:22'} + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', + '/os-migrations?' + 'changes-before=%s' % + '2012-02-29T06%3A23%3A22') + for m in ms: + self.assertIsInstance(m, migrations.Migration) + + +class MigrationsV280Test(MigrationsV266Test): + def setUp(self): + super(MigrationsV280Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.80") + + def test_list_migrations_with_user_id(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + params = {'user_id': user_id} + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-migrations?user_id=%s' % user_id) + for m in ms: + self.assertIsInstance(m, migrations.Migration) + + def test_list_migrations_with_project_id(self): + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + params = {'project_id': project_id} + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-migrations?project_id=%s' + % project_id) + for m in ms: + self.assertIsInstance(m, migrations.Migration) + + def test_list_migrations_with_user_and_project_id(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + params = {'user_id': user_id, 'project_id': project_id} + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', + '/os-migrations?project_id=%s&user_id=%s' + % (project_id, user_id)) + for m in ms: + self.assertIsInstance(m, migrations.Migration) + + def test_list_migrations_with_user_id_pre_v280(self): + self.cs.api_version = api_versions.APIVersion('2.79') + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + ex = self.assertRaises(TypeError, + self.cs.migrations.list, + user_id=user_id) + self.assertIn("unexpected keyword argument 'user_id'", str(ex)) + + def test_list_migrations_with_project_id_pre_v280(self): + self.cs.api_version = api_versions.APIVersion('2.79') + project_id = '23cc0930d27c4be0acc14d7c47a3e1f7' + ex = self.assertRaises(TypeError, + self.cs.migrations.list, + project_id=project_id) + self.assertIn("unexpected keyword argument 'project_id'", str(ex)) diff --git a/novaclient/tests/unit/v2/test_quota_classes.py b/novaclient/tests/unit/v2/test_quota_classes.py index e9d9ad75a..3becb6323 100644 --- a/novaclient/tests/unit/v2/test_quota_classes.py +++ b/novaclient/tests/unit/v2/test_quota_classes.py @@ -49,17 +49,20 @@ def test_refresh_quota(self): class QuotaClassSetsTest2_50(QuotaClassSetsTest): """Tests the quota classes API binding using the 2.50 microversion.""" + api_version = '2.50' + invalid_resources = ['floating_ips', 'fixed_ips', 'networks', + 'security_groups', 'security_group_rules'] + def setUp(self): super(QuotaClassSetsTest2_50, self).setUp() - self.cs = fakes.FakeClient(api_versions.APIVersion("2.50")) + self.cs = fakes.FakeClient(api_versions.APIVersion(self.api_version)) def test_class_quotas_get(self): """Tests that network-related resources aren't in a 2.50 response and server group related resources are in the response. """ q = super(QuotaClassSetsTest2_50, self).test_class_quotas_get() - for invalid_resource in ('floating_ips', 'fixed_ips', 'networks', - 'security_groups', 'security_group_rules'): + for invalid_resource in self.invalid_resources: self.assertFalse(hasattr(q, invalid_resource), '%s should not be in %s' % (invalid_resource, q)) # Also make sure server_groups and server_group_members are in the @@ -73,8 +76,7 @@ def test_update_quota(self): and server group related resources are in the response. """ q = super(QuotaClassSetsTest2_50, self).test_update_quota() - for invalid_resource in ('floating_ips', 'fixed_ips', 'networks', - 'security_groups', 'security_group_rules'): + for invalid_resource in self.invalid_resources: self.assertFalse(hasattr(q, invalid_resource), '%s should not be in %s' % (invalid_resource, q)) # Also make sure server_groups and server_group_members are in the @@ -95,3 +97,27 @@ def test_update_quota_invalid_resources(self): self.assertRaises(TypeError, q.update, security_groups=1) self.assertRaises(TypeError, q.update, security_group_rules=1) self.assertRaises(TypeError, q.update, networks=1) + return q + + +class QuotaClassSetsTest2_57(QuotaClassSetsTest2_50): + """Tests the quota classes API binding using the 2.57 microversion.""" + api_version = '2.57' + + def setUp(self): + super(QuotaClassSetsTest2_57, self).setUp() + self.invalid_resources.extend(['injected_files', + 'injected_file_content_bytes', + 'injected_file_path_bytes']) + + def test_update_quota_invalid_resources(self): + """Tests trying to update quota class values for invalid resources. + + This will fail with TypeError because the file-related resource + kwargs aren't defined. + """ + q = super( + QuotaClassSetsTest2_57, self).test_update_quota_invalid_resources() + self.assertRaises(TypeError, q.update, injected_files=1) + self.assertRaises(TypeError, q.update, injected_file_content_bytes=1) + self.assertRaises(TypeError, q.update, injected_file_path_bytes=1) diff --git a/novaclient/tests/unit/v2/test_quotas.py b/novaclient/tests/unit/v2/test_quotas.py index 0ed766a03..67a0bc3df 100644 --- a/novaclient/tests/unit/v2/test_quotas.py +++ b/novaclient/tests/unit/v2/test_quotas.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import quotas as data from novaclient.tests.unit import utils @@ -29,6 +30,7 @@ def test_tenant_quotas_get(self): q = self.cs.quotas.get(tenant_id) self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/os-quota-sets/%s' % tenant_id) + return q def test_user_quotas_get(self): tenant_id = 'test' @@ -65,6 +67,7 @@ def test_force_update_quota(self): self.assert_called( 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'force': True, 'cores': 2}}) + return q def test_quotas_delete(self): tenant_id = 'test' @@ -79,3 +82,40 @@ def test_user_quotas_delete(self): self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) self.assert_called('DELETE', url) + + +class QuotaSetsTest2_57(QuotaSetsTest): + """Tests the quotas API binding using the 2.57 microversion.""" + data_fixture_class = data.V2_57 + invalid_resources = ['floating_ips', 'fixed_ips', 'networks', + 'security_groups', 'security_group_rules', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes'] + + def setUp(self): + super(QuotaSetsTest2_57, self).setUp() + self.cs.api_version = api_versions.APIVersion('2.57') + + def test_tenant_quotas_get(self): + q = super(QuotaSetsTest2_57, self).test_tenant_quotas_get() + for invalid_resource in self.invalid_resources: + self.assertFalse(hasattr(q, invalid_resource), + '%s should not be in %s' % (invalid_resource, q)) + + def test_force_update_quota(self): + q = super(QuotaSetsTest2_57, self).test_force_update_quota() + for invalid_resource in self.invalid_resources: + self.assertFalse(hasattr(q, invalid_resource), + '%s should not be in %s' % (invalid_resource, q)) + + def test_update_quota_invalid_resources(self): + """Tests trying to update quota values for invalid resources.""" + q = self.cs.quotas.get('test') + self.assertRaises(TypeError, q.update, floating_ips=1) + self.assertRaises(TypeError, q.update, fixed_ips=1) + self.assertRaises(TypeError, q.update, security_groups=1) + self.assertRaises(TypeError, q.update, security_group_rules=1) + self.assertRaises(TypeError, q.update, networks=1) + self.assertRaises(TypeError, q.update, injected_files=1) + self.assertRaises(TypeError, q.update, injected_file_content_bytes=1) + self.assertRaises(TypeError, q.update, injected_file_path_bytes=1) diff --git a/novaclient/tests/unit/v2/test_server_groups.py b/novaclient/tests/unit/v2/test_server_groups.py index 9881b20aa..40af1a13e 100644 --- a/novaclient/tests/unit/v2/test_server_groups.py +++ b/novaclient/tests/unit/v2/test_server_groups.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import server_groups as data @@ -106,3 +107,36 @@ def test_find_no_existing_server_groups_by_name(self): self.cs.server_groups.find, **kwargs) self.assert_called('GET', '/os-server-groups') + + +class ServerGroupsTestV264(ServerGroupsTest): + def setUp(self): + super(ServerGroupsTestV264, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.64") + + def test_create_server_group(self): + name = 'ig1' + policy = 'anti-affinity' + server_group = self.cs.server_groups.create(name, policy) + self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST) + body = {'server_group': {'name': name, 'policy': policy}} + self.assert_called('POST', '/os-server-groups', body) + self.assertIsInstance(server_group, + server_groups.ServerGroup) + + def test_create_server_group_with_rules(self): + kwargs = {'name': 'ig1', + 'policy': 'anti-affinity', + 'rules': {'max_server_per_host': 3}} + server_group = self.cs.server_groups.create(**kwargs) + self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST) + body = { + 'server_group': { + 'name': 'ig1', + 'policy': 'anti-affinity', + 'rules': {'max_server_per_host': 3} + } + } + self.assert_called('POST', '/os-server-groups', body) + self.assertIsInstance(server_group, + server_groups.ServerGroup) diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index 863368574..36eded7a8 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -13,14 +13,12 @@ # under the License. import base64 +import io import os import tempfile - -import mock -import six +from unittest import mock from novaclient import api_versions -from novaclient import base from novaclient import exceptions from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit.fixture_data import floatingips @@ -30,33 +28,18 @@ from novaclient.v2 import servers -class _FloatingIPManager(base.Manager): - resource_class = base.Resource - - @api_versions.deprecated_after('2.35') - def list(self): - """DEPRECATED: List floating IPs""" - return self._list("/os-floating-ips", "floating_ips") - - @api_versions.deprecated_after('2.35') - def get(self, floating_ip): - """DEPRECATED: Retrieve a floating IP""" - return self._get("/os-floating-ips/%s" % base.getid(floating_ip), - "floating_ip") - - class ServersTest(utils.FixturedTestCase): client_fixture_class = client.V1 data_fixture_class = data.V1 api_version = None + supports_files = True def setUp(self): super(ServersTest, self).setUp() self.useFixture(floatingips.FloatingFixture(self.requests_mock)) if self.api_version: self.cs.api_version = api_versions.APIVersion(self.api_version) - self.floating_ips = _FloatingIPManager(self.cs) def _get_server_create_default_nics(self): """Callback for default nics kwarg when creating a server. @@ -71,7 +54,7 @@ def test_list_servers(self): self.assertIsInstance(s, servers.Server) def test_filter_servers_unicode(self): - sl = self.cs.servers.list(search_opts={'name': u't€sting'}) + sl = self.cs.servers.list(search_opts={'name': 't€sting'}) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/detail?name=t%E2%82%ACsting') for s in sl: @@ -92,6 +75,22 @@ def test_list_all_servers(self): for s in sl: self.assertIsInstance(s, servers.Server) + def test_filter_servers_unlocked(self): + # calling the cs.servers.list python binding + # will fail before 2.73 microversion. + e = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.list, + search_opts={'locked': False}) + self.assertIn("'locked' argument is only allowed since " + "microversion 2.73.", str(e)) + + def test_filter_without_config_drive(self): + sl = self.cs.servers.list(search_opts={'config_drive': None}) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/detail') + for s in sl: + self.assertIsInstance(s, servers.Server) + def test_list_servers_undetailed(self): sl = self.cs.servers.list(detailed=False) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) @@ -143,6 +142,12 @@ def test_get_server_promote_details(self): self.assertEqual(s1._info, s2._info) def test_create_server(self): + kwargs = {} + if self.supports_files: + kwargs['files'] = { + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': io.StringIO('data'), # a stream + } s = self.cs.servers.create( name="My server", image=1, @@ -150,11 +155,8 @@ def test_create_server(self): meta={'foo': 'bar'}, userdata="hello moto", key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, - nics=self._get_server_create_default_nics() + nics=self._get_server_create_default_nics(), + **kwargs ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -188,7 +190,7 @@ def test_create_server_from_volume(): nics=nics ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-volumes_boot') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) test_create_server_from_volume() @@ -217,7 +219,7 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): nics=self._get_server_create_default_nics() ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-volumes_boot') + self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_boot_with_nics_ipv6(self): @@ -270,41 +272,53 @@ def wrapped_boot(url, key, *boot_args, **boot_kwargs): self.assertIsInstance(s, servers.Server) def test_create_server_userdata_file_object(self): + kwargs = {} + if self.supports_files: + kwargs['files'] = { + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': io.StringIO('data'), # a stream + } s = self.cs.servers.create( name="My server", image=1, flavor=1, meta={'foo': 'bar'}, - userdata=six.StringIO('hello moto'), - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, + userdata=io.StringIO('hello moto'), nics=self._get_server_create_default_nics(), + **kwargs ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_userdata_unicode(self): + kwargs = {} + if self.supports_files: + kwargs['files'] = { + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': io.StringIO('data'), # a stream + } s = self.cs.servers.create( name="My server", image=1, flavor=1, meta={'foo': 'bar'}, - userdata=six.u('こんにちは'), + userdata='こんにちは', key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, nics=self._get_server_create_default_nics(), + **kwargs ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) def test_create_server_userdata_utf8(self): + kwargs = {} + if self.supports_files: + kwargs['files'] = { + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': io.StringIO('data'), # a stream + } s = self.cs.servers.create( name="My server", image=1, @@ -312,11 +326,8 @@ def test_create_server_userdata_utf8(self): meta={'foo': 'bar'}, userdata='こんにちは', key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, nics=self._get_server_create_default_nics(), + **kwargs ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -340,6 +351,12 @@ def test_create_server_admin_pass(self): self.assertEqual(test_password, body['server']['adminPass']) def test_create_server_userdata_bin(self): + kwargs = {} + if self.supports_files: + kwargs['files'] = { + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': io.StringIO('data'), # a stream + } with tempfile.TemporaryFile(mode='wb+') as bin_file: original_data = os.urandom(1024) bin_file.write(original_data) @@ -352,11 +369,8 @@ def test_create_server_userdata_bin(self): meta={'foo': 'bar'}, userdata=bin_file, key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': six.StringIO('data'), # a stream - }, nics=self._get_server_create_default_nics(), + **kwargs ) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers') @@ -449,16 +463,40 @@ def test_set_server_meta_item(self): self.assert_called('PUT', '/servers/1234/metadata/test_key', {'meta': {'test_key': 'test_value'}}) + def test_get_server_meta(self): + m = self.cs.servers.get_meta(1234, 'Server Label') + self.assert_request_id(m, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234/metadata/Server%20Label') + self.assertEqual(m, {'meta': { + 'Server Label': 'Web Head 1' + }}) + + def test_list_server_meta(self): + m = self.cs.servers.list_meta(1234) + self.assert_request_id(m, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234/metadata') + self.assertEqual(m, {'metadata': { + 'Server Label': 'Web Head 1', + 'Image Version': '2.1' + }}) + def test_find(self): server = self.cs.servers.find(name='sample-server') self.assert_request_id(server, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('GET', '/servers/1234') self.assertEqual('sample-server', server.name) + # The networks should be sorted. + networks = server.networks + self.assertEqual(2, len(networks)) + labels = list(networks) # returns the dict keys + self.assertEqual('private', labels[0]) + self.assertEqual('public', labels[1]) self.assertRaises(exceptions.NoUniqueMatch, self.cs.servers.find, - flavor={"id": 1, "name": "256 MB Server"}) + flavor={"id": 1, "name": "256 MiB Server"}) - sl = self.cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) + sl = self.cs.servers.findall(flavor={"id": 1, + "name": "256 MiB Server"}) self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) self.assertEqual([1234, 5678, 9012], [s.id for s in sl]) @@ -567,81 +605,6 @@ def test_migrate_server(self): self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/action') - @mock.patch('warnings.warn') - def test_add_fixed_ip(self, mock_warn): - s = self.cs.servers.get(1234) - fip = s.add_fixed_ip(1) - mock_warn.assert_called_once() - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - fip = self.cs.servers.add_fixed_ip(s, 1) - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - - @mock.patch('warnings.warn') - def test_remove_fixed_ip(self, mock_warn): - s = self.cs.servers.get(1234) - ret = s.remove_fixed_ip('10.0.0.1') - mock_warn.assert_called_once() - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - ret = self.cs.servers.remove_fixed_ip(s, '10.0.0.1') - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - - @mock.patch('warnings.warn') - def test_add_floating_ip(self, mock_warn): - s = self.cs.servers.get(1234) - fip = s.add_floating_ip('11.0.0.1') - mock_warn.assert_called_once() - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - fip = self.cs.servers.add_floating_ip(s, '11.0.0.1') - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - f = self.floating_ips.list()[0] - fip = self.cs.servers.add_floating_ip(s, f) - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - fip = s.add_floating_ip(f) - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - - def test_add_floating_ip_to_fixed(self): - s = self.cs.servers.get(1234) - fip = s.add_floating_ip('11.0.0.1', fixed_address='12.0.0.1') - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - fip = self.cs.servers.add_floating_ip(s, '11.0.0.1', - fixed_address='12.0.0.1') - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - f = self.floating_ips.list()[0] - fip = self.cs.servers.add_floating_ip(s, f) - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - fip = s.add_floating_ip(f) - self.assert_request_id(fip, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - - @mock.patch('warnings.warn') - def test_remove_floating_ip(self, mock_warn): - s = self.cs.servers.get(1234) - ret = s.remove_floating_ip('11.0.0.1') - mock_warn.assert_called_once() - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - ret = self.cs.servers.remove_floating_ip(s, '11.0.0.1') - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - f = self.floating_ips.list()[0] - ret = self.cs.servers.remove_floating_ip(s, f) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - ret = s.remove_floating_ip(f) - self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/servers/1234/action') - def test_stop(self): s = self.cs.servers.get(1234) ret = s.stop() @@ -791,7 +754,7 @@ def test_get_password(self): s = self.cs.servers.get(1234) password = s.get_password('novaclient/tests/unit/idfake.pem') self.assert_request_id(password, fakes.FAKE_REQUEST_ID_LIST) - self.assertEqual(b'FooBar123', password) + self.assertEqual('FooBar123', password) self.assert_called('GET', '/servers/1234/os-server-password') def test_get_password_without_key(self): @@ -1016,6 +979,7 @@ def test_interface_attach(self): ret = s.interface_attach(None, None, None) self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('POST', '/servers/1234/os-interface') + self.assertIsInstance(ret, servers.NetworkInterface) def test_interface_detach(self): s = self.cs.servers.get(1234) @@ -1384,7 +1348,7 @@ def test_create_server_boot_from_volume_tagged_bdm_v2(self): key_name="fakekey", block_device_mapping_v2=bdm) self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) - self.assert_called('POST', '/os-volumes_boot') + self.assert_called('POST', '/servers') def test_create_server_boot_from_volume_tagged_bdm_v2_pre232(self): self.cs.api_version = api_versions.APIVersion("2.31") @@ -1421,18 +1385,6 @@ def test_create_server_with_nics_auto(self): self.assert_called('POST', '/servers') self.assertIsInstance(s, servers.Server) - def test_add_floating_ip(self): - # self.floating_ips.list() is not available after 2.35 - pass - - def test_add_floating_ip_to_fixed(self): - # self.floating_ips.list() is not available after 2.35 - pass - - def test_remove_floating_ip(self): - # self.floating_ips.list() is not available after 2.35 - pass - class ServersCreateImageBackupV2_45Test(utils.FixturedTestCase): """Tests the 2.45 microversion for createImage and createBackup @@ -1496,6 +1448,7 @@ def test_interface_attach_with_tag(self): {'interfaceAttachment': {'port_id': '7f42712e-63fe-484c-a6df-30ae4867ff66', 'tag': 'test_tag'}}) + self.assertIsInstance(ret, servers.NetworkInterface) def test_add_fixed_ip(self): # novaclient.v2.servers.Server.add_fixed_ip() @@ -1550,3 +1503,605 @@ def test_create_server_with_tags_pre_252_fails(self): key_name="fakekey", nics=self._get_server_create_default_nics(), tags=['tag1', 'tag2']) + + +class ServersV254Test(ServersV252Test): + + api_version = "2.54" + + def test_rebuild_with_key_name(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image="1", key_name="test_keypair") + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': { + 'imageRef': '1', + 'key_name': 'test_keypair'}}) + + def test_rebuild_with_key_name_none(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image="1", key_name=None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': { + 'key_name': None, + 'imageRef': '1'}}) + + def test_rebuild_with_key_name_pre_254_fails(self): + self.cs.api_version = api_versions.APIVersion('2.53') + ex = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.rebuild, + '1234', fakes.FAKE_IMAGE_UUID_1, + key_name='test_keypair') + self.assertIn('key_name', str(ex.message)) + + +class ServersV256Test(ServersV254Test): + + api_version = "2.56" + + def test_migrate_server(self): + s = self.cs.servers.get(1234) + ret = s.migrate() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'migrate': {}}) + ret = s.migrate(host='target-host') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'migrate': {'host': 'target-host'}}) + + def test_migrate_server_pre_256_fails(self): + self.cs.api_version = api_versions.APIVersion('2.55') + s = self.cs.servers.get(1234) + ex = self.assertRaises(TypeError, + s.migrate, host='target-host') + self.assertIn('host', str(ex)) + + +class ServersV257Test(ServersV256Test): + """Tests the servers python API bindings with microversion 2.57 where + personality files are deprecated. + """ + api_version = "2.57" + supports_files = False + + def test_create_server_with_files_fails(self): + ex = self.assertRaises( + exceptions.UnsupportedAttribute, self.cs.servers.create, + name="My server", image=1, flavor=1, + files={ + '/etc/passwd': 'some data', # a file + '/tmp/foo.txt': io.StringIO('data'), # a stream + }, nics='auto') + self.assertIn('files', str(ex)) + + def test_rebuild_server_name_meta_files(self): + files = {'/etc/passwd': 'some data'} + s = self.cs.servers.get(1234) + ex = self.assertRaises( + exceptions.UnsupportedAttribute, s.rebuild, image=1, name='new', + meta={'foo': 'bar'}, files=files) + self.assertIn('files', str(ex)) + + +class ServersV263Test(ServersV257Test): + + api_version = "2.63" + + def test_create_server_with_trusted_image_certificates(self): + self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=self._get_server_create_default_nics(), + trusted_image_certificates=['id1', 'id2'], + ) + self.assert_called('POST', '/servers', + {'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'key_name': 'fakekey', + 'max_count': 1, + 'metadata': {'foo': 'bar'}, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'trusted_image_certificates': ['id1', 'id2'], + 'user_data': 'aGVsbG8gbW90bw==' + }} + ) + + def test_create_server_with_trusted_image_certificates_pre_263_fails(self): + self.cs.api_version = api_versions.APIVersion('2.62') + ex = self.assertRaises( + exceptions.UnsupportedAttribute, self.cs.servers.create, + name="My server", image=1, flavor=1, meta={'foo': 'bar'}, + userdata="hello moto", key_name="fakekey", + nics=self._get_server_create_default_nics(), + trusted_image_certificates=['id1', 'id2']) + self.assertIn('trusted_image_certificates', str(ex)) + + def test_rebuild_server_with_trusted_image_certificates(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image="1", trusted_image_certificates=['id1', 'id2']) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': { + 'imageRef': '1', + 'trusted_image_certificates': ['id1', 'id2']}}) + + def test_rebuild_server_with_trusted_image_certificates_none(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image="1", trusted_image_certificates=None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': { + 'imageRef': '1', + 'trusted_image_certificates': None}}) + + def test_rebuild_with_trusted_image_certificates_pre_263_fails(self): + self.cs.api_version = api_versions.APIVersion('2.62') + ex = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.rebuild, + '1234', fakes.FAKE_IMAGE_UUID_1, + trusted_image_certificates=['id1', 'id2']) + self.assertIn('trusted_image_certificates', str(ex)) + + +class ServersV267Test(ServersV263Test): + """Tests for creating a server with a block_device_mapping_v2 entry + using volume_type for microversion 2.67. + """ + api_version = '2.67' + + def test_create_server_boot_from_volume_with_volume_type(self): + bdm = [{"volume_size": 1, + "uuid": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": True, + "source_type": "snapshot", + "destination_type": "volume", + "boot_index": 0, + "volume_type": "rbd"}] + s = self.cs.servers.create( + name="bfv server", image='', flavor=1, + nics='auto', block_device_mapping_v2=bdm) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers', { + 'server': { + 'flavorRef': '1', + 'imageRef': '', + 'name': 'bfv server', + 'networks': 'auto', + 'block_device_mapping_v2': bdm, + 'min_count': 1, + 'max_count': 1, + }}) + + def test_create_server_boot_from_volume_with_volume_type_pre_267(self): + self.cs.api_version = api_versions.APIVersion('2.66') + bdm = [{"volume_size": 1, + "uuid": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": True, + "source_type": "snapshot", + "destination_type": "volume", + "boot_index": 0, + "volume_type": "rbd"}] + ex = self.assertRaises(ValueError, self.cs.servers.create, + name="bfv server", image='', flavor=1, + nics='none', block_device_mapping_v2=bdm) + self.assertIn("Block device volume_type is not supported before " + "microversion 2.67", str(ex)) + + +class ServersV268Test(ServersV267Test): + + api_version = "2.68" + + def test_evacuate(self): + s = self.cs.servers.get(1234) + ret = s.evacuate('fake_target_host') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'fake_target_host'}}) + + ret = self.cs.servers.evacuate(s, 'fake_target_host') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'fake_target_host'}}) + + ex = self.assertRaises(TypeError, self.cs.servers.evacuate, + 'fake_target_host', force=True) + self.assertIn('force', str(ex)) + + def test_live_migrate_server(self): + s = self.cs.servers.get(1234) + ret = s.live_migrate(host='hostname', block_migration='auto') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + + ret = self.cs.servers.live_migrate(s, host='hostname', + block_migration='auto') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + + ex = self.assertRaises(TypeError, self.cs.servers.live_migrate, + host='hostname', force=True) + self.assertIn('force', str(ex)) + + +class ServersV273Test(ServersV268Test): + + api_version = "2.73" + + def test_lock_server(self): + s = self.cs.servers.get(1234) + ret = s.lock() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'lock': None}) + ret = s.lock(reason='zombie-apocalypse') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'lock': {'locked_reason': 'zombie-apocalypse'}}) + + def test_lock_server_pre_273_fails_with_reason(self): + self.cs.api_version = api_versions.APIVersion('2.72') + s = self.cs.servers.get(1234) + e = self.assertRaises(TypeError, + s.lock, reason='blah') + self.assertIn("unexpected keyword argument 'reason'", str(e)) + + def test_filter_servers_unlocked(self): + # support locked=False + sl = self.cs.servers.list(search_opts={'locked': False}) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/detail?locked=False') + for s in sl: + self.assertIsInstance(s, servers.Server) + + +class ServersV274Test(ServersV273Test): + + api_version = "2.74" + + def test_create_server_with_host(self): + self.cs.servers.create( + name="My server", + image=1, + flavor=1, + nics="auto", + host="new-host" + ) + self.assert_called('POST', '/servers', + {'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'max_count': 1, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'host': 'new-host' + }} + ) + + def test_create_server_with_hypervisor_hostname(self): + self.cs.servers.create( + name="My server", + image=1, + flavor=1, + nics="auto", + hypervisor_hostname="new-host" + ) + self.assert_called('POST', '/servers', + {'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'max_count': 1, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'hypervisor_hostname': 'new-host' + }} + ) + + def test_create_server_with_host_and_hypervisor_hostname(self): + self.cs.servers.create( + name="My server", + image=1, + flavor=1, + nics="auto", + host="new-host", + hypervisor_hostname="new-host" + ) + self.assert_called('POST', '/servers', + {'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'max_count': 1, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'host': 'new-host', + 'hypervisor_hostname': 'new-host' + }} + ) + + def test_create_server_with_host_pre_274_fails(self): + self.cs.api_version = api_versions.APIVersion('2.73') + ex = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.create, + name="My server", image=1, flavor=1, + nics='auto', host="new-host") + self.assertIn("'host' argument is only allowed since microversion " + "2.74", str(ex)) + + def test_create_server_with_hypervisor_hostname_pre_274_fails(self): + self.cs.api_version = api_versions.APIVersion('2.73') + ex = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.create, + name="My server", image=1, flavor=1, + nics='auto', hypervisor_hostname="new-host") + self.assertIn("'hypervisor_hostname' argument is only allowed since " + "microversion 2.74", str(ex)) + + def test_create_server_with_host_and_hypervisor_hostname_pre_274_fails( + self): + self.cs.api_version = api_versions.APIVersion('2.73') + ex = self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.create, + name="My server", image=1, flavor=1, + nics='auto', host="new-host", + hypervisor_hostname="new-host") + self.assertIn("'host' argument is only allowed since microversion " + "2.74", str(ex)) + + +class ServersV277Test(ServersV274Test): + + api_version = "2.77" + + def test_unshelve(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': None}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': None}) + + def test_unshelve_with_az(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve(availability_zone='foo-az') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'unshelve': { + 'availability_zone': 'foo-az'}}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve(s, availability_zone='foo-az') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'unshelve': { + 'availability_zone': 'foo-az'}}) + + def test_unshelve_server_pre_277_fails_with_specified_az(self): + self.cs.api_version = api_versions.APIVersion('2.76') + s = self.cs.servers.get(1234) + # Test going through the Server object. + ex = self.assertRaises(TypeError, + s.unshelve, + availability_zone='foo-az') + self.assertIn("unexpected keyword argument 'availability_zone'", + str(ex)) + # Test going through the ServerManager directly. + ex = self.assertRaises(TypeError, + self.cs.servers.unshelve, + s, availability_zone='foo-az') + self.assertIn("unexpected keyword argument 'availability_zone'", + str(ex)) + + +class ServersV278Test(ServersV277Test): + + api_version = "2.78" + + def test_get_server_topology(self): + s = self.cs.servers.get(1234) + topology = s.topology() + self.assert_request_id(topology, fakes.FAKE_REQUEST_ID_LIST) + self.assertIsNotNone(topology) + self.assert_called('GET', '/servers/1234/topology') + + topology_from_manager = self.cs.servers.topology(1234) + self.assert_request_id(topology, fakes.FAKE_REQUEST_ID_LIST) + self.assertIsNotNone(topology_from_manager) + self.assert_called('GET', '/servers/1234/topology') + + self.assertEqual(topology, topology_from_manager) + + def test_get_server_topology_pre278(self): + self.cs.api_version = api_versions.APIVersion('2.77') + s = self.cs.servers.get(1234) + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, s.topology) + + +class ServersV290Test(ServersV278Test): + + api_version = '2.90' + + def test_create_server_with_hostname(self): + self.cs.servers.create( + name='My server', + image=1, + flavor=1, + nics='auto', + hostname='new-hostname', + ) + self.assert_called( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'imageRef': '1', + 'max_count': 1, + 'min_count': 1, + 'name': 'My server', + 'networks': 'auto', + 'hostname': 'new-hostname' + }, + } + ) + + def test_create_server_with_hostname_pre_290_fails(self): + self.cs.api_version = api_versions.APIVersion('2.89') + ex = self.assertRaises( + exceptions.UnsupportedAttribute, + self.cs.servers.create, + name='My server', + image=1, + flavor=1, + nics='auto', + hostname='new-hostname') + self.assertIn( + "'hostname' argument is only allowed since microversion 2.90", + str(ex)) + + def test_rebuild_server_with_hostname(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image="1", hostname='new-hostname') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', '/servers/1234/action', + { + 'rebuild': { + 'imageRef': '1', + 'hostname': 'new-hostname', + }, + }, + ) + + def test_rebuild_server_with_hostname_pre_290_fails(self): + self.cs.api_version = api_versions.APIVersion('2.89') + ex = self.assertRaises( + exceptions.UnsupportedAttribute, + self.cs.servers.rebuild, + '1234', fakes.FAKE_IMAGE_UUID_1, + hostname='new-hostname') + self.assertIn('hostname', str(ex)) + + def test_update_server_with_hostname(self): + s = self.cs.servers.get(1234) + + s.update(hostname='new-hostname') + self.assert_called( + 'PUT', '/servers/1234', + { + 'server': { + 'hostname': 'new-hostname', + }, + }, + ) + + def test_update_with_hostname_pre_290_fails(self): + self.cs.api_version = api_versions.APIVersion('2.89') + s = self.cs.servers.get(1234) + ex = self.assertRaises( + TypeError, + s.update, + hostname='new-hostname') + self.assertIn('hostname', str(ex)) + + +class ServersV291Test(ServersV290Test): + + api_version = "2.91" + + def test_unshelve_with_host(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve(host='server1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1'}}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve(s, host='server1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1'}}) + + def test_unshelve_server_with_az_and_host(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve(host='server1', availability_zone='foo-az') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1', + 'availability_zone': 'foo-az'}}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve( + s, host='server1', availability_zone='foo-az') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1', + 'availability_zone': 'foo-az'}}) + + def test_unshelve_unpin_az(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve(availability_zone=None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'availability_zone': None}}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve(s, availability_zone=None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'availability_zone': None}}) + + def test_unshelve_server_with_host_and_unpin(self): + s = self.cs.servers.get(1234) + # Test going through the Server object. + ret = s.unshelve(availability_zone=None, host='server1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1', + 'availability_zone': None}}) + # Test going through the ServerManager directly. + ret = self.cs.servers.unshelve( + s, availability_zone=None, host='server1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', + '/servers/1234/action', + {'unshelve': {'host': 'server1', + 'availability_zone': None}}) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 7b33d90fd..846faf9e1 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -18,14 +18,15 @@ import argparse import base64 +import builtins +import collections import datetime +import io import os +from unittest import mock import fixtures -import mock from oslo_utils import timeutils -import six -from six.moves import builtins import testtools import novaclient @@ -36,12 +37,18 @@ import novaclient.shell from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import servers import novaclient.v2.shell FAKE_UUID_1 = fakes.FAKE_IMAGE_UUID_1 FAKE_UUID_2 = fakes.FAKE_IMAGE_UUID_2 +# Converting dictionary to object +TestAbsoluteLimits = collections.namedtuple("TestAbsoluteLimits", + ["name", "value"]) + + class ShellFixture(fixtures.Fixture): def setUp(self): super(ShellFixture, self).setUp() @@ -64,7 +71,7 @@ class ShellTest(utils.TestCase): 'NOVA_PROJECT_ID': 'project_id', 'OS_COMPUTE_API_VERSION': '2', 'NOVA_URL': 'http://no.where', - 'OS_AUTH_URL': 'http://no.where/v2.0', + 'OS_AUTH_URL': 'http://no.where/v3', } def setUp(self): @@ -78,17 +85,27 @@ def setUp(self): self.useFixture(fixtures.MonkeyPatch( 'novaclient.client.Client', fakes.FakeClient)) - @mock.patch('sys.stdout', new_callable=six.StringIO) - @mock.patch('sys.stderr', new_callable=six.StringIO) - def run_command(self, cmd, mock_stderr, mock_stdout, api_version=None): + # TODO(stephenfin): We should migrate most of the existing assertRaises + # calls to simply pass expected_error to this instead so we can easily + # capture and compare output + @mock.patch('sys.stdout', new_callable=io.StringIO) + @mock.patch('sys.stderr', new_callable=io.StringIO) + def run_command(self, cmd, mock_stderr, mock_stdout, api_version=None, + expected_error=None): version_options = [] if api_version: version_options.extend(["--os-compute-api-version", api_version, "--service-type", "computev21"]) - if isinstance(cmd, list): - self.shell.main(version_options + cmd) + if not isinstance(cmd, list): + cmd = cmd.split() + + if expected_error: + self.assertRaises(expected_error, + self.shell.main, + version_options + cmd) else: - self.shell.main(version_options + cmd.split()) + self.shell.main(version_options + cmd) + return mock_stdout.getvalue(), mock_stderr.getvalue() def assert_called(self, method, url, body=None, **kwargs): @@ -97,15 +114,22 @@ def assert_called(self, method, url, body=None, **kwargs): def assert_called_anytime(self, method, url, body=None): return self.shell.cs.assert_called_anytime(method, url, body) + def assert_not_called(self, method, url, body=None): + return self.shell.cs.assert_not_called(method, url, body) + def test_agents_list_with_hypervisor(self): - self.run_command('agent-list --hypervisor xen') + _, err = self.run_command('agent-list --hypervisor xen') self.assert_called('GET', '/os-agents?hypervisor=xen') + self.assertIn( + 'This command has been deprecated since 23.0.0 Wallaby Release ' + 'and will be removed in the first major release ' + 'after the Nova server 24.0.0 X release.', err) def test_agents_create(self): - self.run_command('agent-create win x86 7.0 ' - '/xxx/xxx/xxx ' - 'add6bb58e139be103324d04d82d8f546 ' - 'kvm') + _, err = self.run_command('agent-create win x86 7.0 ' + '/xxx/xxx/xxx ' + 'add6bb58e139be103324d04d82d8f546 ' + 'kvm') self.assert_called( 'POST', '/os-agents', {'agent': { @@ -115,19 +139,31 @@ def test_agents_create(self): 'version': '7.0', 'url': '/xxx/xxx/xxx', 'md5hash': 'add6bb58e139be103324d04d82d8f546'}}) + self.assertIn( + 'This command has been deprecated since 23.0.0 Wallaby Release ' + 'and will be removed in the first major release ' + 'after the Nova server 24.0.0 X release.', err) def test_agents_delete(self): - self.run_command('agent-delete 1') + _, err = self.run_command('agent-delete 1') self.assert_called('DELETE', '/os-agents/1') + self.assertIn( + 'This command has been deprecated since 23.0.0 Wallaby Release ' + 'and will be removed in the first major release ' + 'after the Nova server 24.0.0 X release.', err) def test_agents_modify(self): - self.run_command('agent-modify 1 8.0 /yyy/yyyy/yyyy ' - 'add6bb58e139be103324d04d82d8f546') + _, err = self.run_command('agent-modify 1 8.0 /yyy/yyyy/yyyy ' + 'add6bb58e139be103324d04d82d8f546') self.assert_called('PUT', '/os-agents/1', {"para": { "url": "/yyy/yyyy/yyyy", "version": "8.0", "md5hash": "add6bb58e139be103324d04d82d8f546"}}) + self.assertIn( + 'This command has been deprecated since 23.0.0 Wallaby Release ' + 'and will be removed in the first major release ' + 'after the Nova server 24.0.0 X release.', err) def test_boot(self): self.run_command('boot --flavor 1 --image %s ' @@ -157,6 +193,11 @@ def test_boot_image_with(self): }}, ) + def test_boot_image_with_error_out_no_match(self): + cmd = ("boot --flavor 1" + " --image-with fake_key=fake_value some-server") + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_key(self): self.run_command('boot --flavor 1 --image %s --key-name 1 some-server' % FAKE_UUID_1) @@ -225,42 +266,42 @@ def test_boot_secgroup(self): }}, ) - def test_boot_config_drive(self): + def test_boot_access_ip(self): self.run_command( - 'boot --flavor 1 --image %s --config-drive 1 some-server' % - FAKE_UUID_1) + 'boot --flavor 1 --image %s --access-ip-v4 10.10.10.10 ' + '--access-ip-v6 ::1 some-server' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', 'imageRef': FAKE_UUID_1, - 'min_count': 1, + 'accessIPv4': '10.10.10.10', + 'accessIPv6': '::1', 'max_count': 1, - 'config_drive': True + 'min_count': 1 }}, ) - def test_boot_access_ip(self): + def test_boot_config_drive(self): self.run_command( - 'boot --flavor 1 --image %s --access-ip-v4 10.10.10.10 ' - '--access-ip-v6 ::1 some-server' % FAKE_UUID_1) + 'boot --flavor 1 --image %s --config-drive 1 some-server' % + FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', 'imageRef': FAKE_UUID_1, - 'accessIPv4': '10.10.10.10', - 'accessIPv6': '::1', + 'min_count': 1, 'max_count': 1, - 'min_count': 1 + 'config_drive': True }}, ) - def test_boot_config_drive_custom(self): + def test_boot_config_drive_false(self): self.run_command( - 'boot --flavor 1 --image %s --config-drive /dev/hda some-server' % + 'boot --flavor 1 --image %s --config-drive false some-server' % FAKE_UUID_1) self.assert_called_anytime( 'POST', '/servers', @@ -270,10 +311,17 @@ def test_boot_config_drive_custom(self): 'imageRef': FAKE_UUID_1, 'min_count': 1, 'max_count': 1, - 'config_drive': '/dev/hda' }}, ) + def test_boot_config_drive_invalid_value(self): + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'boot --flavor 1 --image %s --config-drive /dev/hda some-server' % + FAKE_UUID_1) + self.assertIn("The value of the '--config-drive' option must be " + "a boolean value.", str(ex)) + def test_boot_invalid_user_data(self): invalid_file = os.path.join(os.path.dirname(__file__), 'no_such_file') @@ -304,7 +352,7 @@ def test_boot_no_image_bdms(self): 'boot --flavor 1 --block-device-mapping vda=blah:::0 some-server' ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -328,7 +376,7 @@ def test_boot_image_bdms_v2(self): 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -371,7 +419,7 @@ def test_boot_image_bdms_v2_no_source_type_no_destination_type(self): 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -407,7 +455,7 @@ def test_boot_image_bdms_v2_no_destination_type(self): 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -451,7 +499,7 @@ def test_boot_image_bdms_v2_with_tag(self): api_version='2.32' ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -488,7 +536,7 @@ def test_boot_no_image_bdms_v2(self): 'type=disk,shutdown=preserve some-server' ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -515,7 +563,7 @@ def test_boot_no_image_bdms_v2(self): cmd = 'boot --flavor 1 --boot-volume fake-id some-server' self.run_command(cmd) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -537,7 +585,7 @@ def test_boot_no_image_bdms_v2(self): cmd = 'boot --flavor 1 --snapshot fake-id some-server' self.run_command(cmd) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -558,7 +606,7 @@ def test_boot_no_image_bdms_v2(self): self.run_command('boot --flavor 1 --swap 1 some-server') self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -582,7 +630,7 @@ def test_boot_no_image_bdms_v2(self): 'boot --flavor 1 --ephemeral size=1,format=ext4 some-server' ) self.assert_called_anytime( - 'POST', '/os-volumes_boot', + 'POST', '/servers', {'server': { 'flavorRef': '1', 'name': 'some-server', @@ -609,6 +657,106 @@ def test_boot_bdms_v2_invalid_shutdown_value(self): 'size=1,format=ext4,type=disk,shutdown=foobar ' 'some-server' % FAKE_UUID_1)) + def test_boot_from_volume_with_volume_type_latest_microversion(self): + self.run_command( + 'boot --flavor 1 --block-device id=%s,source=image,dest=volume,' + 'size=1,bootindex=0,shutdown=remove,tag=foo,volume_type=lvm ' + 'bfv-server' % FAKE_UUID_1, api_version='2.latest') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'bfv-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'volume', + 'volume_size': '1', + 'delete_on_termination': True, + 'tag': 'foo', + 'boot_index': '0', + 'volume_type': 'lvm' + }, + ], + 'networks': 'auto', + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}) + + def test_boot_from_volume_with_volume_type_old_microversion(self): + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'boot --flavor 1 --block-device id=%s,source=image,dest=volume,' + 'size=1,bootindex=0,shutdown=remove,tag=foo,volume_type=lvm ' + 'bfv-server' % FAKE_UUID_1, api_version='2.66') + self.assertIn("'volume_type' in block device mapping is not supported " + "in API version", str(ex)) + + def test_boot_from_volume_with_volume_type(self): + """Tests creating a volume-backed server from a source image and + specifying the type of volume to create with microversion 2.67. + """ + self.run_command( + 'boot --flavor 1 --block-device id=%s,source=image,dest=volume,' + 'size=1,bootindex=0,shutdown=remove,tag=foo,volume_type=lvm ' + 'bfv-server' % FAKE_UUID_1, api_version='2.67') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'bfv-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'volume', + 'volume_size': '1', + 'delete_on_termination': True, + 'tag': 'foo', + 'boot_index': '0', + 'volume_type': 'lvm' + }, + ], + 'networks': 'auto', + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}) + + def test_boot_from_volume_without_volume_type_2_67(self): + """Tests creating a volume-backed server from a source image but + without specifying the type of volume to create with microversion 2.67. + The volume_type parameter should be omitted in the request to the + API server. + """ + self.run_command( + 'boot --flavor 1 --block-device id=%s,source=image,dest=volume,' + 'size=1,bootindex=0,shutdown=remove,tag=foo bfv-server' % + FAKE_UUID_1, api_version='2.67') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'bfv-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'volume', + 'volume_size': '1', + 'delete_on_termination': True, + 'tag': 'foo', + 'boot_index': '0', + }, + ], + 'networks': 'auto', + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}) + def test_boot_metadata(self): self.run_command('boot --image %s --flavor 1 --meta foo=bar=pants' ' --meta spam=eggs some-server ' % FAKE_UUID_1) @@ -633,9 +781,12 @@ def test_boot_with_incorrect_metadata(self): self.assertEqual(expected, result.args[0]) def test_boot_hints(self): - self.run_command('boot --image %s --flavor 1 ' - '--hint a=b0=c0 --hint a=b1=c1 some-server ' % - FAKE_UUID_1) + cmd = ('boot --image %s --flavor 1 ' + '--hint same_host=a0cf03a5-d921-4877-bb5c-86d26cf818e1 ' + '--hint same_host=8c19174f-4220-44f0-824a-cd1eeef10287 ' + '--hint query=[>=,$free_ram_mb,1024] ' + 'some-server' % FAKE_UUID_1) + self.run_command(cmd) self.assert_called_anytime( 'POST', '/servers', { @@ -646,10 +797,25 @@ def test_boot_hints(self): 'min_count': 1, 'max_count': 1, }, - 'os:scheduler_hints': {'a': ['b0=c0', 'b1=c1']}, + 'os:scheduler_hints': { + 'same_host': [ + 'a0cf03a5-d921-4877-bb5c-86d26cf818e1', + '8c19174f-4220-44f0-824a-cd1eeef10287', + ], + 'query': '[>=,$free_ram_mb,1024]', + }, }, ) + def test_boot_hints_invalid(self): + cmd = ('boot --image %s --flavor 1 ' + '--hint a0cf03a5-d921-4877-bb5c-86d26cf818e1 ' + 'some-server' % FAKE_UUID_1) + _, err = self.run_command(cmd, expected_error=SystemExit) + self.assertIn("'a0cf03a5-d921-4877-bb5c-86d26cf818e1' is not in " + "the format of 'key=value'", + err) + def test_boot_nic_auto_not_alone_after(self): cmd = ('boot --image %s --flavor 1 ' '--nic auto,tag=foo some-server' % @@ -745,7 +911,7 @@ def test_boot_invalid_nics_pre_v2_32(self): '--nic net-id=1,port-id=2 some-server' % FAKE_UUID_1) ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, api_version='2.1') - self.assertNotIn('tag=tag', six.text_type(ex)) + self.assertNotIn('tag=tag', str(ex)) def test_boot_invalid_nics_v2_32(self): # This is a negative test to make sure we fail with the correct message @@ -753,7 +919,7 @@ def test_boot_invalid_nics_v2_32(self): '--nic net-id=1,port-id=2 some-server' % FAKE_UUID_1) ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, api_version='2.32') - self.assertIn('tag=tag', six.text_type(ex)) + self.assertIn('tag=tag', str(ex)) def test_boot_invalid_nics_v2_36_auto(self): """This is a negative test to make sure we fail with the correct @@ -762,7 +928,7 @@ def test_boot_invalid_nics_v2_36_auto(self): cmd = ('boot --image %s --flavor 1 --nic auto test' % FAKE_UUID_1) ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, api_version='2.36') - self.assertNotIn('auto,none', six.text_type(ex)) + self.assertNotIn('auto,none', str(ex)) def test_boot_invalid_nics_v2_37(self): """This is a negative test to make sure we fail with the correct @@ -772,7 +938,7 @@ def test_boot_invalid_nics_v2_37(self): '--nic net-id=1 --nic auto some-server' % FAKE_UUID_1) ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, api_version='2.37') - self.assertIn('auto,none', six.text_type(ex)) + self.assertIn('auto,none', str(ex)) def test_boot_nics_auto_allocate_default(self): """Tests that if microversion>=2.37 is specified and no --nics are @@ -912,7 +1078,7 @@ def test_boot_nics_net_name_neutron_blank(self): cmd = ('boot --image %s --flavor 1 ' '--nic net-name=blank some-server' % FAKE_UUID_1) # this should raise a multiple matches error - msg = 'No Network matching blank\..*' + msg = 'No Network matching blank\\..*' with testtools.ExpectedException(exceptions.CommandError, msg): self.run_command(cmd) @@ -966,6 +1132,16 @@ def test_boot_invalid_files(self): ' --file /foo=%s' % (FAKE_UUID_1, invalid_file)) self.assertRaises(exceptions.CommandError, self.run_command, cmd) + def test_boot_files_2_57(self): + """Tests that trying to run the boot command with the --file option + after microversion 2.56 fails. + """ + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + cmd = ('boot some-server --flavor 1 --image %s' + ' --file /tmp/foo=%s') + self.assertRaises(SystemExit, self.run_command, + cmd % (FAKE_UUID_1, testfile), api_version='2.57') + def test_boot_max_min_count(self): self.run_command('boot --image %s --flavor 1 --min-count 1' ' --max-count 3 server' % FAKE_UUID_1) @@ -1068,10 +1244,10 @@ def test_boot_with_poll_to_check_VM_state_error(self): def test_boot_named_flavor(self): self.run_command(["boot", "--image", FAKE_UUID_1, - "--flavor", "512 MB Server", + "--flavor", "512 MiB Server", "--max-count", "3", "server"]) self.assert_called('GET', '/v2/images/' + FAKE_UUID_1, pos=0) - self.assert_called('GET', '/flavors/512 MB Server', pos=1) + self.assert_called('GET', '/flavors/512 MiB Server', pos=1) self.assert_called('GET', '/flavors?is_public=None', pos=2) self.assert_called('GET', '/flavors/2', pos=3) self.assert_called( @@ -1130,15 +1306,256 @@ def test_boot_with_tags_pre_v2_52(self): self.assertRaises(SystemExit, self.run_command, cmd, api_version='2.51') + def test_boot_with_single_trusted_image_certificates(self): + self.run_command('boot --flavor 1 --image %s --nic auto some-server ' + '--trusted-image-certificate-id id1' + % FAKE_UUID_1, api_version='2.63') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'trusted_image_certificates': ['id1'] + }}, + ) + + def test_boot_with_multiple_trusted_image_certificates(self): + self.run_command('boot --flavor 1 --image %s --nic auto some-server ' + '--trusted-image-certificate-id id1 ' + '--trusted-image-certificate-id id2' + % FAKE_UUID_1, api_version='2.63') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'trusted_image_certificates': ['id1', 'id2'] + }}, + ) + + def test_boot_with_trusted_image_certificates_envar(self): + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'var_id1,var_id2')) + self.run_command('boot --flavor 1 --image %s --nic auto some-server' + % FAKE_UUID_1, api_version='2.63') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'trusted_image_certificates': ['var_id1', 'var_id2'] + }}, + ) + + def test_boot_without_trusted_image_certificates_v263(self): + self.run_command('boot --flavor 1 --image %s --nic auto some-server' + % FAKE_UUID_1, api_version='2.63') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + }}, + ) + + def test_boot_with_trusted_image_certificates_pre_v263(self): + cmd = ('boot --flavor 1 --image %s some-server ' + '--trusted-image-certificate-id id1 ' + '--trusted-image-certificate-id id2' % FAKE_UUID_1) + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.62') + + # OS_TRUSTED_IMAGE_CERTIFICATE_IDS environment variable is not supported in + # microversions < 2.63 (should result in an UnsupportedAttribute exception) + def test_boot_with_trusted_image_certificates_envar_pre_v263(self): + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'var_id1,var_id2')) + cmd = ('boot --flavor 1 --image %s --nic auto some-server ' + % FAKE_UUID_1) + self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, + cmd, api_version='2.62') + + def test_boot_with_trusted_image_certificates_arg_and_envvar(self): + """Tests that if both the environment variable and argument are + specified, the argument takes precedence. + """ + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'cert1')) + self.run_command('boot --flavor 1 --image %s --nic auto ' + '--trusted-image-certificate-id cert2 some-server' + % FAKE_UUID_1, api_version='2.63') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'trusted_image_certificates': ['cert2'] + }}, + ) + + @mock.patch.object(servers.Server, 'networks', + new_callable=mock.PropertyMock) + def test_boot_with_not_found_when_accessing_addresses_attribute( + self, mock_networks): + mock_networks.side_effect = exceptions.NotFound( + 404, 'Instance %s could not be found.' % FAKE_UUID_1) + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'boot --flavor 1 --image %s some-server' % FAKE_UUID_2) + self.assertIn('Instance %s could not be found.' % FAKE_UUID_1, + str(ex)) + + def test_boot_with_host_v274(self): + self.run_command('boot --flavor 1 --image %s ' + '--host new-host --nic auto ' + 'some-server' % FAKE_UUID_1, + api_version='2.74') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'host': 'new-host', + }}, + ) + + def test_boot_with_hypervisor_hostname_v274(self): + self.run_command('boot --flavor 1 --image %s --nic auto ' + '--hypervisor-hostname new-host ' + 'some-server' % FAKE_UUID_1, + api_version='2.74') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'hypervisor_hostname': 'new-host', + }}, + ) + + def test_boot_with_host_and_hypervisor_hostname_v274(self): + self.run_command('boot --flavor 1 --image %s ' + '--host new-host --nic auto ' + '--hypervisor-hostname new-host ' + 'some-server' % FAKE_UUID_1, + api_version='2.74') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'host': 'new-host', + 'hypervisor_hostname': 'new-host', + }}, + ) + + def test_boot_with_host_pre_v274(self): + cmd = ('boot --flavor 1 --image %s --nic auto ' + '--host new-host some-server' + % FAKE_UUID_1) + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.73') + + def test_boot_with_hypervisor_hostname_pre_v274(self): + cmd = ('boot --flavor 1 --image %s --nic auto ' + '--hypervisor-hostname new-host some-server' + % FAKE_UUID_1) + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.73') + + def test_boot_with_host_and_hypervisor_hostname_pre_v274(self): + cmd = ('boot --flavor 1 --image %s --nic auto ' + '--host new-host --hypervisor-hostname new-host some-server' + % FAKE_UUID_1) + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.73') + + def test_boot_with_hostname(self): + self.run_command( + 'boot --flavor 1 --image %s ' + '--hostname my-hostname --nic auto ' + 'some-server' % FAKE_UUID_1, + api_version='2.90') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'hostname': 'my-hostname', + }}, + ) + + def test_boot_with_hostname_pre_v290(self): + cmd = ( + 'boot --flavor 1 --image %s --nic auto ' + '--hostname my-hostname some-server' % FAKE_UUID_1 + ) + self.assertRaises( + SystemExit, self.run_command, + cmd, api_version='2.89') + def test_flavor_list(self): - self.run_command('flavor-list') + out, _ = self.run_command('flavor-list') + self.assert_called_anytime('GET', '/flavors/detail') + self.assertNotIn('Description', out) + + def test_flavor_list_with_description(self): + """Tests that the description column is added for version >= 2.55.""" + out, _ = self.run_command('flavor-list', api_version='2.55') self.assert_called_anytime('GET', '/flavors/detail') + self.assertIn('Description', out) def test_flavor_list_with_extra_specs(self): self.run_command('flavor-list --extra-specs') self.assert_called('GET', '/flavors/aa1/os-extra_specs') self.assert_called_anytime('GET', '/flavors/detail') + def test_flavor_list_with_extra_specs_2_61_or_later(self): + """Tests that the 'os-extra_specs' API is not called + when the '--extra-specs' option is specified since microversion 2.61. + """ + out, _ = self.run_command('flavor-list --extra-specs', + api_version='2.61') + self.assert_not_called('GET', '/flavors/aa1/os-extra_specs') + self.assert_called_anytime('GET', '/flavors/detail') + self.assertIn('extra_specs', out) + def test_flavor_list_with_all(self): self.run_command('flavor-list --all') self.assert_called('GET', '/flavors/detail?is_public=None') @@ -1160,23 +1577,38 @@ def test_flavor_list_with_sort_key_dir(self): self.assert_called('GET', '/flavors/detail?sort_dir=asc&sort_key=id') def test_flavor_show(self): - self.run_command('flavor-show 1') + out, _ = self.run_command('flavor-show 1') self.assert_called_anytime('GET', '/flavors/1') + self.assertNotIn('description', out) + + def test_flavor_show_with_description(self): + """Tests that the description is shown in version >= 2.55.""" + out, _ = self.run_command('flavor-show 1', api_version='2.55') + self.assert_called('GET', '/flavors/1', pos=-2) + self.assert_called('GET', '/flavors/1/os-extra_specs', pos=-1) + self.assertIn('description', out) + + def test_flavor_show_2_61_or_later(self): + """Tests that the 'os-extra_specs' is not called in version >= 2.61.""" + out, _ = self.run_command('flavor-show 1', api_version='2.61') + self.assert_not_called('GET', '/flavors/1/os-extra_specs') + self.assert_called_anytime('GET', '/flavors/1') + self.assertIn('extra_specs', out) def test_flavor_show_with_alphanum_id(self): self.run_command('flavor-show aa1') self.assert_called_anytime('GET', '/flavors/aa1') def test_flavor_show_by_name(self): - self.run_command(['flavor-show', '128 MB Server']) - self.assert_called('GET', '/flavors/128 MB Server', pos=0) + self.run_command(['flavor-show', '128 MiB Server']) + self.assert_called('GET', '/flavors/128 MiB Server', pos=0) self.assert_called('GET', '/flavors?is_public=None', pos=1) self.assert_called('GET', '/flavors/aa1', pos=2) self.assert_called('GET', '/flavors/aa1/os-extra_specs', pos=3) def test_flavor_show_by_name_priv(self): - self.run_command(['flavor-show', '512 MB Server']) - self.assert_called('GET', '/flavors/512 MB Server', pos=0) + self.run_command(['flavor-show', '512 MiB Server']) + self.assert_called('GET', '/flavors/512 MiB Server', pos=0) self.assert_called('GET', '/flavors?is_public=None', pos=1) self.assert_called('GET', '/flavors/2', pos=2) self.assert_called('GET', '/flavors/2/os-extra_specs', pos=3) @@ -1208,7 +1640,7 @@ def test_flavor_access_add_by_id(self): {'addTenantAccess': {'tenant': 'proj2'}}) def test_flavor_access_add_by_name(self): - self.run_command(['flavor-access-add', '512 MB Server', 'proj2']) + self.run_command(['flavor-access-add', '512 MiB Server', 'proj2']) self.assert_called('POST', '/flavors/2/action', {'addTenantAccess': {'tenant': 'proj2'}}) @@ -1218,7 +1650,7 @@ def test_flavor_access_remove_by_id(self): {'removeTenantAccess': {'tenant': 'proj2'}}) def test_flavor_access_remove_by_name(self): - self.run_command(['flavor-access-remove', '512 MB Server', 'proj2']) + self.run_command(['flavor-access-remove', '512 MiB Server', 'proj2']) self.assert_called('POST', '/flavors/2/action', {'removeTenantAccess': {'tenant': 'proj2'}}) @@ -1316,7 +1748,7 @@ def test_list_by_user(self): self.run_command('list --user fake_user') self.assert_called( 'GET', - '/servers/detail?all_tenants=1&user_id=fake_user') + '/servers/detail?user_id=fake_user') def test_list_with_single_sort_key_no_dir(self): self.run_command('list --sort 1') @@ -1381,6 +1813,12 @@ def test_list_fields(self): self.assertIn('securitygroup1', output) self.assertIn('OS-EXT-MOD: Some Thing', output) self.assertIn('mod_some_thing_value', output) + # Testing the 'networks' field that is explicitly added to the + # existing fields list. + output, _err = self.run_command('list --fields networks') + self.assertIn('Networks', output) + self.assertIn('10.11.12.13', output) + self.assertIn('5.6.7.8', output) @mock.patch( 'novaclient.tests.unit.v2.fakes.FakeSessionClient.get_servers_detail') @@ -1397,6 +1835,18 @@ def test_list_invalid_fields(self): self.run_command, 'list --fields host,security_groups,' 'OS-EXT-MOD:some_thing,invalid') + self.assertRaises(exceptions.CommandError, + self.run_command, + 'list --fields __dict__') + self.assertRaises(exceptions.CommandError, + self.run_command, + 'list --fields update') + self.assertRaises(exceptions.CommandError, + self.run_command, + 'list --fields __init__') + self.assertRaises(exceptions.CommandError, + self.run_command, + 'list --fields __module__,updated') def test_list_with_marker(self): self.run_command('list --marker some-uuid') @@ -1415,6 +1865,67 @@ def test_list_with_changes_since_invalid_value(self): self.assertRaises(exceptions.CommandError, self.run_command, 'list --changes-since 0123456789') + def test_list_with_changes_before(self): + self.run_command('list --changes-before 2016-02-29T06:23:22', + api_version='2.66') + self.assert_called( + 'GET', '/servers/detail?changes-before=2016-02-29T06%3A23%3A22') + + def test_list_with_changes_before_invalid_value(self): + ex = self.assertRaises(exceptions.CommandError, self.run_command, + 'list --changes-before 0123456789', + api_version='2.66') + self.assertIn('Invalid changes-before value', str(ex)) + + def test_list_with_changes_before_pre_v266_not_allowed(self): + self.assertRaises(SystemExit, self.run_command, + 'list --changes-before 2016-02-29T06:23:22', + api_version='2.65') + + def test_list_with_availability_zone(self): + self.run_command('list --availability-zone nova') + self.assert_called('GET', '/servers/detail?availability_zone=nova') + + def test_list_with_key_name(self): + self.run_command('list --key-name my_key') + self.assert_called('GET', '/servers/detail?key_name=my_key') + + def test_list_with_config_drive(self): + self.run_command('list --config-drive') + self.assert_called('GET', '/servers/detail?config_drive=True') + + def test_list_with_no_config_drive(self): + self.run_command('list --no-config-drive') + self.assert_called('GET', '/servers/detail?config_drive=False') + + def test_list_with_conflicting_config_drive(self): + self.assertRaises(SystemExit, self.run_command, + 'list --config-drive --no-config-drive') + + def test_list_with_progress(self): + self.run_command('list --progress 100') + self.assert_called('GET', '/servers/detail?progress=100') + + def test_list_with_0_progress(self): + self.run_command('list --progress 0') + self.assert_called('GET', '/servers/detail?progress=0') + + def test_list_with_vm_state(self): + self.run_command('list --vm-state active') + self.assert_called('GET', '/servers/detail?vm_state=active') + + def test_list_with_task_state(self): + self.run_command('list --task-state reboot_started') + self.assert_called('GET', '/servers/detail?task_state=reboot_started') + + def test_list_with_power_state(self): + self.run_command('list --power-state 1') + self.assert_called('GET', '/servers/detail?power_state=1') + + def test_list_with_power_state_filter_for_0_state(self): + self.run_command('list --power-state 0') + self.assert_called('GET', '/servers/detail?power_state=0') + def test_list_fields_redundant(self): output, _err = self.run_command('list --fields id,status,status') header = output.splitlines()[1] @@ -1494,6 +2005,38 @@ def test_rebuild_name_meta(self): self.assert_called('GET', '/flavors/1', pos=4) self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + def test_rebuild_reset_keypair(self): + self.run_command('rebuild sample-server %s --key-name test_keypair' % + FAKE_UUID_1, api_version='2.54') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'key_name': 'test_keypair', + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_unset_keypair(self): + self.run_command('rebuild sample-server %s --key-unset' % + FAKE_UUID_1, api_version='2.54') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'key_name': None, + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_unset_keypair_with_key_name(self): + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'rebuild sample-server %s --key-unset --key-name test_keypair' % + FAKE_UUID_1, api_version='2.54') + self.assertIn("Cannot specify '--key-unset' with '--key-name'.", + str(ex)) + def test_rebuild_with_incorrect_metadata(self): cmd = 'rebuild sample-server %s --name asdf --meta foo' % FAKE_UUID_1 result = self.assertRaises(argparse.ArgumentTypeError, @@ -1501,6 +2044,273 @@ def test_rebuild_with_incorrect_metadata(self): expected = "'['foo']' is not in the format of 'key=value'" self.assertEqual(expected, result.args[0]) + def test_rebuild_user_data_2_56(self): + """Tests that trying to run the rebuild command with the --user-data* + options before microversion 2.57 fails. + """ + cmd = 'rebuild sample-server %s --user-data test' % FAKE_UUID_1 + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.56') + cmd = 'rebuild sample-server %s --user-data-unset' % FAKE_UUID_1 + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.56') + + def test_rebuild_files_2_57(self): + """Tests that trying to run the rebuild command with the --file option + after microversion 2.56 fails. + """ + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + cmd = 'rebuild sample-server %s --file /tmp/foo=%s' + self.assertRaises(SystemExit, self.run_command, + cmd % (FAKE_UUID_1, testfile), api_version='2.57') + + def test_rebuild_change_user_data(self): + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + with open(testfile) as testfile_fd: + data = testfile_fd.read().encode('utf-8') + expected_file_data = servers.ServerManager.transform_userdata(data) + self.run_command('rebuild sample-server %s --user-data %s' % + (FAKE_UUID_1, testfile), api_version='2.57') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'user_data': expected_file_data, + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_invalid_user_data(self): + invalid_file = os.path.join(os.path.dirname(__file__), + 'no_such_file') + cmd = ('rebuild sample-server %s --user-data %s' + % (FAKE_UUID_1, invalid_file)) + ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, + api_version='2.57') + self.assertIn("Can't open '%(user_data)s': " + "[Errno 2] No such file or directory: '%(user_data)s'" % + {'user_data': invalid_file}, str(ex)) + + def test_rebuild_unset_user_data(self): + self.run_command('rebuild sample-server %s --user-data-unset' % + FAKE_UUID_1, api_version='2.57') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'user_data': None, + 'description': None}}, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_user_data_and_unset_user_data(self): + """Tests that trying to set --user-data and --unset-user-data in the + same rebuild call fails. + """ + cmd = ('rebuild sample-server %s --user-data x --user-data-unset' % + FAKE_UUID_1) + ex = self.assertRaises(exceptions.CommandError, self.run_command, cmd, + api_version='2.57') + self.assertIn("Cannot specify '--user-data-unset' with " + "'--user-data'.", str(ex)) + + def test_rebuild_with_single_trusted_image_certificates(self): + self.run_command('rebuild sample-server %s ' + '--trusted-image-certificate-id id1' + % FAKE_UUID_1, api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + 'trusted_image_certificates': ['id1'] + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_multiple_trusted_image_certificate_ids(self): + self.run_command('rebuild sample-server %s ' + '--trusted-image-certificate-id id1 ' + '--trusted-image-certificate-id id2' + % FAKE_UUID_1, api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + 'trusted_image_certificates': ['id1', + 'id2'] + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_trusted_image_certificates_envar(self): + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'var_id1,var_id2')) + self.run_command('rebuild sample-server %s' + % FAKE_UUID_1, api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + 'trusted_image_certificates': + ['var_id1', 'var_id2']} + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_without_trusted_image_certificates_v263(self): + self.run_command('rebuild sample-server %s' % FAKE_UUID_1, + api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_trusted_image_certificates_pre_v263(self): + cmd = ('rebuild sample-server %s' + '--trusted-image-certificate-id id1 ' + '--trusted-image-certificate-id id2' % FAKE_UUID_1) + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.62') + + # OS_TRUSTED_IMAGE_CERTIFICATE_IDS environment variable is not supported in + # microversions < 2.63 (should result in an UnsupportedAttribute exception) + def test_rebuild_with_trusted_image_certificates_envar_pre_v263(self): + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'var_id1,var_id2')) + cmd = ('rebuild sample-server %s' % FAKE_UUID_1) + self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, + cmd, api_version='2.62') + + def test_rebuild_with_trusted_image_certificates_unset(self): + """Tests explicitly unsetting the existing server trusted image + certificate IDs. + """ + self.run_command('rebuild sample-server %s ' + '--trusted-image-certificates-unset' + % FAKE_UUID_1, api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + 'trusted_image_certificates': None + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_trusted_image_certificates_unset_arg_conflict(self): + """Tests the error condition that trusted image certs are both unset + and set via argument during rebuild. + """ + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'rebuild sample-server %s --trusted-image-certificate-id id1 ' + '--trusted-image-certificates-unset' % FAKE_UUID_1, + api_version='2.63') + self.assertIn("Cannot specify '--trusted-image-certificates-unset' " + "with '--trusted-image-certificate-id'", + str(ex)) + + def test_rebuild_with_trusted_image_certificates_unset_env_conflict(self): + """Tests the error condition that trusted image certs are both unset + and set via environment variable during rebuild. + """ + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'var_id1')) + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'rebuild sample-server %s --trusted-image-certificates-unset' % + FAKE_UUID_1, api_version='2.63') + self.assertIn("Cannot specify '--trusted-image-certificates-unset' " + "with '--trusted-image-certificate-id'", + str(ex)) + + def test_rebuild_with_trusted_image_certificates_arg_and_envar(self): + """Tests that if both the environment variable and argument are + specified, the argument takes precedence. + """ + self.useFixture(fixtures.EnvironmentVariable( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS', 'cert1')) + self.run_command('rebuild sample-server ' + '--trusted-image-certificate-id cert2 %s' + % FAKE_UUID_1, api_version='2.63') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + 'trusted_image_certificates': + ['cert2']} + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_server_groups_in_response(self): + out = self.run_command('rebuild sample-server %s' % FAKE_UUID_1, + api_version='2.71')[0] + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + self.assertIn('server_groups', out) + self.assertIn('a67359fb-d397-4697-88f1-f55e3ee7c499', out) + + def test_rebuild_without_server_groups_in_response(self): + out = self.run_command('rebuild sample-server %s' % FAKE_UUID_1, + api_version='2.70')[0] + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called('POST', '/servers/1234/action', + {'rebuild': {'imageRef': FAKE_UUID_1, + 'description': None, + } + }, pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + self.assertNotIn('server_groups', out) + self.assertNotIn('a67359fb-d397-4697-88f1-f55e3ee7c499', out) + + def test_rebuild_with_hostname(self): + self.run_command( + 'rebuild sample-server %s --hostname new-hostname' % FAKE_UUID_1, + api_version='2.90') + self.assert_called('GET', '/servers?name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_1, pos=2) + self.assert_called( + 'POST', '/servers/1234/action', + { + 'rebuild': { + 'imageRef': FAKE_UUID_1, + 'description': None, + 'hostname': 'new-hostname', + }, + }, + pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_rebuild_with_hostname_pre_v290(self): + self.assertRaises( + SystemExit, self.run_command, + 'rebuild sample-server %s --hostname hostname' % FAKE_UUID_1, + api_version='2.89') + def test_start(self): self.run_command('start sample-server') self.assert_called('POST', '/servers/1234/action', {'os-start': None}) @@ -1535,6 +2345,24 @@ def test_lock(self): self.run_command('lock sample-server') self.assert_called('POST', '/servers/1234/action', {'lock': None}) + def test_lock_pre_v273(self): + exp = self.assertRaises(SystemExit, + self.run_command, + 'lock sample-server --reason zombies', + api_version='2.72') + self.assertIn('2', str(exp)) + + def test_lock_v273(self): + self.run_command('lock sample-server', + api_version='2.73') + self.assert_called('POST', '/servers/1234/action', + {'lock': None}) + + self.run_command('lock sample-server --reason zombies', + api_version='2.73') + self.assert_called('POST', '/servers/1234/action', + {'lock': {'locked_reason': 'zombies'}}) + def test_unlock(self): self.run_command('unlock sample-server') self.assert_called('POST', '/servers/1234/action', {'unlock': None}) @@ -1578,10 +2406,96 @@ def test_unshelve(self): self.run_command('unshelve sample-server') self.assert_called('POST', '/servers/1234/action', {'unshelve': None}) + def test_unshelve_pre_v277_with_az_fails(self): + """Tests that trying to unshelve with an --availability-zone before + 2.77 is an error. + """ + self.assertRaises(SystemExit, + self.run_command, + 'unshelve --availability-zone foo-az sample-server', + api_version='2.76') + + def test_unshelve_v277(self): + # Test backward compat without an AZ specified. + self.run_command('unshelve sample-server', + api_version='2.77') + self.assert_called('POST', '/servers/1234/action', + {'unshelve': None}) + # Test with specifying an AZ. + self.run_command('unshelve --availability-zone foo-az sample-server', + api_version='2.77') + self.assert_called('POST', '/servers/1234/action', + {'unshelve': {'availability_zone': 'foo-az'}}) + def test_migrate(self): self.run_command('migrate sample-server') self.assert_called('POST', '/servers/1234/action', {'migrate': None}) + def test_migrate_pre_v256(self): + self.assertRaises(SystemExit, + self.run_command, + 'migrate --host target-host sample-server', + api_version='2.55') + + def test_migrate_v256(self): + self.run_command('migrate sample-server', + api_version='2.56') + self.assert_called('POST', '/servers/1234/action', + {'migrate': {}}) + + self.run_command('migrate --host target-host sample-server', + api_version='2.56') + self.assert_called('POST', '/servers/1234/action', + {'migrate': {'host': 'target-host'}}) + + def test_update(self): + self.run_command('update --name new-name sample-server') + expected_put_body = { + "server": { + "name": "new-name" + } + } + self.assert_called('GET', '/servers/1234', pos=-2) + self.assert_called('PUT', '/servers/1234', expected_put_body, pos=-1) + + def test_update_with_description(self): + self.run_command( + 'update --description new-description sample-server', + api_version='2.19') + expected_put_body = { + "server": { + "description": "new-description" + } + } + self.assert_called('GET', '/servers/1234', pos=-2) + self.assert_called('PUT', '/servers/1234', expected_put_body, pos=-1) + + def test_update_with_description_pre_v219(self): + self.assertRaises( + SystemExit, + self.run_command, + 'update --description new-description sample-server', + api_version='2.18') + + def test_update_with_hostname(self): + self.run_command( + 'update --hostname new-hostname sample-server', + api_version='2.90') + expected_put_body = { + "server": { + "hostname": "new-hostname" + } + } + self.assert_called('GET', '/servers/1234', pos=-2) + self.assert_called('PUT', '/servers/1234', expected_put_body, pos=-1) + + def test_update_with_hostname_pre_v290(self): + self.assertRaises( + SystemExit, + self.run_command, + 'update --hostname new-hostname sample-server', + api_version='2.89') + def test_resize(self): self.run_command('resize sample-server 1') self.assert_called('POST', '/servers/1234/action', @@ -1636,6 +2550,26 @@ def test_show_with_name_help(self): output, _ = self.run_command('show help') self.assert_called('GET', '/servers/9014', pos=-6) + def test_show_with_server_groups_in_response(self): + # Starting microversion 2.71, the 'server_groups' is included + # in the output (the response). + out = self.run_command('show 1234', api_version='2.71')[0] + self.assert_called('GET', '/servers?name=1234', pos=0) + self.assert_called('GET', '/servers?name=1234', pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=3) + self.assertIn('server_groups', out) + self.assertIn('a67359fb-d397-4697-88f1-f55e3ee7c499', out) + + def test_show_without_server_groups_in_response(self): + out = self.run_command('show 1234', api_version='2.70')[0] + self.assert_called('GET', '/servers?name=1234', pos=0) + self.assert_called('GET', '/servers?name=1234', pos=1) + self.assert_called('GET', '/servers/1234', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=3) + self.assertNotIn('server_groups', out) + self.assertNotIn('a67359fb-d397-4697-88f1-f55e3ee7c499', out) + @mock.patch('novaclient.v2.shell.utils.print_dict') def test_print_server(self, mock_print_dict): self.run_command('show 5678') @@ -1725,6 +2659,19 @@ def test_diagnostics(self): self.run_command('diagnostics sample-server') self.assert_called('GET', '/servers/1234/diagnostics') + def test_server_topology(self): + self.run_command('server-topology 1234', api_version='2.78') + self.assert_called('GET', '/servers/1234/topology') + self.run_command('server-topology sample-server', api_version='2.78') + self.assert_called('GET', '/servers/1234/topology') + + def test_server_topology_pre278(self): + exp = self.assertRaises(SystemExit, + self.run_command, + 'server-topology 1234', + api_version='2.77') + self.assertIn('2', str(exp)) + def test_refresh_network(self): self.run_command('refresh-network 1234') self.assert_called('POST', '/os-server-external-events', @@ -1762,31 +2709,39 @@ def test_set_host_meta(self): {'metadata': {'key1': 'val1', 'key2': 'val2'}}, pos=4) + def test_set_host_meta_strict(self): + self.run_command('host-meta hyper1 --strict set key1=val1 key2=val2') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}, + pos=1) + self.assert_called('POST', '/servers/uuid2/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}, + pos=2) + + def test_set_host_meta_no_match(self): + cmd = 'host-meta hyper --strict set key1=val1 key2=val2' + self.assertRaises(exceptions.NotFound, self.run_command, cmd) + def test_set_host_meta_with_no_servers(self): self.run_command('host-meta hyper_no_servers set key1=val1 key2=val2') self.assert_called('GET', '/os-hypervisors/hyper_no_servers/servers') + def test_set_host_meta_with_no_servers_strict(self): + cmd = 'host-meta hyper_no_servers --strict set key1=val1 key2=val2' + self.assertRaises(exceptions.NotFound, self.run_command, cmd) + def test_delete_host_meta(self): self.run_command('host-meta hyper delete key1') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) self.assert_called('DELETE', '/servers/uuid1/metadata/key1', pos=1) self.assert_called('DELETE', '/servers/uuid2/metadata/key1', pos=2) - def test_server_floating_ip_associate(self): - _, err = self.run_command( - 'floating-ip-associate sample-server 11.0.0.1') - self.assertIn('WARNING: Command floating-ip-associate is deprecated', - err) - self.assert_called('POST', '/servers/1234/action', - {'addFloatingIp': {'address': '11.0.0.1'}}) - - def test_server_floating_ip_disassociate(self): - _, err = self.run_command( - 'floating-ip-disassociate sample-server 11.0.0.1') - self.assertIn( - 'WARNING: Command floating-ip-disassociate is deprecated', err) - self.assert_called('POST', '/servers/1234/action', - {'removeFloatingIp': {'address': '11.0.0.1'}}) + def test_delete_host_meta_strict(self): + self.run_command('host-meta hyper1 --strict delete key1') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('DELETE', '/servers/uuid1/metadata/key1', pos=1) + self.assert_called('DELETE', '/servers/uuid2/metadata/key1', pos=2) def test_usage_list(self): cmd = 'usage-list --start 2000-01-20 --end 2005-02-01' @@ -1796,8 +2751,8 @@ def test_usage_list(self): 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'detailed=1') - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours - self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) + # Servers, RAM MiB-Hours, CPU Hours, Disk GiB-Hours + self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) def test_usage_list_stitch_together_next_results(self): cmd = 'usage-list --start 2000-01-20 --end 2005-02-01' @@ -1817,8 +2772,8 @@ def test_usage_list_stitch_together_next_results(self): 'start=2000-01-20T00:00:00&' 'end=2005-02-01T00:00:00&' 'marker=%s&detailed=1' % (marker), pos=pos + 1) - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours - self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) + # Servers, RAM MiB-Hours, CPU Hours, Disk GiB-Hours + self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_list_no_args(self): timeutils.set_time_override(datetime.datetime(2005, 2, 1, 0, 0)) @@ -1837,8 +2792,8 @@ def test_usage(self): '/os-simple-tenant-usage/test?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00') - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours - self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) + # Servers, RAM MiB-Hours, CPU Hours, Disk GiB-Hours + self.assertIn('1 | 25451.76 | 49.71 | 0.00', stdout) def test_usage_stitch_together_next_results(self): cmd = 'usage --start 2000-01-20 --end 2005-02-01' @@ -1857,8 +2812,8 @@ def test_usage_stitch_together_next_results(self): 'start=2000-01-20T00:00:00&' 'end=2005-02-01T00:00:00&' 'marker=%s' % (marker), pos=pos + 1) - # Servers, RAM MB-Hours, CPU Hours, Disk GB-Hours - self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) + # Servers, RAM MiB-Hours, CPU Hours, Disk GiB-Hours + self.assertIn('2 | 50903.53 | 99.42 | 0.00', stdout) def test_usage_no_tenant(self): self.run_command('usage --start 2000-01-20 --end 2005-02-01') @@ -1878,6 +2833,41 @@ def test_flavor_create(self): self.assert_called('POST', '/flavors', pos=-2) self.assert_called('GET', '/flavors/1', pos=-1) + def test_flavor_create_with_description(self): + """Tests creating a flavor with a description.""" + self.run_command("flavor-create description " + "1234 512 10 1 --description foo", api_version='2.55') + expected_post_body = { + "flavor": { + "name": "description", + "ram": 512, + "vcpus": 1, + "disk": 10, + "id": "1234", + "swap": 0, + "OS-FLV-EXT-DATA:ephemeral": 0, + "rxtx_factor": 1.0, + "os-flavor-access:is_public": True, + "description": "foo" + } + } + self.assert_called('POST', '/flavors', expected_post_body, pos=-2) + + def test_flavor_update(self): + """Tests creating a flavor with a description.""" + out, _ = self.run_command( + "flavor-update with-description new-description", + api_version='2.55') + expected_put_body = { + "flavor": { + "description": "new-description" + } + } + self.assert_called('GET', '/flavors/with-description', pos=-2) + self.assert_called('PUT', '/flavors/with-description', + expected_put_body, pos=-1) + self.assertIn('new-description', out) + def test_aggregate_list(self): out, err = self.run_command('aggregate-list') self.assert_called('GET', '/os-aggregates') @@ -1953,6 +2943,13 @@ def test_aggregate_update_with_availability_zone_by_name(self): self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) self.assert_called('GET', '/os-aggregates/1', pos=-1) + def test_aggregate_update_without_availability_zone_and_name(self): + ex = self.assertRaises(exceptions.CommandError, self.run_command, + 'aggregate-update test') + self.assertIn("Either '--name ' or '--availability-zone " + "' must be specified.", + str(ex)) + def test_aggregate_set_metadata_add_by_id(self): out, err = self.run_command('aggregate-set-metadata 3 foo=bar') body = {"set_metadata": {"metadata": {"foo": "bar"}}} @@ -2048,6 +3045,30 @@ def test_aggregate_show_by_name(self): self.run_command('aggregate-show test') self.assert_called('GET', '/os-aggregates') + def test_aggregate_cache_images(self): + self.run_command( + 'aggregate-cache-images 1 %s %s' % ( + FAKE_UUID_1, FAKE_UUID_2), + api_version='2.81') + body = { + 'cache': [{'id': FAKE_UUID_1}, + {'id': FAKE_UUID_2}], + } + self.assert_called('POST', '/os-aggregates/1/images', body) + + def test_aggregate_cache_images_no_images(self): + self.assertRaises(SystemExit, + self.run_command, + 'aggregate-cache-images 1', + api_version='2.81') + + def test_aggregate_cache_images_pre281(self): + self.assertRaises(SystemExit, + self.run_command, + 'aggregate-cache-images 1 %s %s' % ( + FAKE_UUID_1, FAKE_UUID_2), + api_version='2.80') + def test_live_migration(self): self.run_command('live-migration sample-server hostname') self.assert_called('POST', '/servers/1234/action', @@ -2096,6 +3117,18 @@ def test_live_migration_v2_30(self): 'block_migration': 'auto', 'force': True}}) + def test_live_migration_v2_68(self): + self.run_command('live-migration sample-server hostname', + api_version='2.68') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + + self.assertRaises( + SystemExit, self.run_command, + 'live-migration --force sample-server hostname', + api_version='2.68') + def test_live_migration_force_complete(self): self.run_command('live-migration-force-complete sample-server 1', api_version='2.22') @@ -2107,11 +3140,39 @@ def test_list_migrations(self): api_version='2.23') self.assert_called('GET', '/servers/1234/migrations') + def test_list_migrations_pre_v280(self): + out = self.run_command('server-migration-list sample-server', + api_version='2.79')[0] + self.assert_called('GET', '/servers/1234/migrations') + self.assertNotIn('User ID', out) + self.assertNotIn('Project ID', out) + + def test_list_migrations_v280(self): + out = self.run_command('server-migration-list sample-server', + api_version='2.80')[0] + self.assert_called('GET', '/servers/1234/migrations') + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + def test_get_migration(self): self.run_command('server-migration-show sample-server 1', api_version='2.23') self.assert_called('GET', '/servers/1234/migrations/1') + def test_get_migration_pre_v280(self): + out = self.run_command('server-migration-show sample-server 1', + api_version='2.79')[0] + self.assert_called('GET', '/servers/1234/migrations/1') + self.assertNotIn('user_id', out) + self.assertNotIn('project_id', out) + + def test_get_migration_v280(self): + out = self.run_command('server-migration-show sample-server 1', + api_version='2.80')[0] + self.assert_called('GET', '/servers/1234/migrations/1') + self.assertIn('user_id', out) + self.assertIn('project_id', out) + def test_live_migration_abort(self): self.run_command('live-migration-abort sample-server 1', api_version='2.24') @@ -2128,6 +3189,19 @@ def test_host_evacuate_live_with_no_target_host(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_no_target_host_strict(self): + self.run_command('host-evacuate-live hyper1 --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': False, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + + def test_host_evacuate_live_no_match(self): + cmd = 'host-evacuate-live hyper --strict' + self.assertRaises(exceptions.NotFound, self.run_command, cmd) + def test_host_evacuate_live_2_25(self): self.run_command('host-evacuate-live hyper', api_version='2.25') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2137,6 +3211,14 @@ def test_host_evacuate_live_2_25(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_2_25_strict(self): + self.run_command('host-evacuate-live hyper1 --strict', + api_version='2.25') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + body = {'os-migrateLive': {'host': None, 'block_migration': 'auto'}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + def test_host_evacuate_live_with_target_host(self): self.run_command('host-evacuate-live hyper ' '--target-host hostname') @@ -2149,6 +3231,16 @@ def test_host_evacuate_live_with_target_host(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_target_host_strict(self): + self.run_command('host-evacuate-live hyper1 ' + '--target-host hostname --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + body = {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + def test_host_evacuate_live_2_30(self): self.run_command('host-evacuate-live --force hyper ' '--target-host hostname', @@ -2162,6 +3254,17 @@ def test_host_evacuate_live_2_30(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_2_30_strict(self): + self.run_command('host-evacuate-live --force hyper1 ' + '--target-host hostname --strict', + api_version='2.30') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + body = {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto', + 'force': True}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + def test_host_evacuate_live_with_block_migration(self): self.run_command('host-evacuate-live --block-migrate hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2173,6 +3276,15 @@ def test_host_evacuate_live_with_block_migration(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_block_migration_strict(self): + self.run_command('host-evacuate-live --block-migrate hyper2 --strict') + self.assert_called('GET', '/os-hypervisors/hyper2/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': True, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid3/action', body, pos=1) + self.assert_called('POST', '/servers/uuid4/action', body, pos=2) + def test_host_evacuate_live_with_block_migration_2_25(self): self.run_command('host-evacuate-live --block-migrate hyper', api_version='2.25') @@ -2183,6 +3295,14 @@ def test_host_evacuate_live_with_block_migration_2_25(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_block_migration_2_25_strict(self): + self.run_command('host-evacuate-live --block-migrate hyper2 --strict', + api_version='2.25') + self.assert_called('GET', '/os-hypervisors/hyper2/servers', pos=0) + body = {'os-migrateLive': {'host': None, 'block_migration': True}} + self.assert_called('POST', '/servers/uuid3/action', body, pos=1) + self.assert_called('POST', '/servers/uuid4/action', body, pos=2) + def test_host_evacuate_live_with_disk_over_commit(self): self.run_command('host-evacuate-live --disk-over-commit hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2194,11 +3314,26 @@ def test_host_evacuate_live_with_disk_over_commit(self): self.assert_called('POST', '/servers/uuid3/action', body, pos=3) self.assert_called('POST', '/servers/uuid4/action', body, pos=4) + def test_host_evacuate_live_with_disk_over_commit_strict(self): + self.run_command('host-evacuate-live --disk-over-commit hyper2 ' + '--strict') + self.assert_called('GET', '/os-hypervisors/hyper2/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': False, + 'disk_over_commit': True}} + self.assert_called('POST', '/servers/uuid3/action', body, pos=1) + self.assert_called('POST', '/servers/uuid4/action', body, pos=2) + def test_host_evacuate_live_with_disk_over_commit_2_25(self): self.assertRaises(SystemExit, self.run_command, 'host-evacuate-live --disk-over-commit hyper', api_version='2.25') + def test_host_evacuate_live_with_disk_over_commit_2_25_strict(self): + self.assertRaises(SystemExit, self.run_command, + 'host-evacuate-live --disk-over-commit hyper2 ' + '--strict', api_version='2.25') + def test_host_evacuate_list_with_max_servers(self): self.run_command('host-evacuate-live --max-servers 1 hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2207,6 +3342,14 @@ def test_host_evacuate_list_with_max_servers(self): 'disk_over_commit': False}} self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + def test_host_evacuate_list_with_max_servers_strict(self): + self.run_command('host-evacuate-live --max-servers 1 hyper1 --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + body = {'os-migrateLive': {'host': None, + 'block_migration': False, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + def test_reset_state(self): self.run_command('reset-state sample-server') self.assert_called('POST', '/servers/1234/action', @@ -2251,6 +3394,23 @@ def test_services_list_v2_53(self): self.run_command('service-list', api_version='2.53') self.assert_called('GET', '/os-services') + def test_services_list_v269_with_down_cells(self): + """Tests nova service-list at the 2.69 microversion.""" + stdout, _stderr = self.run_command('service-list', api_version='2.69') + self.assertEqual( + '''\ ++--------------------------------------+--------------+-----------+------+----------+-------+---------------------+-----------------+-------------+ +| Id | Binary | Host | Zone | Status | State | Updated_at | Disabled Reason | Forced down | ++--------------------------------------+--------------+-----------+------+----------+-------+---------------------+-----------------+-------------+ +| 75e9eabc-ed3b-4f11-8bba-add1e7e7e2de | nova-compute | host1 | nova | enabled | up | 2012-10-29 13:42:02 | | | +| 1f140183-c914-4ddf-8757-6df73028aa86 | nova-compute | host1 | nova | disabled | down | 2012-09-18 08:03:38 | | | +| | nova-compute | host-down | | UNKNOWN | | | | | ++--------------------------------------+--------------+-----------+------+----------+-------+---------------------+-----------------+-------------+ +''', # noqa + stdout, + ) + self.assert_called('GET', '/os-services') + def test_services_list_with_host(self): self.run_command('service-list --host host1') self.assert_called('GET', '/os-services?host=host1') @@ -2264,8 +3424,8 @@ def test_services_list_with_host_binary(self): self.assert_called('GET', '/os-services?host=host1&binary=nova-cert') def test_services_enable(self): - self.run_command('service-enable host1 nova-cert') - body = {'host': 'host1', 'binary': 'nova-cert'} + self.run_command('service-enable host1') + body = {'host': 'host1', 'binary': 'nova-compute'} self.assert_called('PUT', '/os-services/enable', body) def test_services_enable_v2_53(self): @@ -2276,15 +3436,9 @@ def test_services_enable_v2_53(self): self.assert_called( 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, body) - def test_services_enable_default_binary(self): - """Tests that the default binary is nova-compute if not specified.""" - self.run_command('service-enable host1') - body = {'host': 'host1', 'binary': 'nova-compute'} - self.assert_called('PUT', '/os-services/enable', body) - def test_services_disable(self): - self.run_command('service-disable host1 nova-cert') - body = {'host': 'host1', 'binary': 'nova-cert'} + self.run_command('service-disable host1') + body = {'host': 'host1', 'binary': 'nova-compute'} self.assert_called('PUT', '/os-services/disable', body) def test_services_disable_v2_53(self): @@ -2295,15 +3449,9 @@ def test_services_disable_v2_53(self): self.assert_called( 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, body) - def test_services_disable_default_binary(self): - """Tests that the default binary is nova-compute if not specified.""" - self.run_command('service-disable host1') - body = {'host': 'host1', 'binary': 'nova-compute'} - self.assert_called('PUT', '/os-services/disable', body) - def test_services_disable_with_reason(self): - self.run_command('service-disable host1 nova-cert --reason no_reason') - body = {'host': 'host1', 'binary': 'nova-cert', + self.run_command('service-disable host1 --reason no_reason') + body = {'host': 'host1', 'binary': 'nova-compute', 'disabled_reason': 'no_reason'} self.assert_called('PUT', '/os-services/disable-log-reason', body) @@ -2333,66 +3481,6 @@ def test_services_delete_v2_53(self): self.assert_called( 'DELETE', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1) - def test_host_list(self): - _, err = self.run_command('host-list') - # make sure we said it's deprecated - self.assertIn('WARNING: Command host-list is deprecated', err) - # and replaced with hypervisor-list - self.assertIn('hypervisor-list', err) - self.assert_called('GET', '/os-hosts') - - def test_host_list_with_zone(self): - self.run_command('host-list --zone nova') - self.assert_called('GET', '/os-hosts?zone=nova') - - def test_host_update_status(self): - _, err = self.run_command('host-update sample-host_1 --status enable') - # make sure we said it's deprecated - self.assertIn('WARNING: Command host-update is deprecated', err) - # and replaced with service-enable - self.assertIn('service-enable', err) - body = {'status': 'enable'} - self.assert_called('PUT', '/os-hosts/sample-host_1', body) - - def test_host_update_maintenance(self): - _, err = ( - self.run_command('host-update sample-host_2 --maintenance enable')) - # make sure we said it's deprecated - self.assertIn('WARNING: Command host-update is deprecated', err) - # and there is no replacement - self.assertIn('There is no replacement', err) - body = {'maintenance_mode': 'enable'} - self.assert_called('PUT', '/os-hosts/sample-host_2', body) - - def test_host_update_multiple_settings(self): - _, err = self.run_command('host-update sample-host_3 ' - '--status disable --maintenance enable') - # make sure we said it's deprecated - self.assertIn('WARNING: Command host-update is deprecated', err) - # and replaced with service-disable - self.assertIn('service-disable', err) - body = {'status': 'disable', 'maintenance_mode': 'enable'} - self.assert_called('PUT', '/os-hosts/sample-host_3', body) - - def test_host_startup(self): - _, err = self.run_command('host-action sample-host --action startup') - # make sure we said it's deprecated - self.assertIn('WARNING: Command host-action is deprecated', err) - # and there is no replacement - self.assertIn('There is no replacement', err) - self.assert_called( - 'GET', '/os-hosts/sample-host/startup') - - def test_host_shutdown(self): - self.run_command('host-action sample-host --action shutdown') - self.assert_called( - 'GET', '/os-hosts/sample-host/shutdown') - - def test_host_reboot(self): - self.run_command('host-action sample-host --action reboot') - self.assert_called( - 'GET', '/os-hosts/sample-host/reboot') - def test_host_evacuate_v2_14(self): self.run_command('host-evacuate hyper --target target_hyper', api_version='2.14') @@ -2406,6 +3494,15 @@ def test_host_evacuate_v2_14(self): self.assert_called('POST', '/servers/uuid4/action', {'evacuate': {'host': 'target_hyper'}}, pos=4) + def test_host_evacuate_v2_14_strict(self): + self.run_command('host-evacuate hyper1 --target target_hyper --strict', + api_version='2.14') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper'}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper'}}, pos=2) + def test_host_evacuate(self): self.run_command('host-evacuate hyper --target target_hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2422,6 +3519,20 @@ def test_host_evacuate(self): {'evacuate': {'host': 'target_hyper', 'onSharedStorage': False}}, pos=4) + def test_host_evacuate_strict(self): + self.run_command('host-evacuate hyper1 --target target_hyper --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': False}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': False}}, pos=2) + + def test_host_evacuate_no_match(self): + cmd = 'host-evacuate hyper --target target_hyper --strict' + self.assertRaises(exceptions.NotFound, self.run_command, cmd) + def test_host_evacuate_v2_29(self): self.run_command('host-evacuate hyper --target target_hyper --force', api_version='2.29') @@ -2439,6 +3550,17 @@ def test_host_evacuate_v2_29(self): {'evacuate': {'host': 'target_hyper', 'force': True} }, pos=4) + def test_host_evacuate_v2_29_strict(self): + self.run_command('host-evacuate hyper1 --target target_hyper' + ' --force --strict', api_version='2.29') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper', 'force': True} + }, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper', 'force': True} + }, pos=2) + def test_host_evacuate_with_shared_storage(self): self.run_command( 'host-evacuate --on-shared-storage hyper --target target_hyper') @@ -2456,6 +3578,17 @@ def test_host_evacuate_with_shared_storage(self): {'evacuate': {'host': 'target_hyper', 'onSharedStorage': True}}, pos=4) + def test_host_evacuate_with_shared_storage_strict(self): + self.run_command('host-evacuate --on-shared-storage hyper1' + ' --target target_hyper --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': True}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': True}}, pos=2) + def test_host_evacuate_with_no_target_host(self): self.run_command('host-evacuate --on-shared-storage hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2468,6 +3601,14 @@ def test_host_evacuate_with_no_target_host(self): self.assert_called('POST', '/servers/uuid4/action', {'evacuate': {'onSharedStorage': True}}, pos=4) + def test_host_evacuate_with_no_target_host_strict(self): + self.run_command('host-evacuate --on-shared-storage hyper1 --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'onSharedStorage': True}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'onSharedStorage': True}}, pos=2) + def test_host_servers_migrate(self): self.run_command('host-servers-migrate hyper') self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) @@ -2480,6 +3621,18 @@ def test_host_servers_migrate(self): self.assert_called('POST', '/servers/uuid4/action', {'migrate': None}, pos=4) + def test_host_servers_migrate_strict(self): + self.run_command('host-servers-migrate hyper1 --strict') + self.assert_called('GET', '/os-hypervisors/hyper1/servers', pos=0) + self.assert_called('POST', + '/servers/uuid1/action', {'migrate': None}, pos=1) + self.assert_called('POST', + '/servers/uuid2/action', {'migrate': None}, pos=2) + + def test_host_servers_migrate_no_match(self): + cmd = 'host-servers-migrate hyper --strict' + self.assertRaises(exceptions.NotFound, self.run_command, cmd) + def test_hypervisor_list(self): self.run_command('hypervisor-list') self.assert_called('GET', '/os-hypervisors') @@ -2525,6 +3678,16 @@ def test_hypervisor_stats(self): self.run_command('hypervisor-stats') self.assert_called('GET', '/os-hypervisors/statistics') + def test_hypervisor_stats_v2_88(self): + """Tests nova hypervisor-stats at the 2.88 microversion.""" + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'hypervisor-stats', api_version='2.88') + self.assertIn( + 'The hypervisor-stats command is not supported in API version ' + '2.88 or later.', + str(ex)) + def test_quota_show(self): self.run_command( 'quota-show --tenant ' @@ -2610,6 +3773,17 @@ def test_quota_update_fixed_ip(self): 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', {'quota_set': {'fixed_ips': 5}}) + def test_quota_update_injected_file_2_57(self): + """Tests that trying to update injected_file* quota with microversion + 2.57 fails. + """ + for quota in ('--injected-files', '--injected-file-content-bytes', + '--injected-file-path-bytes'): + cmd = ('quota-update 97f4c221bff44578b0300df4ef119353 %s=5' % + quota) + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.57') + def test_quota_delete(self): self.run_command('quota-delete --tenant ' '97f4c221bff44578b0300df4ef119353') @@ -2647,32 +3821,15 @@ def test_quota_class_update(self): 'PUT', '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353', body) - def test_cloudpipe_list(self): - self.run_command('cloudpipe-list') - self.assert_called('GET', '/os-cloudpipe') - - def test_cloudpipe_create(self): - self.run_command('cloudpipe-create myproject') - body = {'cloudpipe': {'project_id': "myproject"}} - self.assert_called('POST', '/os-cloudpipe', body) - - def test_cloudpipe_configure(self): - self.run_command('cloudpipe-configure 192.168.1.1 1234') - body = {'configure_project': {'vpn_ip': "192.168.1.1", - 'vpn_port': '1234'}} - self.assert_called('PUT', '/os-cloudpipe/configure-project', body) - - def test_add_fixed_ip(self): - _, err = self.run_command('add-fixed-ip sample-server 1') - self.assertIn('WARNING: Command add-fixed-ip is deprecated', err) - self.assert_called('POST', '/servers/1234/action', - {'addFixedIp': {'networkId': '1'}}) - - def test_remove_fixed_ip(self): - _, err = self.run_command('remove-fixed-ip sample-server 10.0.0.10') - self.assertIn('WARNING: Command remove-fixed-ip is deprecated', err) - self.assert_called('POST', '/servers/1234/action', - {'removeFixedIp': {'address': '10.0.0.10'}}) + def test_quota_class_update_injected_file_2_57(self): + """Tests that trying to update injected_file* quota with microversion + 2.57 fails. + """ + for quota in ('--injected-files', '--injected-file-content-bytes', + '--injected-file-path-bytes'): + cmd = 'quota-class-update default %s=5' % quota + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.57') def test_backup(self): out, err = self.run_command('backup sample-server back1 daily 1') @@ -2706,8 +3863,9 @@ def test_backup_2_45(self): 'rotation': '1'}}) def test_limits(self): - self.run_command('limits') + out = self.run_command('limits')[0] self.assert_called('GET', '/limits') + self.assertIn('Personality', out) self.run_command('limits --reserved') self.assert_called('GET', '/limits?reserved=1') @@ -2719,6 +3877,28 @@ def test_limits(self): self.assertIn('Verb', stdout) self.assertIn('Name', stdout) + def test_print_absolute_limits(self): + # Note: This test is to validate that no exception is + # thrown if in case we pass multiple custom fields + limits = [TestAbsoluteLimits('maxTotalPrivateNetworks', 3), + TestAbsoluteLimits('totalPrivateNetworksUsed', 0), + # Above two fields are custom fields + TestAbsoluteLimits('maxImageMeta', 15), + TestAbsoluteLimits('totalCoresUsed', 10), + TestAbsoluteLimits('totalInstancesUsed', 5), + TestAbsoluteLimits('maxServerMeta', 10), + TestAbsoluteLimits('totalRAMUsed', 10240), + TestAbsoluteLimits('totalFloatingIpsUsed', 10)] + novaclient.v2.shell._print_absolute_limits(limits=limits) + + def test_limits_2_57(self): + """Tests the limits command at microversion 2.57 where personality + size limits should not be shown. + """ + out = self.run_command('limits', api_version='2.57')[0] + self.assert_called('GET', '/limits') + self.assertNotIn('Personality', out) + def test_evacuate(self): self.run_command('evacuate sample-server new_host') self.assert_called('POST', '/servers/1234/action', @@ -2751,6 +3931,17 @@ def test_evacuate_v2_29(self): {'evacuate': {'host': 'new_host', 'force': True}}) + def test_evacuate_v2_68(self): + self.run_command('evacuate sample-server new_host', + api_version='2.68') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host'}}) + + self.assertRaises( + SystemExit, self.run_command, + 'evacuate --force sample-server new_host', + api_version='2.68') + def test_evacuate_with_no_target_host(self): self.run_command('evacuate sample-server') self.assert_called('POST', '/servers/1234/action', @@ -2779,6 +3970,12 @@ def test_availability_zone_list(self): self.run_command('availability-zone-list') self.assert_called('GET', '/os-availability-zone/detail') + def test_console_log(self): + out = self.run_command('console-log --length 20 1234')[0] + self.assert_called('POST', '/servers/1234/action', + body={'os-getConsoleOutput': {'length': '20'}}) + self.assertIn('foo', out) + def test_server_security_group_add(self): self.run_command('add-secgroup sample-server testgroup') self.assert_called('POST', '/servers/1234/action', @@ -2794,8 +3991,14 @@ def test_server_security_group_list(self): self.assert_called('GET', '/servers/1234/os-security-groups') def test_interface_list(self): - self.run_command('interface-list 1234') + out = self.run_command('interface-list 1234')[0] + self.assert_called('GET', '/servers/1234/os-interface') + self.assertNotIn('Tag', out) + + def test_interface_list_v2_70(self): + out = self.run_command('interface-list 1234', api_version='2.70')[0] self.assert_called('GET', '/servers/1234/os-interface') + self.assertIn('test-tag', out) def test_interface_attach(self): self.run_command('interface-attach --port-id port_id 1234') @@ -2809,20 +4012,37 @@ def test_interface_attach_with_tag_pre_v2_49(self): api_version='2.48') def test_interface_attach_with_tag(self): - self.run_command( - 'interface-attach --port-id port_id --tag test_tag 1234', - api_version='2.49') + out = self.run_command( + 'interface-attach --port-id port_id --tag test-tag 1234', + api_version='2.49')[0] + self.assert_called('POST', '/servers/1234/os-interface', + {'interfaceAttachment': {'port_id': 'port_id', + 'tag': 'test-tag'}}) + self.assertNotIn('test-tag', out) + + def test_interface_attach_v2_70(self): + out = self.run_command( + 'interface-attach --port-id port_id --tag test-tag 1234', + api_version='2.70')[0] self.assert_called('POST', '/servers/1234/os-interface', {'interfaceAttachment': {'port_id': 'port_id', - 'tag': 'test_tag'}}) + 'tag': 'test-tag'}}) + self.assertIn('test-tag', out) def test_interface_detach(self): self.run_command('interface-detach 1234 port_id') self.assert_called('DELETE', '/servers/1234/os-interface/port_id') def test_volume_attachments(self): - self.run_command('volume-attachments 1234') + out = self.run_command('volume-attachments 1234')[0] self.assert_called('GET', '/servers/1234/os-volume_attachments') + self.assertNotIn('test-tag', out) + + def test_volume_attachments_v2_70(self): + out = self.run_command( + 'volume-attachments 1234', api_version='2.70')[0] + self.assert_called('GET', '/servers/1234/os-volume_attachments') + self.assertIn('test-tag', out) def test_volume_attach(self): self.run_command('volume-attach sample-server Work /dev/vdb') @@ -2844,20 +4064,130 @@ def test_volume_attach_with_tag_pre_v2_49(self): api_version='2.48') def test_volume_attach_with_tag(self): - self.run_command( + out = self.run_command( 'volume-attach --tag test_tag sample-server Work /dev/vdb', - api_version='2.49') + api_version='2.49')[0] self.assert_called('POST', '/servers/1234/os-volume_attachments', {'volumeAttachment': {'device': '/dev/vdb', 'volumeId': 'Work', 'tag': 'test_tag'}}) + self.assertNotIn('test-tag', out) + + def test_volume_attach_with_tag_v2_70(self): + out = self.run_command( + 'volume-attach --tag test-tag sample-server Work /dev/vdb', + api_version='2.70')[0] + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'device': '/dev/vdb', + 'volumeId': 'Work', + 'tag': 'test-tag'}}) + self.assertIn('test-tag', out) - def test_volume_update(self): - self.run_command('volume-update sample-server Work Work') + def test_volume_attachments_pre_v2_79(self): + out = self.run_command( + 'volume-attachments 1234', api_version='2.78')[0] + self.assert_called('GET', '/servers/1234/os-volume_attachments') + self.assertNotIn('DELETE ON TERMINATION', out) + + def test_volume_attachments_v2_79(self): + out = self.run_command( + 'volume-attachments 1234', api_version='2.79')[0] + self.assert_called('GET', '/servers/1234/os-volume_attachments') + self.assertIn('DELETE ON TERMINATION', out) + + def test_volume_attachments_pre_v2_89(self): + out = self.run_command( + 'volume-attachments 1234', api_version='2.88')[0] + self.assert_called('GET', '/servers/1234/os-volume_attachments') + # We can't assert just ID here as it's part of various other fields + self.assertIn('| ID', out) + self.assertNotIn('ATTACHMENT ID', out) + self.assertNotIn('BDM UUID', out) + + def test_volume_attachments_v2_89(self): + out = self.run_command( + 'volume-attachments 1234', api_version='2.89')[0] + self.assert_called('GET', '/servers/1234/os-volume_attachments') + # We can't assert just ID here as it's part of various other fields + self.assertNotIn('| ID', out) + self.assertIn('ATTACHMENT ID', out) + self.assertIn('BDM UUID', out) + + def test_volume_attach_with_delete_on_termination_pre_v2_79(self): + self.assertRaises( + SystemExit, self.run_command, + 'volume-attach --delete-on-termination sample-server ' + 'Work /dev/vdb', api_version='2.78') + + def test_volume_attach_with_delete_on_termination_v2_79(self): + out = self.run_command( + 'volume-attach --delete-on-termination sample-server ' + '2 /dev/vdb', api_version='2.79')[0] + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'device': '/dev/vdb', + 'volumeId': '2', + 'delete_on_termination': True}}) + self.assertIn('delete_on_termination', out) + + def test_volume_attach_without_delete_on_termination(self): + self.run_command('volume-attach sample-server Work', + api_version='2.79') + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'volumeId': 'Work'}}) + + def test_volume_update_pre_v285(self): + """Before microversion 2.85, we should keep the original behavior""" + self.run_command('volume-update sample-server Work Work', + api_version='2.84') + self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', + {'volumeAttachment': {'volumeId': 'Work'}}) + + def test_volume_update_swap_v285(self): + """Microversion 2.85, we should also keep the original behavior.""" + self.run_command('volume-update sample-server Work Work', + api_version='2.85') self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', {'volumeAttachment': {'volumeId': 'Work'}}) + def test_volume_update_delete_on_termination_pre_v285(self): + self.assertRaises( + SystemExit, self.run_command, + 'volume-update sample-server --delete-on-termination Work Work', + api_version='2.84') + + def test_volume_update_no_delete_on_termination_pre_v285(self): + self.assertRaises( + SystemExit, self.run_command, + 'volume-update sample-server --no-delete-on-termination Work Work', + api_version='2.84') + + def test_volume_update_v285(self): + self.run_command('volume-update sample-server --delete-on-termination ' + 'Work Work', api_version='2.85') + body = {'volumeAttachment': + {'volumeId': 'Work', 'delete_on_termination': True}} + self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', + body) + + self.run_command('volume-update sample-server ' + '--no-delete-on-termination ' + 'Work Work', api_version='2.85') + body = {'volumeAttachment': + {'volumeId': 'Work', 'delete_on_termination': False}} + self.assert_called('PUT', '/servers/1234/os-volume_attachments/Work', + body) + + def test_volume_update_v285_conflicting(self): + self.assertRaises( + SystemExit, self.run_command, + 'volume-update sample-server --delete-on-termination ' + '--no-delete-on-termination Work Work', + api_version='2.85') + def test_volume_detach(self): self.run_command('volume-detach sample-server Work') self.assert_called('DELETE', @@ -2873,31 +4203,199 @@ def test_instance_action_get(self): 'GET', '/servers/1234/os-instance-actions/req-abcde12345') - def test_cell_show(self): - self.run_command('cell-show child_cell') - self.assert_called('GET', '/os-cells/child_cell') + def test_instance_action_list_marker_pre_v258_not_allowed(self): + cmd = 'instance-action-list sample-server --marker %s' + self.assertRaises(SystemExit, self.run_command, + cmd % FAKE_UUID_1, api_version='2.57') + + def test_instance_action_list_limit_pre_v258_not_allowed(self): + cmd = 'instance-action-list sample-server --limit 10' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.57') + + def test_instance_action_list_changes_since_pre_v258_not_allowed(self): + cmd = 'instance-action-list sample-server --changes-since ' \ + '2016-02-29T06:23:22' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.57') + + def test_instance_action_list_limit_marker_v258(self): + out = self.run_command('instance-action-list sample-server --limit 10 ' + '--marker %s' % FAKE_UUID_1, + api_version='2.58')[0] + # Assert that the updated_at value is in the output. + self.assertIn('2013-03-25T13:50:09.000000', out) + self.assert_called( + 'GET', + '/servers/1234/os-instance-actions?' + 'limit=10&marker=%s' % FAKE_UUID_1) + + def test_instance_action_list_with_changes_since_v258(self): + self.run_command('instance-action-list sample-server ' + '--changes-since 2016-02-29T06:23:22', + api_version='2.58') + self.assert_called( + 'GET', + '/servers/1234/os-instance-actions?' + 'changes-since=2016-02-29T06%3A23%3A22') + + def test_instance_action_list_with_changes_since_invalid_value_v258(self): + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'instance-action-list sample-server --changes-since 0123456789', + api_version='2.58') + self.assertIn('Invalid changes-since value', str(ex)) + + def test_instance_action_list_changes_before_pre_v266_not_allowed(self): + cmd = 'instance-action-list sample-server --changes-before ' \ + '2016-02-29T06:23:22' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.65') + + def test_instance_action_list_with_changes_before_v266(self): + self.run_command('instance-action-list sample-server ' + '--changes-before 2016-02-29T06:23:22', + api_version='2.66') + self.assert_called( + 'GET', + '/servers/1234/os-instance-actions?' + 'changes-before=2016-02-29T06%3A23%3A22') + + def test_instance_action_list_with_changes_before_invalid_value_v266(self): + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'instance-action-list sample-server --changes-before 0123456789', + api_version='2.66') + self.assertIn('Invalid changes-before value', str(ex)) - def test_cell_capacities_with_cell_name(self): - self.run_command('cell-capacities --cell child_cell') - self.assert_called('GET', '/os-cells/child_cell/capacities') + def test_instance_usage_audit_log(self): + self.run_command('instance-usage-audit-log') + self.assert_called('GET', '/os-instance_usage_audit_log') - def test_cell_capacities_without_cell_name(self): - self.run_command('cell-capacities') - self.assert_called('GET', '/os-cells/capacities') + def test_instance_usage_audit_log_with_before(self): + self.run_command( + ["instance-usage-audit-log", "--before", + "2016-12-10 13:59:59.999999"]) + self.assert_called('GET', '/os-instance_usage_audit_log' + '/2016-12-10%2013%3A59%3A59.999999') def test_migration_list(self): self.run_command('migration-list') self.assert_called('GET', '/os-migrations') def test_migration_list_v223(self): - self.run_command('migration-list', api_version="2.23") + out, _ = self.run_command('migration-list', api_version="2.23") self.assert_called('GET', '/os-migrations') + # Make sure there is no UUID in the output. Uses "| UUID" to + # avoid collisions with the "Instance UUID" column. + self.assertNotIn('| UUID', out) def test_migration_list_with_filters(self): self.run_command('migration-list --host host1 --status finished') self.assert_called('GET', '/os-migrations?host=host1&status=finished') + def test_migration_list_marker_pre_v259_not_allowed(self): + cmd = 'migration-list --marker %s' + self.assertRaises(SystemExit, self.run_command, + cmd % FAKE_UUID_1, api_version='2.58') + + def test_migration_list_limit_pre_v259_not_allowed(self): + cmd = 'migration-list --limit 10' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.58') + + def test_migration_list_changes_since_pre_v259_not_allowed(self): + cmd = 'migration-list --changes-since 2016-02-29T06:23:22' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.58') + + def test_migration_list_limit_marker_v259(self): + out, _ = self.run_command( + 'migration-list --limit 10 --marker %s' % FAKE_UUID_1, + api_version='2.59') + self.assert_called( + 'GET', + '/os-migrations?limit=10&marker=%s' % FAKE_UUID_1) + # Make sure the UUID column is now in the output. Uses "| UUID" to + # avoid collisions with the "Instance UUID" column. + self.assertIn('| UUID', out) + + def test_migration_list_with_changes_since_v259(self): + self.run_command('migration-list --changes-since 2016-02-29T06:23:22', + api_version='2.59') + self.assert_called( + 'GET', '/os-migrations?changes-since=2016-02-29T06%3A23%3A22') + + def test_migration_list_with_changes_since_invalid_value_v259(self): + ex = self.assertRaises(exceptions.CommandError, self.run_command, + 'migration-list --changes-since 0123456789', + api_version='2.59') + self.assertIn('Invalid changes-since value', str(ex)) + + def test_migration_list_with_changes_before_v266(self): + self.run_command('migration-list --changes-before 2016-02-29T06:23:22', + api_version='2.66') + self.assert_called( + 'GET', '/os-migrations?changes-before=2016-02-29T06%3A23%3A22') + + def test_migration_list_with_changes_before_invalid_value_v266(self): + ex = self.assertRaises(exceptions.CommandError, self.run_command, + 'migration-list --changes-before 0123456789', + api_version='2.66') + self.assertIn('Invalid changes-before value', str(ex)) + + def test_migration_list_with_changes_before_pre_v266_not_allowed(self): + cmd = 'migration-list --changes-before 2016-02-29T06:23:22' + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.65') + + def test_migration_list_with_user_id_v280(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + out = self.run_command('migration-list --user-id %s' % user_id, + api_version='2.80')[0] + self.assert_called('GET', '/os-migrations?user_id=%s' % user_id) + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + + def test_migration_list_with_project_id_v280(self): + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + out = self.run_command('migration-list --project-id %s' % project_id, + api_version='2.80')[0] + self.assert_called('GET', '/os-migrations?project_id=%s' % project_id) + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + + def test_migration_list_with_user_and_project_id_v280(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + out = self.run_command('migration-list --project-id %(project_id)s ' + '--user-id %(user_id)s' % + {'user_id': user_id, 'project_id': project_id}, + api_version='2.80')[0] + self.assert_called('GET', '/os-migrations?project_id=%s&user_id=%s' + % (project_id, user_id)) + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + + def test_migration_list_with_user_id_pre_v280_not_allowed(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + cmd = 'migration-list --user-id %s' % user_id + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.79') + + def test_migration_list_with_project_id_pre_v280_not_allowed(self): + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + cmd = 'migration-list --project-id %s' % project_id + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.79') + + def test_migration_list_pre_v280(self): + out = self.run_command('migration-list', api_version='2.79')[0] + self.assert_called('GET', '/os-migrations') + self.assertNotIn('User ID', out) + self.assertNotIn('Project ID', out) + @mock.patch('novaclient.v2.shell._find_server') @mock.patch('os.system') def test_ssh(self, mock_system, mock_find_server): @@ -3021,7 +4519,7 @@ def test_keypair_import_x509(self): api_version="2.2") def test_keypair_stdin(self): - with mock.patch('sys.stdin', six.StringIO('FAKE_PUBLIC_KEY')): + with mock.patch('sys.stdin', io.StringIO('FAKE_PUBLIC_KEY')): self.run_command('keypair-add --pub-key - test', api_version="2.2") self.assert_called( 'POST', '/os-keypairs', { @@ -3062,6 +4560,52 @@ def test_create_server_group(self): {'server_group': {'name': 'wjsg', 'policies': ['affinity']}}) + def test_create_server_group_v2_64(self): + self.run_command('server-group-create sg1 affinity', + api_version='2.64') + self.assert_called('POST', '/os-server-groups', + {'server_group': { + 'name': 'sg1', + 'policy': 'affinity' + }}) + + def test_create_server_group_with_rules(self): + self.run_command('server-group-create sg1 anti-affinity ' + '--rule max_server_per_host=3', api_version='2.64') + self.assert_called('POST', '/os-server-groups', + {'server_group': { + 'name': 'sg1', + 'policy': 'anti-affinity', + 'rules': {'max_server_per_host': 3} + }}) + + def test_create_server_group_with_multi_rules(self): + self.run_command('server-group-create sg1 anti-affinity ' + '--rule a=b --rule c=d', api_version='2.64') + self.assert_called('POST', '/os-server-groups', + {'server_group': { + 'name': 'sg1', + 'policy': 'anti-affinity', + 'rules': {'a': 'b', 'c': 'd'} + }}) + + def test_create_server_group_with_invalid_value(self): + result = self.assertRaises( + exceptions.CommandError, self.run_command, + 'server-group-create sg1 anti-affinity ' + '--rule max_server_per_host=foo', api_version='2.64') + self.assertIn("Invalid 'max_server_per_host' value: foo", + str(result)) + + def test_create_server_group_with_rules_pre_264(self): + self.assertRaises(SystemExit, self.run_command, + 'server-group-create sg1 anti-affinity ' + '--rule max_server_per_host=3', api_version='2.63') + + def test_create_server_group_with_multiple_policies(self): + self.assertRaises(SystemExit, self.run_command, + 'server-group-create wjsg affinity anti-affinity') + def test_delete_multi_server_groups(self): self.run_command('server-group-delete 12345 56789') self.assert_called('DELETE', '/os-server-groups/56789') @@ -3079,12 +4623,6 @@ def test_list_server_group_with_limit_and_offset(self): self.run_command('server-group-list --limit 20 --offset 5') self.assert_called('GET', '/os-server-groups?limit=20&offset=5') - def test_list_server_os_virtual_interfaces(self): - _, err = self.run_command('virtual-interface-list 1234') - self.assertIn('WARNING: Command virtual-interface-list is deprecated', - err) - self.assert_called('GET', '/servers/1234/os-virtual-interfaces') - def test_versions(self): exclusions = set([ 1, # Same as version 2.0 @@ -3093,6 +4631,10 @@ def test_versions(self): 5, # doesn't require any changes in novaclient 7, # doesn't require any changes in novaclient 9, # doesn't require any changes in novaclient + 12, # no longer supported + 13, # 13 adds information ``project_id`` and ``user_id`` to + # ``os-server-groups``, but is not explicitly tested + # via wraps and _SUBSTITUTIONS. 15, # doesn't require any changes in novaclient 16, # doesn't require any changes in novaclient 18, # NOTE(andreykurilin): this microversion requires changes in @@ -3102,8 +4644,9 @@ def test_versions(self): # before feature-freeze # (we can do it, since nova-api change didn't actually add # new microversion, just an additional checks. See - # https://review.openstack.org/#/c/233076/ for more details) + # https://review.opendev.org/#/c/233076/ for more details) 20, # doesn't require any changes in novaclient + 21, # doesn't require any changes in novaclient 27, # NOTE(cdent): 27 adds support for updated microversion # headers, and is tested in test_api_versions, but is # not explicitly tested via wraps and _SUBSTITUTIONS. @@ -3126,15 +4669,51 @@ def test_versions(self): 48, # There are no version-wrapped shell method changes for this. 51, # There are no version-wrapped shell method changes for this. 52, # There are no version-wrapped shell method changes for this. + 54, # There are no version-wrapped shell method changes for this. + 57, # There are no version-wrapped shell method changes for this. + 60, # There are no client-side changes for volume multiattach. + 61, # There are no version-wrapped shell method changes for this. + 62, # There are no version-wrapped shell method changes for this. + 63, # There are no version-wrapped shell method changes for this. + 65, # There are no version-wrapped shell method changes for this. + 67, # There are no version-wrapped shell method changes for this. + 69, # NOTE(tssurya): 2.69 adds support for missing keys in the + # responses of `GET /servers``, ``GET /servers/detail``, + # ``GET /servers/{server_id}`` and ``GET /os-services`` when + # a cell is down to return minimal constructs. From 2.69 and + # upwards, if the response for ``GET /servers/detail`` does + # not have the 'flavor' key for those instances in the down + # cell, they will be handled on the client side by being + # skipped when forming the detailed lists for embedded + # flavor information. + 70, # There are no version-wrapped shell method changes for this. + 71, # There are no version-wrapped shell method changes for this. + 72, # There are no version-wrapped shell method changes for this. + 74, # There are no version-wrapped shell method changes for this. + 75, # There are no version-wrapped shell method changes for this. + 76, # doesn't require any changes in novaclient. + 77, # There are no version-wrapped shell method changes for this. + 82, # There are no version-wrapped shell method changes for this. + 83, # There are no version-wrapped shell method changes for this. + 84, # There are no version-wrapped shell method changes for this. + 86, # doesn't require any changes in novaclient. + 87, # doesn't require any changes in novaclient. + 89, # There are no version-wrapped shell method changes for this. + 93, # There are no version-wrapped shell method changes for this. + 94, # There are no version-wrapped shell method changes for this. + 95, # There are no version-wrapped shell method changes for this. + 96, # There are no version-wrapped shell method changes for this. ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) versions_covered = set() for key, values in api_versions._SUBSTITUTIONS.items(): - for value in values: - if value.start_version.ver_major == 2: - versions_covered.add(value.start_version.ver_minor) + # Exclude version-wrapped + if 'novaclient.tests' not in key: + for value in values: + if value.start_version.ver_major == 2: + versions_covered.add(value.start_version.ver_minor) versions_not_covered = versions_supported - versions_covered unaccounted_for = versions_not_covered - exclusions @@ -3203,6 +4782,90 @@ def test_list_v2_26_not_tags_any(self): self.run_command('list --not-tags-any tag1,tag2', api_version='2.26') self.assert_called('GET', '/servers/detail?not-tags-any=tag1%2Ctag2') + def test_list_detail_v269_with_down_cells(self): + """Tests nova list at the 2.69 microversion.""" + stdout, _stderr = self.run_command('list', api_version='2.69') + self.assertIn( + '''\ ++------+----------------+---------+------------+-------------+----------------------------------------------+ +| ID | Name | Status | Task State | Power State | Networks | ++------+----------------+---------+------------+-------------+----------------------------------------------+ +| 9015 | | UNKNOWN | N/A | N/A | | +| 9014 | help | ACTIVE | N/A | N/A | | +| 1234 | sample-server | BUILD | N/A | N/A | private=10.11.12.13; public=1.2.3.4, 5.6.7.8 | +| 5678 | sample-server2 | ACTIVE | N/A | N/A | private=10.13.12.13; public=4.5.6.7, 5.6.9.8 | +| 9012 | sample-server3 | ACTIVE | N/A | N/A | private=10.13.12.13; public=4.5.6.7, 5.6.9.8 | +| 9013 | sample-server4 | ACTIVE | N/A | N/A | | ++------+----------------+---------+------------+-------------+----------------------------------------------+ +''', # noqa + stdout, + ) + self.assert_called('GET', '/servers/detail') + + def test_list_v269_with_down_cells(self): + stdout, _stderr = self.run_command( + 'list --minimal', api_version='2.69') + expected = '''\ ++------+----------------+ +| ID | Name | ++------+----------------+ +| 9015 | | +| 9014 | help | +| 1234 | sample-server | +| 5678 | sample-server2 | ++------+----------------+ +''' + self.assertEqual(expected, stdout) + self.assert_called('GET', '/servers') + + def test_show_v269_with_down_cells(self): + stdout, _stderr = self.run_command('show 9015', api_version='2.69') + self.assertEqual( + '''\ ++-----------------------------+---------------------------------------------------+ +| Property | Value | ++-----------------------------+---------------------------------------------------+ +| OS-EXT-AZ:availability_zone | geneva | +| OS-EXT-STS:power_state | 0 | +| created | 2018-12-03T21:06:18Z | +| flavor:disk | 1 | +| flavor:ephemeral | 0 | +| flavor:extra_specs | {} | +| flavor:original_name | m1.tiny | +| flavor:ram | 512 | +| flavor:swap | 0 | +| flavor:vcpus | 1 | +| id | 9015 | +| image | CentOS 5.2 (c99d7632-bd66-4be9-aed5-3dd14b223a76) | +| status | UNKNOWN | +| tenant_id | 6f70656e737461636b20342065766572 | +| user_id | fake | ++-----------------------------+---------------------------------------------------+ +''', # noqa + stdout, + ) + FAKE_UUID_2 = 'c99d7632-bd66-4be9-aed5-3dd14b223a76' + self.assert_called('GET', '/servers?name=9015', pos=0) + self.assert_called('GET', '/servers?name=9015', pos=1) + self.assert_called('GET', '/servers/9015', pos=2) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=3) + + def test_list_pre_v273(self): + exp = self.assertRaises(SystemExit, + self.run_command, + 'list --locked t', + api_version='2.72') + self.assertEqual(2, exp.code) + + def test_list_v273(self): + self.run_command('list --locked t', api_version='2.73') + self.assert_called('GET', '/servers/detail?locked=t') + + def test_list_v273_with_sort_key_dir(self): + self.run_command('list --sort locked:asc', api_version='2.73') + self.assert_called( + 'GET', '/servers/detail?sort_dir=asc&sort_key=locked') + class PollForStatusTestCase(utils.TestCase): @mock.patch("novaclient.v2.shell.time") @@ -3289,3 +4952,27 @@ def test_error_state(self, mock_time): action=action, show_progress=True, silent=False) + + +class TestUtilMethods(utils.TestCase): + def setUp(self): + super(TestUtilMethods, self).setUp() + self.shell = self.useFixture(ShellFixture()).shell + # NOTE(danms): Get a client that we can use to call things outside of + # the shell main + self.shell.cs = fakes.FakeClient('2.1') + + def test_find_images(self): + """Test find_images() with a name and id.""" + images = novaclient.v2.shell._find_images(self.shell.cs, + [FAKE_UUID_1, + 'back1']) + self.assertEqual(2, len(images)) + self.assertEqual(FAKE_UUID_1, images[0].id) + self.assertEqual(fakes.FAKE_IMAGE_UUID_BACKUP, images[1].id) + + def test_find_images_missing(self): + """Test find_images() where one of the images is not found.""" + self.assertRaises(exceptions.CommandError, + novaclient.v2.shell._find_images, + self.shell.cs, [FAKE_UUID_1, 'foo']) diff --git a/novaclient/tests/unit/v2/test_usage.py b/novaclient/tests/unit/v2/test_usage.py index 900842334..c7473aa9d 100644 --- a/novaclient/tests/unit/v2/test_usage.py +++ b/novaclient/tests/unit/v2/test_usage.py @@ -13,8 +13,6 @@ import datetime -import six - from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -60,8 +58,8 @@ def test_usage_get(self): self.assertIsInstance(u, usage.Usage) def test_usage_class_get(self): - start = six.u('2012-01-22T19:48:41.750722') - stop = six.u('2012-01-22T19:48:41.750722') + start = '2012-01-22T19:48:41.750722' + stop = '2012-01-22T19:48:41.750722' info = {'tenant_id': 'tenantfoo', 'start': start, 'stop': stop} diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py index 63d63c1a3..23b72840e 100644 --- a/novaclient/tests/unit/v2/test_versions.py +++ b/novaclient/tests/unit/v2/test_versions.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from novaclient import api_versions from novaclient import exceptions as exc diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py index 932b71d79..c06661186 100644 --- a/novaclient/tests/unit/v2/test_volumes.py +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -13,7 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +from unittest import mock from novaclient import api_versions from novaclient.tests.unit import utils @@ -126,3 +126,56 @@ def test_delete_server_volume_with_warn(self, mock_warn): volume_id=None, attachment_id="Work") mock_warn.assert_called_once() + + +class VolumesV279Test(VolumesV249Test): + api_version = "2.79" + + def test_create_server_volume_with_delete_on_termination(self): + v = self.cs.volumes.create_server_volume( + server_id=1234, + volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', + device='/dev/vdb', + tag='tag1', + delete_on_termination=True + ) + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': { + 'volumeId': '15e59938-07d5-11e1-90e3-e3dffe0c5983', + 'device': '/dev/vdb', + 'tag': 'tag1', + 'delete_on_termination': True}}) + self.assertIsInstance(v, volumes.Volume) + + def test_create_server_volume_with_delete_on_termination_pre_v279(self): + self.cs.api_version = api_versions.APIVersion('2.78') + ex = self.assertRaises( + TypeError, self.cs.volumes.create_server_volume, "1234", + volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', + delete_on_termination=True) + self.assertIn('delete_on_termination', str(ex)) + + +class VolumesV285Test(VolumesV279Test): + api_version = "2.85" + + def test_volume_update_server_volume(self): + v = self.cs.volumes.update_server_volume( + server_id=1234, + src_volid='Work', + dest_volid='Work', + delete_on_termination=True + ) + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('PUT', + '/servers/1234/os-volume_attachments/Work') + self.assertIsInstance(v, volumes.Volume) + + def test_volume_update_server_volume_pre_v285(self): + self.cs.api_version = api_versions.APIVersion('2.84') + ex = self.assertRaises( + TypeError, self.cs.volumes.update_server_volume, "1234", + 'Work', 'Work', delete_on_termination=True) + self.assertIn('delete_on_termination', str(ex)) diff --git a/novaclient/utils.py b/novaclient/utils.py index 451b2372d..d0219795c 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -12,18 +12,16 @@ # under the License. import contextlib -import json import os import re import textwrap import time -import uuid +from urllib import parse from oslo_serialization import jsonutils from oslo_utils import encodeutils +from oslo_utils import uuidutils import prettytable -import six -from six.moves.urllib import parse from novaclient import exceptions from novaclient.i18n import _ @@ -118,50 +116,14 @@ def inner(f): return inner -def add_resource_manager_extra_kwargs_hook(f, hook): - """Add hook to bind CLI arguments to ResourceManager calls. +def pretty_choice_list(values): + return ', '.join("'%s'" % x for x in values) - The `do_foo` calls in shell.py will receive CLI args and then in turn pass - them through to the ResourceManager. Before passing through the args, the - hooks registered here will be called, giving us a chance to add extra - kwargs (taken from the command-line) to what's passed to the - ResourceManager. - """ - if not hasattr(f, 'resource_manager_kwargs_hooks'): - f.resource_manager_kwargs_hooks = [] - - names = [h.__name__ for h in f.resource_manager_kwargs_hooks] - if hook.__name__ not in names: - f.resource_manager_kwargs_hooks.append(hook) - - -def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False): - """Return extra_kwargs by calling resource manager kwargs hooks.""" - hooks = getattr(f, "resource_manager_kwargs_hooks", []) - extra_kwargs = {} - for hook in hooks: - hook_kwargs = hook(args) - hook_name = hook.__name__ - conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) - if conflicting_keys and not allow_conflicts: - msg = (_("Hook '%(hook_name)s' is attempting to redefine " - "attributes '%(conflicting_keys)s'") % - {'hook_name': hook_name, - 'conflicting_keys': conflicting_keys}) - raise exceptions.NoUniqueMatch(msg) - - extra_kwargs.update(hook_kwargs) - - return extra_kwargs - - -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) - -def pretty_choice_dict(d): +def pretty_choice_dict(values): """Returns a formatted dict as 'key=value'.""" - return pretty_choice_list(['%s=%s' % (k, d[k]) for k in sorted(d.keys())]) + return pretty_choice_list( + ['%s=%s' % (k, values[k]) for k in sorted(values)]) def print_list(objs, fields, formatters={}, sortby_index=None): @@ -187,7 +149,7 @@ def print_list(objs, fields, formatters={}, sortby_index=None): if data is None: data = '-' # '\r' would break the table, so remove it. - data = six.text_type(data).replace("\r", "") + data = str(data).replace("\r", "") row.append(data) pt.add_row(row) @@ -196,8 +158,7 @@ def print_list(objs, fields, formatters={}, sortby_index=None): else: result = encodeutils.safe_encode(pt.get_string()) - if six.PY3: - result = result.decode() + result = result.decode() print(result) @@ -234,9 +195,9 @@ def flatten_dict(data): data = data.copy() # Try and decode any nested JSON structures. for key, value in data.items(): - if isinstance(value, six.string_types): + if isinstance(value, str): try: - data[key] = json.loads(value) + data[key] = jsonutils.loads(value) except ValueError: pass @@ -251,10 +212,10 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): if isinstance(v, (dict, list)): v = jsonutils.dumps(v, ensure_ascii=False) if wrap > 0: - v = textwrap.fill(six.text_type(v), wrap) + v = textwrap.fill(str(v), wrap) # if value has a newline, add in multiple rows # e.g. fault with stacktrace - if v and isinstance(v, six.string_types) and (r'\n' in v or '\r' in v): + if v and isinstance(v, str) and (r'\n' in v or '\r' in v): # '\r' would break the table, so remove it. if '\r' in v: v = v.replace('\r', '') @@ -270,8 +231,7 @@ def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): result = encodeutils.safe_encode(pt.get_string()) - if six.PY3: - result = result.decode() + result = result.decode() print(result) @@ -290,12 +250,11 @@ def find_resource(manager, name_or_id, wrap_exception=True, **find_args): try: tmp_id = encodeutils.safe_encode(name_or_id) - if six.PY3: - tmp_id = tmp_id.decode() + tmp_id = tmp_id.decode() - uuid.UUID(tmp_id) - return manager.get(tmp_id) - except (TypeError, ValueError, exceptions.NotFound): + if uuidutils.is_uuid_like(tmp_id): + return manager.get(tmp_id) + except (TypeError, exceptions.NotFound): pass # then try to get entity as name @@ -399,6 +358,18 @@ def safe_issubclass(*args): return False +def _get_resource_string(resource): + if hasattr(resource, 'human_id') and resource.human_id: + if hasattr(resource, 'id') and resource.id: + return "%s (%s)" % (resource.human_id, resource.id) + else: + return resource.human_id + elif hasattr(resource, 'id') and resource.id: + return resource.id + else: + return resource + + def do_action_on_many(action, resources, success_msg, error_msg): """Helper to run an action on many resources.""" failure_flag = False @@ -406,10 +377,10 @@ def do_action_on_many(action, resources, success_msg, error_msg): for resource in resources: try: action(resource) - print(success_msg % resource) + print(success_msg % _get_resource_string(resource)) except Exception as e: failure_flag = True - print(encodeutils.safe_encode(six.text_type(e))) + print(encodeutils.safe_encode(str(e))) if failure_flag: raise exceptions.CommandError(error_msg) @@ -454,5 +425,15 @@ def record_time(times, enabled, *args): def prepare_query_string(params): """Convert dict params to query string""" + # Transform the dict to a sequence of two-element tuples in fixed + # order, then the encoded string will be consistent in Python 2&3. + if not params: + return '' params = sorted(params.items(), key=lambda x: x[0]) return '?%s' % parse.urlencode(params) if params else '' + + +def get_url_with_filter(url, filters): + query_string = prepare_query_string(filters) + url = "%s%s" % (url, query_string) + return url diff --git a/novaclient/v2/agents.py b/novaclient/v2/agents.py index 0a6c223e5..dac3ff214 100644 --- a/novaclient/v2/agents.py +++ b/novaclient/v2/agents.py @@ -19,6 +19,11 @@ from novaclient import base +# NOTE(takashin): The os-agents APIs have been removed +# in https://review.opendev.org/c/openstack/nova/+/749309 . +# But the following API bindings remains as there are +# because the python-openstackclient depends on them. + class Agent(base.Resource): def __repr__(self): diff --git a/novaclient/v2/aggregates.py b/novaclient/v2/aggregates.py index 9d4dff822..d2cbaa858 100644 --- a/novaclient/v2/aggregates.py +++ b/novaclient/v2/aggregates.py @@ -15,6 +15,7 @@ """Aggregate interface.""" +from novaclient import api_versions from novaclient import base @@ -45,6 +46,10 @@ def delete(self): """ return self.manager.delete(self) + @api_versions.wraps("2.81") + def cache_images(self, images): + return self.manager.cache_images(self, images) + class AggregateManager(base.ManagerWithFind): resource_class = Aggregate @@ -103,3 +108,20 @@ def delete(self, aggregate): :returns: An instance of novaclient.base.TupleWithMeta """ return self._delete('/os-aggregates/%s' % (base.getid(aggregate))) + + @api_versions.wraps("2.81") + def cache_images(self, aggregate, images): + """ + Request images be cached on a given aggregate. + + :param aggregate: The aggregate to target + :param images: A list of image IDs to request caching + :returns: An instance of novaclient.base.TupleWithMeta + """ + body = { + 'cache': [{'id': base.getid(image)} for image in images], + } + resp, body = self.api.client.post( + "/os-aggregates/%s/images" % base.getid(aggregate), + body=body) + return self.convert_into_with_meta(body, resp) diff --git a/novaclient/v2/assisted_volume_snapshots.py b/novaclient/v2/assisted_volume_snapshots.py index 79807897d..9e0675a93 100644 --- a/novaclient/v2/assisted_volume_snapshots.py +++ b/novaclient/v2/assisted_volume_snapshots.py @@ -16,7 +16,7 @@ Assisted volume snapshots - to be used by Cinder and not end users. """ -import json +from oslo_serialization import jsonutils from novaclient import base @@ -51,4 +51,5 @@ def delete(self, snapshot, delete_info): :returns: An instance of novaclient.base.TupleWithMeta """ return self._delete("/os-assisted-volume-snapshots/%s?delete_info=%s" % - (base.getid(snapshot), json.dumps(delete_info))) + (base.getid(snapshot), + jsonutils.dumps(delete_info))) diff --git a/novaclient/v2/cells.py b/novaclient/v2/cells.py deleted file mode 100644 index e6a166671..000000000 --- a/novaclient/v2/cells.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient import base - - -class Cell(base.Resource): - def __repr__(self): - return "" % self.name - - -class CellsManager(base.Manager): - resource_class = Cell - - def get(self, cell_name): - """ - Get a cell. - - :param cell_name: Name of the :class:`Cell` to get. - :rtype: :class:`Cell` - """ - return self._get("/os-cells/%s" % cell_name, "cell") - - def capacities(self, cell_name=None): - """ - Get capacities for a cell. - - :param cell_name: Name of the :class:`Cell` to get capacities for. - :rtype: :class:`Cell` - """ - path = ["%s/capacities" % cell_name, "capacities"][cell_name is None] - return self._get("/os-cells/%s" % path, "cell") diff --git a/novaclient/v2/certs.py b/novaclient/v2/certs.py deleted file mode 100644 index 655af54db..000000000 --- a/novaclient/v2/certs.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -DEPRECATED Certificate interface. -""" - -import warnings - -from novaclient import base -from novaclient.i18n import _ - -CERT_DEPRECATION_WARNING = ( - _('The nova-cert service is deprecated. This API binding will be removed ' - 'in the first major release after the Nova server 16.0.0 Pike release.') -) - - -class Certificate(base.Resource): - """DEPRECATED""" - def __repr__(self): - return ("" % - (len(self.private_key) if self.private_key else 0, - len(self.data))) - - -class CertificateManager(base.Manager): - """DEPRECATED Manage :class:`Certificate` resources.""" - resource_class = Certificate - - def create(self): - """DEPRECATED Create a x509 certificate for a user in tenant.""" - warnings.warn(CERT_DEPRECATION_WARNING, DeprecationWarning) - return self._create('/os-certificates', {}, 'certificate') - - def get(self): - """DEPRECATED Get root certificate.""" - warnings.warn(CERT_DEPRECATION_WARNING, DeprecationWarning) - return self._get("/os-certificates/root", 'certificate') diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py index b85077392..e7b677b72 100644 --- a/novaclient/v2/client.py +++ b/novaclient/v2/client.py @@ -22,19 +22,14 @@ from novaclient.v2 import aggregates from novaclient.v2 import assisted_volume_snapshots from novaclient.v2 import availability_zones -from novaclient.v2 import cells -from novaclient.v2 import certs -from novaclient.v2 import cloudpipe -from novaclient.v2 import contrib from novaclient.v2 import flavor_access from novaclient.v2 import flavors -from novaclient.v2 import hosts from novaclient.v2 import hypervisors from novaclient.v2 import images from novaclient.v2 import instance_action +from novaclient.v2 import instance_usage_audit_log from novaclient.v2 import keypairs from novaclient.v2 import limits -from novaclient.v2 import list_extensions from novaclient.v2 import migrations from novaclient.v2 import networks from novaclient.v2 import quota_classes @@ -46,7 +41,6 @@ from novaclient.v2 import services from novaclient.v2 import usage from novaclient.v2 import versions -from novaclient.v2 import virtual_interfaces from novaclient.v2 import volumes @@ -149,18 +143,13 @@ def __init__(self, # extensions self.agents = agents.AgentsManager(self) - self.cloudpipe = cloudpipe.CloudpipeManager(self) - self.certs = certs.CertificateManager(self) self.volumes = volumes.VolumeManager(self) self.keypairs = keypairs.KeypairManager(self) self.neutron = networks.NeutronManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.usage = usage.UsageManager(self) - self.virtual_interfaces = \ - virtual_interfaces.VirtualInterfaceManager(self) self.aggregates = aggregates.AggregateManager(self) - self.hosts = hosts.HostManager(self) self.hypervisors = hypervisors.HypervisorManager(self) self.hypervisor_stats = hypervisors.HypervisorStatsManager(self) self.services = services.ServiceManager(self) @@ -176,9 +165,9 @@ def __init__(self, # deprecated now, which is why it is not initialized by default. self.assisted_volume_snapshots = \ assisted_volume_snapshots.AssistedSnapshotManager(self) - self.cells = cells.CellsManager(self) self.instance_action = instance_action.InstanceActionManager(self) - self.list_extensions = list_extensions.ListExtManager(self) + self.instance_usage_audit_log = \ + instance_usage_audit_log.InstanceUsageAuditLogManager(self) self.migrations = migrations.MigrationManager(self) self.server_external_events = \ server_external_events.ServerExternalEventManager(self) @@ -188,15 +177,6 @@ def __init__(self, # Add in any extensions... if extensions: for extension in extensions: - # do not import extensions from contrib directory twice. - if extension.name in contrib.V2_0_EXTENSIONS: - # NOTE(andreykurilin): this message looks more like - # warning or note, but it is not critical, so let's do - # not flood "warning" logging level and use just debug.. - self.logger.debug("Nova 2.0 extenstion '%s' is auto-loaded" - " by default. You do not need to specify" - " it manually.", extension.name) - continue if extension.manager_class: setattr(self, extension.name, extension.manager_class(self)) @@ -239,50 +219,17 @@ def api_version(self): def api_version(self, value): self.client.api_version = value - @property - def projectid(self): - self.logger.warning(_("Property 'projectid' is deprecated since " - "Ocata. Use 'project_name' instead.")) - return self.project_name - - @property - def tenant_id(self): - self.logger.warning(_("Property 'tenant_id' is deprecated since " - "Ocata. Use 'project_id' instead.")) - return self.project_id - def __enter__(self): - self.logger.warning(_("NovaClient instance can't be used as a " - "context manager since Ocata (deprecated " - "behaviour) since it is redundant in case of " - "SessionClient.")) - return self + raise exceptions.InvalidUsage(_( + "NovaClient instance can't be used as a context manager " + "since it is redundant in case of SessionClient.")) def __exit__(self, t, v, tb): # do not do anything pass - def set_management_url(self, url): - self.logger.warning( - _("Method `set_management_url` is deprecated since Ocata. " - "Use `endpoint_override` argument instead while initializing " - "novaclient's instance.")) - self.client.set_management_url(url) - def get_timings(self): return self.client.get_timings() def reset_timings(self): self.client.reset_timings() - - def authenticate(self): - """Authenticate against the server. - - Normally this is called automatically when you first access the API, - but you can call this method to force authentication right now. - - Returns on success; raises :exc:`exceptions.Unauthorized` if the - credentials are wrong. - """ - self.logger.warning(_( - "Method 'authenticate' is deprecated since Ocata.")) diff --git a/novaclient/v2/cloudpipe.py b/novaclient/v2/cloudpipe.py deleted file mode 100644 index b9ac9f65a..000000000 --- a/novaclient/v2/cloudpipe.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""DEPRECATED Cloudpipe interface.""" - -import warnings - -from novaclient import base -from novaclient.i18n import _ - - -DEPRECATION_WARNING = ( - _('The os-cloudpipe Nova API has been removed. This API binding will be ' - 'removed in the first major release after the Nova server 16.0.0 Pike ' - 'release.') -) - - -class Cloudpipe(base.Resource): - """A cloudpipe instance is a VPN attached to a project's VLAN.""" - - def __repr__(self): - return "" % self.project_id - - def delete(self): - """ - DEPRECATED Delete the own cloudpipe instance - - :returns: An instance of novaclient.base.TupleWithMeta - """ - - warnings.warn(DEPRECATION_WARNING) - - return self.manager.delete(self) - - -class CloudpipeManager(base.ManagerWithFind): - """DEPRECATED""" - - resource_class = Cloudpipe - - def create(self, project): - """DEPRECATED Launch a cloudpipe instance. - - :param project: UUID of the project (tenant) for the cloudpipe - """ - - warnings.warn(DEPRECATION_WARNING) - - body = {'cloudpipe': {'project_id': project}} - return self._create('/os-cloudpipe', body, 'instance_id', - return_raw=True) - - def list(self): - """DEPRECATED Get a list of cloudpipe instances.""" - - warnings.warn(DEPRECATION_WARNING) - - return self._list('/os-cloudpipe', 'cloudpipes') - - def update(self, address, port): - """DEPRECATED Configure cloudpipe parameters for the project. - - Update VPN address and port for all networks associated - with the project defined by authentication - - :param address: IP address - :param port: Port number - :returns: An instance of novaclient.base.TupleWithMeta - """ - - warnings.warn(DEPRECATION_WARNING) - - body = {'configure_project': {'vpn_ip': address, - 'vpn_port': port}} - return self._update("/os-cloudpipe/configure-project", body) diff --git a/novaclient/v2/contrib/__init__.py b/novaclient/v2/contrib/__init__.py deleted file mode 100644 index 3cbadc17b..000000000 --- a/novaclient/v2/contrib/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import inspect -import warnings - -from novaclient.i18n import _ - -# NOTE(andreykurilin): "tenant_networks" extension excluded -# here deliberately. It was deprecated separately from deprecation -# extension mechanism and I prefer to not auto-load it by default -# (V2_0_EXTENSIONS is designed for such behaviour). -V2_0_EXTENSIONS = { - 'assisted_volume_snapshots': - 'novaclient.v2.assisted_volume_snapshots', - 'cells': 'novaclient.v2.cells', - 'instance_action': 'novaclient.v2.instance_action', - 'list_extensions': 'novaclient.v2.list_extensions', - 'migrations': 'novaclient.v2.migrations', - 'server_external_events': 'novaclient.v2.server_external_events', -} - - -def warn(alternative=True): - """Prints warning msg for contrib modules.""" - frm = inspect.stack()[1] - module_name = inspect.getmodule(frm[0]).__name__ - if module_name.startswith("novaclient.v2.contrib."): - if alternative: - new_module_name = module_name.replace("contrib.", "") - msg = _("Module `%(module)s` is deprecated as of OpenStack " - "Ocata in favor of `%(new_module)s` and will be " - "removed after OpenStack Pike.") % { - "module": module_name, "new_module": new_module_name} - - if not alternative: - msg = _("Module `%s` is deprecated as of OpenStack Ocata " - "All shell commands were moved to " - "`novaclient.v2.shell` and will be automatically " - "loaded.") % module_name - - warnings.warn(msg) diff --git a/novaclient/v2/contrib/assisted_volume_snapshots.py b/novaclient/v2/contrib/assisted_volume_snapshots.py deleted file mode 100644 index 95b5073a2..000000000 --- a/novaclient/v2/contrib/assisted_volume_snapshots.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (C) 2013, Red Hat, 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. - -""" -Assisted volume snapshots - to be used by Cinder and not end users. -""" - -from novaclient.v2 import assisted_volume_snapshots -from novaclient.v2 import contrib - - -AssistedSnapshotManager = assisted_volume_snapshots.AssistedSnapshotManager -Snapshot = assisted_volume_snapshots.Snapshot - -manager_class = AssistedSnapshotManager -name = 'assisted_volume_snapshots' - -contrib.warn() diff --git a/novaclient/v2/contrib/cells.py b/novaclient/v2/contrib/cells.py deleted file mode 100644 index a689d00a9..000000000 --- a/novaclient/v2/contrib/cells.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.v2 import cells -from novaclient.v2 import contrib - - -Cell = cells.Cell -CellsManager = cells.CellsManager - -contrib.warn() diff --git a/novaclient/v2/contrib/deferred_delete.py b/novaclient/v2/contrib/deferred_delete.py deleted file mode 100644 index bd5798ac7..000000000 --- a/novaclient/v2/contrib/deferred_delete.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from novaclient.v2 import contrib - -contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/host_evacuate.py b/novaclient/v2/contrib/host_evacuate.py deleted file mode 100644 index b5305bcb7..000000000 --- a/novaclient/v2/contrib/host_evacuate.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.v2 import contrib -from novaclient.v2 import shell - - -EvacuateHostResponse = shell.EvacuateHostResponse - -contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/host_evacuate_live.py b/novaclient/v2/contrib/host_evacuate_live.py deleted file mode 100644 index 7e27d7359..000000000 --- a/novaclient/v2/contrib/host_evacuate_live.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.v2 import contrib - -contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/host_servers_migrate.py b/novaclient/v2/contrib/host_servers_migrate.py deleted file mode 100644 index e88da876b..000000000 --- a/novaclient/v2/contrib/host_servers_migrate.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.v2 import contrib -from novaclient.v2 import shell - - -HostServersMigrateResponse = shell.HostServersMigrateResponse - -contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/instance_action.py b/novaclient/v2/contrib/instance_action.py deleted file mode 100644 index a90b99bd5..000000000 --- a/novaclient/v2/contrib/instance_action.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.v2 import contrib -from novaclient.v2 import instance_action - - -InstanceActionManager = instance_action.InstanceActionManager - -contrib.warn() diff --git a/novaclient/v2/contrib/list_extensions.py b/novaclient/v2/contrib/list_extensions.py deleted file mode 100644 index 07f4e3d7f..000000000 --- a/novaclient/v2/contrib/list_extensions.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.v2 import contrib -from novaclient.v2 import list_extensions - - -ListExtResource = list_extensions.ListExtResource -ListExtManager = list_extensions.ListExtManager - -contrib.warn() diff --git a/novaclient/v2/contrib/metadata_extensions.py b/novaclient/v2/contrib/metadata_extensions.py deleted file mode 100644 index eb6fbd224..000000000 --- a/novaclient/v2/contrib/metadata_extensions.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2013 Rackspace Hosting -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.v2 import contrib - - -contrib.warn(alternative=False) diff --git a/novaclient/v2/contrib/server_external_events.py b/novaclient/v2/contrib/server_external_events.py deleted file mode 100644 index bbd1b032f..000000000 --- a/novaclient/v2/contrib/server_external_events.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (C) 2014, Red Hat, 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. - -""" -External event triggering for servers, not to be used by users. -""" - -from novaclient.v2 import contrib -from novaclient.v2 import server_external_events - - -Event = server_external_events.Event -ServerExternalEventManager = server_external_events.ServerExternalEventManager - -manager_class = ServerExternalEventManager -name = 'server_external_events' - -contrib.warn() diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py index a0a7b6f95..803e226a0 100644 --- a/novaclient/v2/flavors.py +++ b/novaclient/v2/flavors.py @@ -17,8 +17,8 @@ """ from oslo_utils import strutils -from six.moves.urllib import parse +from novaclient import api_versions from novaclient import base from novaclient import exceptions from novaclient.i18n import _ @@ -86,6 +86,16 @@ def delete(self): """ return self.manager.delete(self) + @api_versions.wraps('2.55') + def update(self, description=None): + """ + Update the description for this flavor. + + :param description: The description to set on the flavor. + :returns: :class:`Flavor` + """ + return self.manager.update(self, description=description) + class FlavorManager(base.ManagerWithFind): """Manage :class:`Flavor` resources.""" @@ -104,8 +114,11 @@ def list(self, detailed=True, is_public=True, marker=None, min_disk=None, :param marker: Begin returning flavors that appear later in the flavor list than that represented by this flavor id (optional). :param min_disk: Filters the flavors by a minimum disk space, in GiB. - :param min_ram: Filters the flavors by a minimum RAM, in MB. + :param min_ram: Filters the flavors by a minimum RAM, in MiB. :param limit: maximum number of flavors to return (optional). + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. :param sort_key: Flavors list sort key (optional). :param sort_dir: Flavors list sort direction (optional). :returns: list of :class:`Flavor`. @@ -128,14 +141,11 @@ def list(self, detailed=True, is_public=True, marker=None, min_disk=None, qparams['sort_dir'] = str(sort_dir) if not is_public: qparams['is_public'] = is_public - qparams = sorted(qparams.items(), key=lambda x: x[0]) - query_string = "?%s" % parse.urlencode(qparams) if qparams else "" - detail = "" if detailed: detail = "/detail" - return self._list("/flavors%s%s" % (detail, query_string), "flavors") + return self._list("/flavors%s" % detail, "flavors", filters=qparams) def get(self, flavor): """Get a specific flavor. @@ -148,7 +158,8 @@ def get(self, flavor): def delete(self, flavor): """Delete a specific flavor. - :param flavor: The ID of the :class:`Flavor` to get. + :param flavor: Instance of :class:`Flavor` to delete or ID of the + flavor to delete. :returns: An instance of novaclient.base.TupleWithMeta """ return self._delete("/flavors/%s" % base.getid(flavor)) @@ -170,18 +181,25 @@ def _build_body(self, name, ram, vcpus, disk, id, swap, } def create(self, name, ram, vcpus, disk, flavorid="auto", - ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): + ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True, + description=None): """Create a flavor. :param name: Descriptive name of the flavor - :param ram: Memory in MB for the flavor + :param ram: Memory in MiB for the flavor :param vcpus: Number of VCPUs for the flavor - :param disk: Size of local disk in GB + :param disk: Size of local disk in GiB :param flavorid: ID for the flavor (optional). You can use the reserved value ``"auto"`` to have Nova generate a UUID for the flavor in cases where you cannot simply pass ``None``. - :param swap: Swap space in MB + :param ephemeral: Ephemeral disk space in GiB. + :param swap: Swap space in MiB :param rxtx_factor: RX/TX factor + :param is_public: Whether or not the flavor is public. + :param description: A free form description of the flavor. + Limited to 65535 characters in length. + Only printable characters are allowed. + (Available starting with microversion 2.55) :returns: :class:`Flavor` """ @@ -219,7 +237,28 @@ def create(self, name, ram, vcpus, disk, flavorid="auto", except Exception: raise exceptions.CommandError(_("is_public must be a boolean.")) + supports_description = api_versions.APIVersion('2.55') + if description and self.api_version < supports_description: + raise exceptions.UnsupportedAttribute('description', '2.55') + body = self._build_body(name, ram, vcpus, disk, flavorid, swap, ephemeral, rxtx_factor, is_public) + if description: + body['flavor']['description'] = description return self._create("/flavors", body, "flavor") + + @api_versions.wraps('2.55') + def update(self, flavor, description=None): + """ + Update the description of the flavor. + + :param flavor: The :class:`Flavor` (or its ID) to update. + :param description: The description to set on the flavor. + """ + body = { + 'flavor': { + 'description': description + } + } + return self._update('/flavors/%s' % base.getid(flavor), body, 'flavor') diff --git a/novaclient/v2/hosts.py b/novaclient/v2/hosts.py deleted file mode 100644 index be7ca58e4..000000000 --- a/novaclient/v2/hosts.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -DEPRECATED host interface -""" -import warnings - -from novaclient import api_versions -from novaclient import base -from novaclient.i18n import _ - - -HOSTS_DEPRECATION_WARNING = ( - _('The os-hosts API is deprecated. This API binding will be removed ' - 'in the first major release after the Nova server 16.0.0 Pike release.') -) - - -class Host(base.Resource): - """DEPRECATED""" - def __repr__(self): - return "" % self.host - - def _add_details(self, info): - dico = 'resource' in info and info['resource'] or info - for (k, v) in dico.items(): - setattr(self, k, v) - - @api_versions.wraps("2.0", "2.42") - def update(self, values): - return self.manager.update(self.host, values) - - @api_versions.wraps("2.0", "2.42") - def startup(self): - return self.manager.host_action(self.host, 'startup') - - @api_versions.wraps("2.0", "2.42") - def shutdown(self): - return self.manager.host_action(self.host, 'shutdown') - - @api_versions.wraps("2.0", "2.42") - def reboot(self): - return self.manager.host_action(self.host, 'reboot') - - @property - def host_name(self): - return self.host - - @host_name.setter - def host_name(self, value): - # A host from hosts.list() has the attribute "host_name" instead of - # "host." This sets "host" if that's the case. Even though it doesn't - # exactly mirror the response format, it enables users to work with - # host objects from list and non-list operations interchangeably. - self.host = value - - -class HostManager(base.ManagerWithFind): - resource_class = Host - - @api_versions.wraps("2.0", "2.42") - def get(self, host): - """ - DEPRECATED Describes cpu/memory/hdd info for host. - - :param host: destination host name. - """ - warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) - return self._list("/os-hosts/%s" % host, "host") - - @api_versions.wraps("2.0", "2.42") - def update(self, host, values): - """DEPRECATED Update status or maintenance mode for the host.""" - warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) - return self._update("/os-hosts/%s" % host, values) - - @api_versions.wraps("2.0", "2.42") - def host_action(self, host, action): - """ - DEPRECATED Perform an action on a host. - - :param host: The host to perform an action - :param action: The action to perform - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) - url = '/os-hosts/%s/%s' % (host, action) - resp, body = self.api.client.get(url) - return base.TupleWithMeta((resp, body), resp) - - @api_versions.wraps("2.0", "2.42") - def list(self, zone=None): - warnings.warn(HOSTS_DEPRECATION_WARNING, DeprecationWarning) - url = '/os-hosts' - if zone: - url = '/os-hosts?zone=%s' % zone - return self._list(url, "hosts") - - list_all = list diff --git a/novaclient/v2/hypervisors.py b/novaclient/v2/hypervisors.py index e43ef7a18..f0cc5863c 100644 --- a/novaclient/v2/hypervisors.py +++ b/novaclient/v2/hypervisors.py @@ -17,10 +17,12 @@ Hypervisors interface """ -from six.moves.urllib import parse +from urllib import parse from novaclient import api_versions from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ from novaclient import utils @@ -68,10 +70,13 @@ def list(self, detailed=True, marker=None, limit=None): marker must be a UUID hypervisor ID. (optional). :param limit: maximum number of hypervisors to return (optional). + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. """ return self._list_base(detailed=detailed, marker=marker, limit=limit) - def search(self, hypervisor_match, servers=False): + def search(self, hypervisor_match, servers=False, detailed=False): """ Get a list of matching hypervisors. @@ -79,16 +84,23 @@ def search(self, hypervisor_match, servers=False): The hypervisor hosts are selected with the host name matching this pattern. :param servers: If True, server information is also retrieved. + :param detailed: If True, detailed hypervisor information is returned. + This requires API version 2.53 or greater. """ # Starting with microversion 2.53, the /servers and /search routes are # deprecated and we get the same results using GET /os-hypervisors # using query parameters for the hostname pattern and servers. if self.api_version >= api_versions.APIVersion('2.53'): - url = ('/os-hypervisors?hypervisor_hostname_pattern=%s' % - parse.quote(hypervisor_match, safe='')) + url = ('/os-hypervisors%s?hypervisor_hostname_pattern=%s' % + ('/detail' if detailed else '', + parse.quote(hypervisor_match, safe=''))) if servers: url += '&with_servers=True' else: + if detailed: + raise exceptions.UnsupportedVersion( + _('Parameter "detailed" requires API version 2.53 or ' + 'greater.')) target = 'servers' if servers else 'search' url = ('/os-hypervisors/%s/%s' % (parse.quote(hypervisor_match, safe=''), target)) @@ -111,8 +123,25 @@ def uptime(self, hypervisor): :param hypervisor: Either a Hypervisor object or an ID. Starting with microversion 2.53 the ID must be a UUID value. """ - return self._get("/os-hypervisors/%s/uptime" % base.getid(hypervisor), - "hypervisor") + # Starting with microversion 2.88, the '/os-hypervisors/{id}/uptime' + # route is removed in favour of returning 'uptime' in the response of + # the '/os-hypervisors/{id}' route. This behaves slightly differently, + # in that it won't error out if a virt driver doesn't support reporting + # uptime or if the hypervisor is down, but it's a good enough + # approximation + if self.api_version < api_versions.APIVersion("2.88"): + return self._get( + "/os-hypervisors/%s/uptime" % base.getid(hypervisor), + "hypervisor") + + resp, body = self.api.client.get( + "/os-hypervisors/%s" % base.getid(hypervisor) + ) + content = { + k: v for k, v in body['hypervisor'].items() + if k in ('id', 'hypervisor_hostname', 'state', 'status', 'uptime') + } + return self.resource_class(self, content, loaded=True, resp=resp) def statistics(self): """ @@ -133,8 +162,15 @@ def __repr__(self): class HypervisorStatsManager(base.Manager): resource_class = HypervisorStats + @api_versions.wraps("2.0", "2.87") def statistics(self): """ Get hypervisor statistics over all compute nodes. """ return self._get("/os-hypervisors/statistics", "hypervisor_statistics") + + @api_versions.wraps("2.88") + def statistics(self): + raise exceptions.UnsupportedVersion( + _("The 'statistics' API is removed in API version 2.88 or later.") + ) diff --git a/novaclient/v2/images.py b/novaclient/v2/images.py index e83d9b2fe..0aced5c7a 100644 --- a/novaclient/v2/images.py +++ b/novaclient/v2/images.py @@ -66,6 +66,43 @@ def find_image(self, name_or_id): matches[0].append_request_ids(matches.request_ids) return matches[0] + def find_images(self, names_or_ids): + """Find multiple images by name or id (user provided input). + + :param names_or_ids: A list of strings to use to find images. + :returns: novaclient.v2.images.Image objects for each images found + :raises exceptions.NotFound: If one or more images is not found + :raises exceptions.ClientException: If the image service returns any + unexpected images. + + NOTE: This method always makes two calls to the image service, even if + only one image is provided by ID and is returned in the first query. + """ + with self.alternate_service_type( + 'image', allowed_types=('image',)): + matches = self._list('/v2/images?id=in:%s' % ','.join( + names_or_ids), 'images') + matches.extend(self._list('/v2/images?names=in:%s' % ','.join( + names_or_ids), 'images')) + missed = (set(names_or_ids) - + set(m.name for m in matches) - + set(m.id for m in matches)) + if missed: + msg = _("Unable to find image(s): %(images)s") % { + "images": ",".join(missed)} + raise exceptions.NotFound(404, msg) + for match in matches: + match.append_request_ids(matches.request_ids) + + additional = [] + for i in matches: + if i.name not in names_or_ids and i.id not in names_or_ids: + additional.append(i) + if additional: + msg = _('Additional images found in response') + raise exceptions.ClientException(500, msg) + return matches + def list(self): """ Get a detailed list of all images. diff --git a/novaclient/v2/instance_action.py b/novaclient/v2/instance_action.py index 5531d35fc..d84c50f90 100644 --- a/novaclient/v2/instance_action.py +++ b/novaclient/v2/instance_action.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import base @@ -32,9 +33,72 @@ def get(self, server, request_id): return self._get("/servers/%s/os-instance-actions/%s" % (base.getid(server), request_id), 'instanceAction') + @api_versions.wraps("2.0", "2.57") def list(self, server): """ Get a list of actions performed on a server. + + :param server: The :class:`Server` (or its ID) """ return self._list('/servers/%s/os-instance-actions' % base.getid(server), 'instanceActions') + + @api_versions.wraps("2.58", "2.65") + def list(self, server, marker=None, limit=None, changes_since=None): + """ + Get a list of actions performed on a server. + + :param server: The :class:`Server` (or its ID) + :param marker: Begin returning actions that appear later in the action + list than that represented by this action request id + (optional). + :param limit: Maximum number of actions to return. (optional). + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. + :param changes_since: List only instance actions changed later or + equal to a certain point of time. The provided + time should be an ISO 8061 formatted time. + e.g. 2016-03-04T06:27:59Z . (optional). + """ + opts = {} + if marker: + opts['marker'] = marker + if limit: + opts['limit'] = limit + if changes_since: + opts['changes-since'] = changes_since + return self._list('/servers/%s/os-instance-actions' % + base.getid(server), 'instanceActions', filters=opts) + + @api_versions.wraps("2.66") + def list(self, server, marker=None, limit=None, changes_since=None, + changes_before=None): + """ + Get a list of actions performed on a server. + + :param server: The :class:`Server` (or its ID) + :param marker: Begin returning actions that appear later in the action + list than that represented by this action request id + (optional). + :param limit: Maximum number of actions to return. (optional). + :param changes_since: List only instance actions changed later or + equal to a certain point of time. The provided + time should be an ISO 8061 formatted time. + e.g. 2016-03-04T06:27:59Z . (optional). + :param changes_before: List only instance actions changed earlier or + equal to a certain point of time. The provided + time should be an ISO 8061 formatted time. + e.g. 2016-03-05T06:27:59Z . (optional). + """ + opts = {} + if marker: + opts['marker'] = marker + if limit: + opts['limit'] = limit + if changes_since: + opts['changes-since'] = changes_since + if changes_before: + opts['changes-before'] = changes_before + return self._list('/servers/%s/os-instance-actions' % + base.getid(server), 'instanceActions', filters=opts) diff --git a/novaclient/v2/instance_usage_audit_log.py b/novaclient/v2/instance_usage_audit_log.py new file mode 100644 index 000000000..83db8c8e6 --- /dev/null +++ b/novaclient/v2/instance_usage_audit_log.py @@ -0,0 +1,40 @@ +# Copyright 2013 Rackspace Hosting +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from urllib import parse + +from novaclient import base + + +class InstanceUsageAuditLog(base.Resource): + pass + + +class InstanceUsageAuditLogManager(base.Manager): + resource_class = InstanceUsageAuditLog + + def get(self, before=None): + """Get server usage audits. + + :param before: Filters the response by the date and time + before which to list usage audits. + """ + if before: + return self._get('/os-instance_usage_audit_log/%s' % + parse.quote(before, safe=''), + 'instance_usage_audit_log') + else: + return self._get('/os-instance_usage_audit_log', + 'instance_usage_audit_logs') diff --git a/novaclient/v2/keypairs.py b/novaclient/v2/keypairs.py index 021cc6273..5d12f8cd4 100644 --- a/novaclient/v2/keypairs.py +++ b/novaclient/v2/keypairs.py @@ -19,7 +19,6 @@ from novaclient import api_versions from novaclient import base -from novaclient import utils class Keypair(base.Resource): @@ -115,7 +114,7 @@ def create(self, name, public_key=None, key_type="ssh"): body['keypair']['public_key'] = public_key return self._create('/%s' % self.keypair_prefix, body, 'keypair') - @api_versions.wraps("2.10") + @api_versions.wraps("2.10", "2.91") def create(self, name, public_key=None, key_type="ssh", user_id=None): """ Create a keypair @@ -133,6 +132,23 @@ def create(self, name, public_key=None, key_type="ssh", user_id=None): body['keypair']['user_id'] = user_id return self._create('/%s' % self.keypair_prefix, body, 'keypair') + @api_versions.wraps("2.92") + def create(self, name, public_key, key_type="ssh", user_id=None): + """ + Create a keypair + + :param name: name for the keypair to create + :param public_key: existing public key to import + :param key_type: keypair type to create + :param user_id: user to add. + """ + body = {'keypair': {'name': name, + 'type': key_type, + 'public_key': public_key}} + if user_id: + body['keypair']['user_id'] = user_id + return self._create('/%s' % self.keypair_prefix, body, 'keypair') + @api_versions.wraps("2.0", "2.9") def delete(self, key): """ @@ -170,9 +186,11 @@ def list(self, user_id=None): :param user_id: Id of key-pairs owner (Admin only). """ - query_string = "?user_id=%s" % user_id if user_id else "" - url = '/%s%s' % (self.keypair_prefix, query_string) - return self._list(url, 'keypairs') + params = {} + if user_id: + params['user_id'] = user_id + return self._list('/%s' % self.keypair_prefix, 'keypairs', + filters=params) @api_versions.wraps("2.35") def list(self, user_id=None, marker=None, limit=None): @@ -184,6 +202,9 @@ def list(self, user_id=None, marker=None, limit=None): keypair list than that represented by this keypair name (optional). :param limit: maximum number of keypairs to return (optional). + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. """ params = {} if user_id: @@ -192,6 +213,5 @@ def list(self, user_id=None, marker=None, limit=None): params['limit'] = int(limit) if marker: params['marker'] = str(marker) - query_string = utils.prepare_query_string(params) - url = '/%s%s' % (self.keypair_prefix, query_string) - return self._list(url, 'keypairs') + return self._list('/%s' % self.keypair_prefix, 'keypairs', + filters=params) diff --git a/novaclient/v2/limits.py b/novaclient/v2/limits.py index 46d77def8..d90d3e9a4 100644 --- a/novaclient/v2/limits.py +++ b/novaclient/v2/limits.py @@ -12,8 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -from six.moves.urllib import parse - from novaclient import base @@ -95,6 +93,4 @@ def get(self, reserved=False, tenant_id=None): opts['reserved'] = 1 if tenant_id: opts['tenant_id'] = tenant_id - query_string = "?%s" % parse.urlencode(opts) if opts else "" - - return self._get("/limits%s" % query_string, "limits") + return self._get("/limits", "limits", filters=opts) diff --git a/novaclient/v2/list_extensions.py b/novaclient/v2/list_extensions.py deleted file mode 100644 index faeead601..000000000 --- a/novaclient/v2/list_extensions.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient import base - - -class ListExtResource(base.Resource): - @property - def summary(self): - descr = self.description.strip() - if not descr: - return '??' - lines = descr.split("\n") - if len(lines) == 1: - return lines[0] - else: - return lines[0] + "..." - - -class ListExtManager(base.Manager): - resource_class = ListExtResource - - def show_all(self): - return self._list("/extensions", 'extensions') diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index 72e0deda9..1fe764b5e 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -14,12 +14,8 @@ migration interface """ -from six.moves.urllib import parse - +from novaclient import api_versions from novaclient import base -from novaclient.i18n import _ - -import warnings class Migration(base.Resource): @@ -30,30 +26,152 @@ def __repr__(self): class MigrationManager(base.ManagerWithFind): resource_class = Migration - def list(self, host=None, status=None, cell_name=None, instance_uuid=None): - """ - Get a list of migrations. - :param host: (optional) filter migrations by host name. - :param status: (optional) filter migrations by status. - :param cell_name: (optional) filter migrations for a cell. - """ + def _list_base(self, host=None, status=None, instance_uuid=None, + marker=None, limit=None, changes_since=None, + changes_before=None, migration_type=None, + source_compute=None, user_id=None, project_id=None): opts = {} if host: opts['host'] = host if status: opts['status'] = status - if cell_name: - warnings.warn(_("Argument 'cell_name' is " - "deprecated since Pike, and will " - "be removed in a future release.")) - opts['cell_name'] = cell_name if instance_uuid: opts['instance_uuid'] = instance_uuid + if marker: + opts['marker'] = marker + if limit: + opts['limit'] = limit + if changes_since: + opts['changes-since'] = changes_since + if changes_before: + opts['changes-before'] = changes_before + if migration_type: + opts['migration_type'] = migration_type + if source_compute: + opts['source_compute'] = source_compute + if user_id: + opts['user_id'] = user_id + if project_id: + opts['project_id'] = project_id - # Transform the dict to a sequence of two-element tuples in fixed - # order, then the encoded string will be consistent in Python 2&3. - new_opts = sorted(opts.items(), key=lambda x: x[0]) + return self._list("/os-migrations", "migrations", filters=opts) - query_string = "?%s" % parse.urlencode(new_opts) if new_opts else "" + @api_versions.wraps("2.0", "2.58") + def list(self, host=None, status=None, instance_uuid=None, + migration_type=None, source_compute=None): + """ + Get a list of migrations. + :param host: filter migrations by host name (optional). + :param status: filter migrations by status (optional). + :param instance_uuid: filter migrations by instance uuid (optional). + :param migration_type: Filter migrations by type. Valid values are: + evacuation, live-migration, migration (cold), resize + :param source_compute: Filter migrations by source compute host name. + """ + return self._list_base(host=host, status=status, + instance_uuid=instance_uuid, + migration_type=migration_type, + source_compute=source_compute) - return self._list("/os-migrations%s" % query_string, "migrations") + @api_versions.wraps("2.59", "2.65") + def list(self, host=None, status=None, instance_uuid=None, + marker=None, limit=None, changes_since=None, + migration_type=None, source_compute=None): + """ + Get a list of migrations. + :param host: filter migrations by host name (optional). + :param status: filter migrations by status (optional). + :param instance_uuid: filter migrations by instance uuid (optional). + :param marker: Begin returning migrations that appear later in the + migrations list than that represented by this migration UUID + (optional). + :param limit: maximum number of migrations to return (optional). + Note the API server has a configurable default limit. If no limit is + specified here or limit is larger than default, the default limit will + be used. + :param changes_since: only return migrations changed later or equal + to a certain point of time. The provided time should be an ISO 8061 + formatted time. e.g. 2016-03-04T06:27:59Z . (optional). + :param migration_type: Filter migrations by type. Valid values are: + evacuation, live-migration, migration (cold), resize + :param source_compute: Filter migrations by source compute host name. + """ + return self._list_base(host=host, status=status, + instance_uuid=instance_uuid, + marker=marker, limit=limit, + changes_since=changes_since, + migration_type=migration_type, + source_compute=source_compute) + + @api_versions.wraps("2.66", "2.79") + def list(self, host=None, status=None, instance_uuid=None, + marker=None, limit=None, changes_since=None, + changes_before=None, migration_type=None, source_compute=None): + """ + Get a list of migrations. + :param host: filter migrations by host name (optional). + :param status: filter migrations by status (optional). + :param instance_uuid: filter migrations by instance uuid (optional). + :param marker: Begin returning migrations that appear later in the + migrations list than that represented by this migration UUID + (optional). + :param limit: maximum number of migrations to return (optional). + Note the API server has a configurable default limit. If no limit is + specified here or limit is larger than default, the default limit will + be used. + :param changes_since: Only return migrations changed later or equal + to a certain point of time. The provided time should be an ISO 8061 + formatted time. e.g. 2016-03-04T06:27:59Z . (optional). + :param changes_before: Only return migrations changed earlier or + equal to a certain point of time. The provided time should be an ISO + 8061 formatted time. e.g. 2016-03-05T06:27:59Z . (optional). + :param migration_type: Filter migrations by type. Valid values are: + evacuation, live-migration, migration (cold), resize + :param source_compute: Filter migrations by source compute host name. + """ + return self._list_base(host=host, status=status, + instance_uuid=instance_uuid, + marker=marker, limit=limit, + changes_since=changes_since, + changes_before=changes_before, + migration_type=migration_type, + source_compute=source_compute) + + @api_versions.wraps("2.80") + def list(self, host=None, status=None, instance_uuid=None, + marker=None, limit=None, changes_since=None, + changes_before=None, migration_type=None, + source_compute=None, user_id=None, project_id=None): + """ + Get a list of migrations. + :param host: filter migrations by host name (optional). + :param status: filter migrations by status (optional). + :param instance_uuid: filter migrations by instance uuid (optional). + :param marker: Begin returning migrations that appear later in the + migrations list than that represented by this migration UUID + (optional). + :param limit: maximum number of migrations to return (optional). + Note the API server has a configurable default limit. If no limit is + specified here or limit is larger than default, the default limit will + be used. + :param changes_since: Only return migrations changed later or equal + to a certain point of time. The provided time should be an ISO 8061 + formatted time. e.g. 2016-03-04T06:27:59Z . (optional). + :param changes_before: Only return migrations changed earlier or + equal to a certain point of time. The provided time should be an ISO + 8061 formatted time. e.g. 2016-03-05T06:27:59Z . (optional). + :param migration_type: Filter migrations by type. Valid values are: + evacuation, live-migration, migration, resize + :param source_compute: Filter migrations by source compute host name. + :param user_id: filter migrations by user (optional). + :param project_id: filter migrations by project (optional). + """ + return self._list_base(host=host, status=status, + instance_uuid=instance_uuid, + marker=marker, limit=limit, + changes_since=changes_since, + changes_before=changes_before, + migration_type=migration_type, + source_compute=source_compute, + user_id=user_id, + project_id=project_id) diff --git a/novaclient/v2/quota_classes.py b/novaclient/v2/quota_classes.py index eae5bfdec..917cc9c43 100644 --- a/novaclient/v2/quota_classes.py +++ b/novaclient/v2/quota_classes.py @@ -50,7 +50,7 @@ def update(self, class_name, **kwargs): # NOTE(mriedem): 2.50 does strict validation of the resources you can # specify since the network-related resources are blocked in 2.50. - @api_versions.wraps("2.50") + @api_versions.wraps("2.50", "2.56") def update(self, class_name, instances=None, cores=None, ram=None, metadata_items=None, injected_files=None, injected_file_content_bytes=None, injected_file_path_bytes=None, @@ -81,3 +81,30 @@ def update(self, class_name, instances=None, cores=None, ram=None, body = {'quota_class_set': resources} return self._update('/os-quota-class-sets/%s' % class_name, body, 'quota_class_set') + + # NOTE(mriedem): 2.57 deprecates the usage of injected_files, + # injected_file_content_bytes and injected_file_path_bytes so those + # kwargs are removed. + @api_versions.wraps("2.57") + def update(self, class_name, instances=None, cores=None, ram=None, + metadata_items=None, key_pairs=None, server_groups=None, + server_group_members=None): + resources = {} + if instances is not None: + resources['instances'] = instances + if cores is not None: + resources['cores'] = cores + if ram is not None: + resources['ram'] = ram + if metadata_items is not None: + resources['metadata_items'] = metadata_items + if key_pairs is not None: + resources['key_pairs'] = key_pairs + if server_groups is not None: + resources['server_groups'] = server_groups + if server_group_members is not None: + resources['server_group_members'] = server_group_members + + body = {'quota_class_set': resources} + return self._update('/os-quota-class-sets/%s' % class_name, body, + 'quota_class_set') diff --git a/novaclient/v2/quotas.py b/novaclient/v2/quotas.py index 1aee5b17e..82249f25e 100644 --- a/novaclient/v2/quotas.py +++ b/novaclient/v2/quotas.py @@ -13,20 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import base class QuotaSet(base.Resource): - @property - def id(self): - """QuotaSet does not have a 'id' attribute but base.Resource needs it - to self-refresh and QuotaSet is indexed by tenant_id. - """ - return self.tenant_id - def update(self, *args, **kwargs): - return self.manager.update(self.tenant_id, *args, **kwargs) + return self.manager.update(self.id, *args, **kwargs) class QuotaSetManager(base.Manager): @@ -37,8 +31,6 @@ def get(self, tenant_id, user_id=None, detail=False): if detail: url += '/detail' - if hasattr(tenant_id, 'tenant_id'): - tenant_id = tenant_id.tenant_id if user_id: params = {'tenant_id': tenant_id, 'user_id': user_id} url += '?user_id=%(user_id)s' @@ -47,6 +39,10 @@ def get(self, tenant_id, user_id=None, detail=False): return self._get(url % params, "quota_set") + # NOTE(mriedem): Before 2.57 the resources you could update was just a + # kwargs dict and not validated on the client-side, only on the API server + # side. + @api_versions.wraps("2.0", "2.56") def update(self, tenant_id, **kwargs): user_id = kwargs.pop('user_id', None) @@ -62,6 +58,40 @@ def update(self, tenant_id, **kwargs): url = '/os-quota-sets/%s' % tenant_id return self._update(url, body, 'quota_set') + # NOTE(mriedem): 2.57 does strict validation of the resources you can + # specify. 2.36 blocks network-related resources and 2.57 blocks + # injected files related quotas. + @api_versions.wraps("2.57") + def update(self, tenant_id, user_id=None, force=False, + instances=None, cores=None, ram=None, + metadata_items=None, key_pairs=None, server_groups=None, + server_group_members=None): + + resources = {} + if force: + resources['force'] = force + if instances is not None: + resources['instances'] = instances + if cores is not None: + resources['cores'] = cores + if ram is not None: + resources['ram'] = ram + if metadata_items is not None: + resources['metadata_items'] = metadata_items + if key_pairs is not None: + resources['key_pairs'] = key_pairs + if server_groups is not None: + resources['server_groups'] = server_groups + if server_group_members is not None: + resources['server_group_members'] = server_group_members + body = {'quota_set': resources} + + if user_id: + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + else: + url = '/os-quota-sets/%s' % tenant_id + return self._update(url, body, 'quota_set') + def defaults(self, tenant_id): return self._get('/os-quota-sets/%s/defaults' % tenant_id, 'quota_set') diff --git a/novaclient/v2/server_groups.py b/novaclient/v2/server_groups.py index 7a01ba0db..bc66b01a2 100644 --- a/novaclient/v2/server_groups.py +++ b/novaclient/v2/server_groups.py @@ -17,9 +17,10 @@ Server group interface. """ -from six.moves.urllib import parse - +from novaclient import api_versions from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ class ServerGroup(base.Resource): @@ -50,6 +51,9 @@ def list(self, all_projects=False, limit=None, offset=None): :param all_projects: Lists server groups for all projects. (optional) :param limit: Maximum number of server groups to return. (optional) + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. :param offset: Use with `limit` to return a slice of server groups. `offset` is where to start in the groups list. (optional) @@ -62,10 +66,8 @@ def list(self, all_projects=False, limit=None, offset=None): qparams['limit'] = int(limit) if offset: qparams['offset'] = int(offset) - qparams = sorted(qparams.items(), key=lambda x: x[0]) - query_string = "?%s" % parse.urlencode(qparams) if qparams else "" - return self._list('/os-server-groups%s' % query_string, - 'server_groups') + return self._list('/os-server-groups', 'server_groups', + filters=qparams) def get(self, id): """Get a specific server group. @@ -84,6 +86,7 @@ def delete(self, id): """ return self._delete('/os-server-groups/%s' % id) + @api_versions.wraps("2.0", "2.63") def create(self, name, policies): """Create (allocate) a server group. @@ -96,3 +99,29 @@ def create(self, name, policies): body = {'server_group': {'name': name, 'policies': policies}} return self._create('/os-server-groups', body, 'server_group') + + @api_versions.wraps("2.64") + def create(self, name, policy, rules=None): + """Create (allocate) a server group. + + :param name: The name of the server group. + :param policy: Policy name to associate with the server group. + :param rules: The rules of policy which is a dict, can be applied to + the policy, now only ``max_server_per_host`` for ``anti-affinity`` + policy would be supported (optional). + :rtype: list of :class:`ServerGroup` + """ + body = {'server_group': { + 'name': name, 'policy': policy + }} + if rules: + key = 'max_server_per_host' + try: + if key in rules: + rules[key] = int(rules[key]) + except ValueError: + msg = _("Invalid '%(key)s' value: %(value)s") + raise exceptions.CommandError(msg % { + 'key': key, 'value': rules[key]}) + body['server_group']['rules'] = rules + return self._create('/os-server-groups', body, 'server_group') diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index 4c38ed700..81c702dfb 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -20,11 +20,8 @@ """ import base64 -import warnings - -from oslo_utils import encodeutils -import six -from six.moves.urllib import parse +import collections +from urllib import parse from novaclient import api_versions from novaclient import base @@ -32,6 +29,7 @@ from novaclient import exceptions from novaclient.i18n import _ +_SENTINEL = object() REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' @@ -52,12 +50,6 @@ 'webmks': 'mks' } -ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING = _( - 'The %s server action API is deprecated as of the 2.44 microversion. This ' - 'API binding will be removed in the first major release after the Nova ' - '16.0.0 Pike release. Use python-neutronclient or openstacksdk instead.' -) - class Server(base.Resource): HUMAN_ID = True @@ -76,25 +68,42 @@ def delete(self): @api_versions.wraps("2.0", "2.18") def update(self, name=None): """ - Update the name and the description for this server. + Update attributes of this server. :param name: Update the server's name. :returns: :class:`Server` """ return self.manager.update(self, name=name) - @api_versions.wraps("2.19") + @api_versions.wraps("2.19", "2.89") def update(self, name=None, description=None): """ - Update the name and the description for this server. + Update attributes of this server. + + :param name: Update the server's name. + :param description: Update the server's description. + :returns: :class:`Server` + """ + update_kwargs = {"name": name} + if description is not None: + update_kwargs["description"] = description + return self.manager.update(self, **update_kwargs) + + @api_versions.wraps("2.90") + def update(self, name=None, description=None, hostname=None): + """ + Update attributes of this server. :param name: Update the server's name. :param description: Update the server's description. + :param hostname: Update the server's hostname. :returns: :class:`Server` """ update_kwargs = {"name": name} if description is not None: update_kwargs["description"] = description + if hostname is not None: + update_kwargs["hostname"] = hostname return self.manager.update(self, **update_kwargs) def get_console_output(self, length=None): @@ -172,35 +181,6 @@ def clear_password(self): """ return self.manager.clear_password(self) - def add_fixed_ip(self, network_id): - """ - Add an IP address on a network. - - :param network_id: The ID of the network the IP should be on. - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.add_fixed_ip(self, network_id) - - def add_floating_ip(self, address, fixed_address=None): - """ - Add floating IP to an instance - - :param address: The IP address or FloatingIP to add to the instance - :param fixed_address: The fixedIP address the FloatingIP is to be - associated with (optional) - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.add_floating_ip(self, address, fixed_address) - - def remove_floating_ip(self, address): - """ - Remove floating IP from an instance - - :param address: The IP address or FloatingIP to remove - :returns: An instance of novaclient.base.TupleWithMeta - """ - return self.manager.remove_floating_ip(self, address) - def stop(self): """ Stop -- Stop the running server. @@ -249,6 +229,7 @@ def unpause(self): """ return self.manager.unpause(self) + @api_versions.wraps("2.0", "2.72") def lock(self): """ Lock -- Lock the instance from certain operations. @@ -257,6 +238,16 @@ def lock(self): """ return self.manager.lock(self) + @api_versions.wraps("2.73") + def lock(self, reason=None): + """ + Lock -- Lock the instance from certain operations. + + :param reason: (Optional) The lock reason. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.lock(self, reason=reason) + def unlock(self): """ Unlock -- Remove instance lock. @@ -315,6 +306,7 @@ def shelve_offload(self): """ return self.manager.shelve_offload(self) + @api_versions.wraps("2.0", "2.76") def unshelve(self): """ Unshelve -- Unshelve the server. @@ -323,10 +315,59 @@ def unshelve(self): """ return self.manager.unshelve(self) + @api_versions.wraps("2.77", "2.90") + def unshelve(self, availability_zone=None): + """ + Unshelve -- Unshelve the server. + + :param availability_zone: The specified availability zone name + (Optional) + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.unshelve(self, + availability_zone=availability_zone) + + @api_versions.wraps("2.91") + def unshelve(self, availability_zone=object(), host=None): + """ + Unshelve -- Unshelve the server. + + :param availability_zone: If specified the instance will be unshelved + to the availability_zone. + If None is passed the instance defined + availability_zone is unpin and the instance + will be scheduled to any availability_zone + (free scheduling). + If not specified the instance will be + unshelved to either its defined + availability_zone or any + availability_zone (free scheduling). + :param host: The specified host + (Optional) + :returns: An instance of novaclient.base.TupleWithMeta + """ + if ( + availability_zone is None or isinstance(availability_zone, str) + ) and host: + return self.manager.unshelve( + self, availability_zone=availability_zone, host=host) + if availability_zone is None or isinstance(availability_zone, str): + return self.manager.unshelve( + self, availability_zone=availability_zone) + if host: + return self.manager.unshelve(self, host=host) + return self.manager.unshelve(self) + def diagnostics(self): """Diagnostics -- Retrieve server diagnostics.""" return self.manager.diagnostics(self) + @api_versions.wraps("2.78") + def topology(self): + """Retrieve server topology.""" + return self.manager.topology(self) + + @api_versions.wraps("2.0", "2.55") def migrate(self): """ Migrate a server to a new host. @@ -335,14 +376,15 @@ def migrate(self): """ return self.manager.migrate(self) - def remove_fixed_ip(self, address): + @api_versions.wraps("2.56") + def migrate(self, host=None): """ - Remove an IP address. + Migrate a server to a new host. - :param address: The IP address to remove. + :param host: (Optional) The target host. :returns: An instance of novaclient.base.TupleWithMeta """ - return self.manager.remove_fixed_ip(self, address) + return self.manager.migrate(self, host=host) def change_password(self, password): """ @@ -363,34 +405,90 @@ def reboot(self, reboot_type=REBOOT_SOFT): """ return self.manager.reboot(self, reboot_type) - def rebuild(self, image, password=None, preserve_ephemeral=False, - **kwargs): + # NOTE(stephenfin): It would be nice to make everything bar image a + # kwarg-only argument but there are backwards-compatbility concerns + def rebuild( + self, + image, + password=None, + preserve_ephemeral=False, + *, + disk_config=None, + name=None, + meta=None, + files=None, + description=_SENTINEL, + key_name=_SENTINEL, + userdata=_SENTINEL, + trusted_image_certificates=_SENTINEL, + hostname=_SENTINEL, + ): """ Rebuild -- shut down and then re-image -- this server. - :param image: the :class:`Image` (or its ID) to re-image with. - :param password: string to set as the admin password on the rebuilt - server. + :param image: The :class:`Image` (or its ID) to re-image with. + :param password: String to set as password on the rebuilt server. :param preserve_ephemeral: If True, request that any ephemeral device be preserved when rebuilding the instance. Defaults to False. + :param disk_config: Partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL' + :param name: Something to name the server. + :param meta: A dict of arbitrary key/value metadata to store for this + server. Both keys and values must be <=255 characters. + :param files: A dict of files to overwrite on the server upon boot. + Keys are file names (i.e. ``/etc/passwd``) and values are the file + contents (either as a string or as a file-like object). A maximum + of five entries is allowed, and each file must be 10k or less. + (deprecated starting with microversion 2.57) + :param description: Optional description of the server. If None is + specified, the existing description will be unset. + (starting from microversion 2.19) + :param key_name: Optional key pair name for rebuild operation. If None + is specified, the existing key will be unset. + (starting from microversion 2.54) + :param userdata: Optional user data to pass to be exposed by the + metadata server; this can be a file type object as well or a + string. If None is specified, the existing user_data is unset. + (starting from microversion 2.57) + :param trusted_image_certificates: A list of trusted certificate IDs + or None to unset/reset the servers trusted image certificates + (starting from microversion 2.63) + :param hostname: Optional hostname to configure for the instance. If + None is specified, the existing hostname will be unset. + (starting from microversion 2.90) + :returns: :class:`Server` """ - return self.manager.rebuild(self, image, password=password, - preserve_ephemeral=preserve_ephemeral, - **kwargs) - - def resize(self, flavor, **kwargs): + return self.manager.rebuild( + self, + image, + password=password, + disk_config=disk_config, + preserve_ephemeral=preserve_ephemeral, + name=name, + meta=meta, + files=files, + description=description, + key_name=key_name, + userdata=userdata, + trusted_image_certificates=trusted_image_certificates, + hostname=hostname, + ) + + def resize(self, flavor, *, disk_config=None): """ Resize the server's resources. - :param flavor: the :class:`Flavor` (or its ID) to resize to. + :param flavor: The :class:`Flavor` (or its ID) to resize to. + :param disk_config: Partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL'. :returns: An instance of novaclient.base.TupleWithMeta Until a resize event is confirmed with :meth:`confirm_resize`, the old server will be kept around and you'll be able to roll back to the old flavor quickly with :meth:`revert_resize`. All resizes are - automatically confirmed after 24 hours. + automatically confirmed after 24 hours by default. """ - return self.manager.resize(self, flavor, **kwargs) + return self.manager.resize(self, flavor, disk_config=disk_config) def create_image(self, image_name, metadata=None): """ @@ -433,13 +531,19 @@ def revert_resize(self): def networks(self): """ Generate a simplified list of addresses + + :returns: An OrderedDict, keyed by network name, and sorted by network + name in ascending order. """ - networks = {} + networks = collections.OrderedDict() try: - for network_label, address_list in self.addresses.items(): + # Sort the keys by network name in natural (ascending) order. + network_labels = sorted(self.addresses.keys()) + for network_label in network_labels: + address_list = self.addresses[network_label] networks[network_label] = [a['addr'] for a in address_list] return networks - except Exception: + except AttributeError: return {} @api_versions.wraps("2.0", "2.24") @@ -478,7 +582,7 @@ def live_migrate(self, host=None, block_migration=None): block_migration = "auto" return self.manager.live_migrate(self, host, block_migration) - @api_versions.wraps("2.30") + @api_versions.wraps("2.30", "2.67") def live_migrate(self, host=None, block_migration=None, force=None): """ Migrates a running instance to a new machine. @@ -493,6 +597,20 @@ def live_migrate(self, host=None, block_migration=None, force=None): block_migration = "auto" return self.manager.live_migrate(self, host, block_migration, force) + @api_versions.wraps("2.68") + def live_migrate(self, host=None, block_migration=None): + """ + Migrates a running instance to a new machine. + + :param host: destination host name. + :param block_migration: if True, do block_migration, the default + value is None which is mapped to 'auto'. + :returns: An instance of novaclient.base.TupleWithMeta + """ + if block_migration is None: + block_migration = "auto" + return self.manager.live_migrate(self, host, block_migration) + def reset_state(self, state='error'): """ Reset the state of an instance to active or error. @@ -558,7 +676,7 @@ def evacuate(self, host=None, password=None): """ return self.manager.evacuate(self, host, password) - @api_versions.wraps("2.29") + @api_versions.wraps("2.29", "2.67") def evacuate(self, host=None, password=None, force=None): """ Evacuate an instance from failed host to specified host. @@ -571,6 +689,18 @@ def evacuate(self, host=None, password=None, force=None): """ return self.manager.evacuate(self, host, password, force) + @api_versions.wraps("2.68") + def evacuate(self, host=None, password=None): + """ + Evacuate an instance from failed host to specified host. + + :param host: Name of the target host + :param password: string to set as admin password on the evacuated + server. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.evacuate(self, host, password) + def interface_list(self): """ List interfaces attached to an instance. @@ -655,15 +785,53 @@ def __str__(self): class ServerManager(base.BootingManagerWithFind): resource_class = Server - def _boot(self, resource_url, response_key, name, image, flavor, - meta=None, files=None, userdata=None, - reservation_id=False, return_raw=False, min_count=None, - max_count=None, security_groups=None, key_name=None, - availability_zone=None, block_device_mapping=None, - block_device_mapping_v2=None, nics=None, scheduler_hints=None, - config_drive=None, admin_pass=None, disk_config=None, - access_ip_v4=None, access_ip_v6=None, description=None, - tags=None, **kwargs): + @staticmethod + def transform_userdata(userdata): + if hasattr(userdata, 'read'): + userdata = userdata.read() + + # NOTE(melwitt): Text file data is converted to bytes prior to + # base64 encoding. The utf-8 encoding will fail for binary files. + try: + userdata = userdata.encode("utf-8") + except AttributeError: + # In python 3, 'bytes' object has no attribute 'encode' + pass + + return base64.b64encode(userdata).decode('utf-8') + + def _boot( + self, + response_key, + name, + image, + flavor, + meta=None, + files=None, + userdata=None, + reservation_id=False, + return_raw=False, + min_count=None, + max_count=None, + security_groups=None, + key_name=None, + availability_zone=None, + block_device_mapping=None, + block_device_mapping_v2=None, + nics=None, + scheduler_hints=None, + config_drive=None, + admin_pass=None, + disk_config=None, + access_ip_v4=None, + access_ip_v6=None, + description=None, + tags=None, + trusted_image_certificates=None, + host=None, + hypervisor_hostname=None, + hostname=None, + ): """ Create (boot) a new server. """ @@ -673,25 +841,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, "flavorRef": str(base.getid(flavor)), }} if userdata: - if hasattr(userdata, 'read'): - userdata = userdata.read() - - # NOTE(melwitt): Text file data is converted to bytes prior to - # base64 encoding. The utf-8 encoding will fail for binary files. - if six.PY3: - try: - userdata = userdata.encode("utf-8") - except AttributeError: - # In python 3, 'bytes' object has no attribute 'encode' - pass - else: - try: - userdata = encodeutils.safe_encode(userdata) - except UnicodeDecodeError: - pass - - userdata_b64 = base64.b64encode(userdata).decode('utf-8') - body["server"]["user_data"] = userdata_b64 + body["server"]["user_data"] = self.transform_userdata(userdata) if meta: body["server"]["metadata"] = meta if reservation_id: @@ -729,7 +879,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, else: data = file_or_string - if six.PY3 and isinstance(data, str): + if isinstance(data, str): data = data.encode('utf-8') cont = base64.b64encode(data).decode('utf-8') personality.append({ @@ -741,6 +891,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, body["server"]["availability_zone"] = availability_zone # Block device mappings are passed as a list of dictionaries + # in the create API if block_device_mapping: body['server']['block_device_mapping'] = \ self._parse_block_device_mapping(block_device_mapping) @@ -758,7 +909,7 @@ def _boot(self, resource_url, response_key, name, image, flavor, if nics is not None: # With microversion 2.37+ nics can be an enum of 'auto' or 'none' # or a list of dicts. - if isinstance(nics, six.string_types): + if isinstance(nics, str): all_net_data = nics else: # NOTE(tr3buchet): nics can be an empty list @@ -799,8 +950,22 @@ def _boot(self, resource_url, response_key, name, image, flavor, if tags: body['server']['tags'] = tags - return self._create(resource_url, body, response_key, - return_raw=return_raw, **kwargs) + if trusted_image_certificates: + body['server']['trusted_image_certificates'] = ( + trusted_image_certificates) + + if host: + body['server']['host'] = host + + if hypervisor_hostname: + body['server']['hypervisor_hostname'] = hypervisor_hostname + + if hostname: + body['server']['hostname'] = hostname + + return self._create( + '/servers', body, response_key, return_raw=return_raw, + ) def get(self, server): """ @@ -821,10 +986,14 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, match the search_opts (optional). The search opts format is a dictionary of key / value pairs that will be appended to the query string. For a complete list of keys see: - https://developer.openstack.org/api-ref/compute/#list-servers + https://docs.openstack.org/api-ref/compute/#list-servers :param marker: Begin returning servers that appear later in the server list than that represented by this server id (optional). :param limit: Maximum number of servers to return (optional). + Note the API server has a configurable default limit. + If no limit is specified here or limit is larger than + default, the default limit will be used. + If limit == -1, all servers will be returned. :param sort_keys: List of sort keys :param sort_dirs: List of sort directions @@ -844,12 +1013,22 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, search_opts = {} qparams = {} - + # In microversion 2.73 we added ``locked`` filtering option + # for listing server details. + if ('locked' in search_opts and + self.api_version < api_versions.APIVersion('2.73')): + raise exceptions.UnsupportedAttribute("locked", "2.73") for opt, val in search_opts.items(): - if val: - if isinstance(val, six.text_type): + # support locked=False from 2.73 microversion + if val or (opt == 'locked' and val is False): + if isinstance(val, str): val = val.encode('utf-8') qparams[opt] = val + # NOTE(gibi): The False value won't actually do anything until we + # fix bug 1871409 and clean up the API inconsistency, but we do it + # in preparation for that (hopefully backportable) fix + if opt == 'config_drive' and val is not None: + qparams[opt] = str(val) detail = "" if detailed: @@ -891,69 +1070,6 @@ def list(self, detailed=True, search_opts=None, marker=None, limit=None, marker = result[-1].id return result - @api_versions.wraps('2.0', '2.43') - def add_fixed_ip(self, server, network_id): - """ - DEPRECATED Add an IP address on a network. - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param network_id: The ID of the network the IP should be on. - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % - 'addFixedIP', DeprecationWarning) - return self._action('addFixedIp', server, {'networkId': network_id}) - - @api_versions.wraps('2.0', '2.43') - def remove_fixed_ip(self, server, address): - """ - DEPRECATED Remove an IP address. - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param address: The IP address to remove. - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % - 'removeFixedIP', DeprecationWarning) - return self._action('removeFixedIp', server, {'address': address}) - - @api_versions.wraps('2.0', '2.43') - def add_floating_ip(self, server, address, fixed_address=None): - """ - DEPRECATED Add a floating IP to an instance - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param address: The FloatingIP or string floating address to add. - :param fixed_address: The FixedIP the floatingIP should be - associated with (optional) - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % - 'addFloatingIP', DeprecationWarning) - address = address.ip if hasattr(address, 'ip') else address - if fixed_address: - if hasattr(fixed_address, 'ip'): - fixed_address = fixed_address.ip - return self._action('addFloatingIp', server, - {'address': address, - 'fixed_address': fixed_address}) - else: - return self._action('addFloatingIp', server, {'address': address}) - - @api_versions.wraps('2.0', '2.43') - def remove_floating_ip(self, server, address): - """ - DEPRECATED Remove a floating IP address. - - :param server: The :class:`Server` (or its ID) to remove an IP from. - :param address: The FloatingIP or string floating address to remove. - :returns: An instance of novaclient.base.TupleWithMeta - """ - warnings.warn(ADD_REMOVE_FIXED_FLOATING_DEPRECATION_WARNING % - 'removeFloatingIP', DeprecationWarning) - address = address.ip if hasattr(address, 'ip') else address - return self._action('removeFloatingIp', server, {'address': address}) - def get_vnc_console(self, server, console_type): """ Get a vnc console for an instance @@ -1153,6 +1269,7 @@ def unpause(self, server): """ return self._action('unpause', server, None) + @api_versions.wraps("2.0", "2.72") def lock(self, server): """ Lock the server. @@ -1162,6 +1279,22 @@ def lock(self, server): """ return self._action('lock', server, None) + @api_versions.wraps("2.73") + def lock(self, server, reason=None): + """ + Lock the server. + + :param server: The :class:`Server` (or its ID) to lock + :param reason: (Optional) The lock reason. + :returns: An instance of novaclient.base.TupleWithMeta + """ + info = None + + if reason: + info = {'locked_reason': reason} + + return self._action('lock', server, info) + def unlock(self, server): """ Unlock the server. @@ -1234,6 +1367,7 @@ def shelve_offload(self, server): """ return self._action('shelveOffload', server, None) + @api_versions.wraps("2.0", "2.76") def unshelve(self, server): """ Unshelve the server. @@ -1243,6 +1377,51 @@ def unshelve(self, server): """ return self._action('unshelve', server, None) + @api_versions.wraps("2.77", "2.90") + def unshelve(self, server, availability_zone=None): + """ + Unshelve the server. + + :param server: The :class:`Server` (or its ID) to unshelve + :param availability_zone: The specified availability zone name + (Optional) + :returns: An instance of novaclient.base.TupleWithMeta + """ + info = None + if availability_zone: + info = {'availability_zone': availability_zone} + return self._action('unshelve', server, info) + + @api_versions.wraps("2.91") + def unshelve(self, server, availability_zone=object(), host=None): + """ + Unshelve the server. + + :param availability_zone: If specified the instance will be unshelved + to the availability_zone. + If None is passed the instance defined + availability_zone is unpin and the instance + will be scheduled to any availability_zone + (free scheduling). + If not specified the instance will be + unshelved to either its defined + availability_zone or any + availability_zone (free scheduling). + :param host: The specified host + (Optional) + :returns: An instance of novaclient.base.TupleWithMeta + """ + info = None + + if availability_zone is None or isinstance(availability_zone, str): + info = {'availability_zone': availability_zone} + if host: + if info: + info['host'] = host + else: + info = {'host': host} + return self._action('unshelve', server, info) + def ips(self, server): """ Return IP Addresses associated with the server. @@ -1250,7 +1429,7 @@ def ips(self, server): Often a cheaper call then getting all the details for a server. :param server: The :class:`Server` (or its ID) for which - the IP adresses are to be returned + the IP addresses are to be returned :returns: An instance of novaclient.base.DictWithMeta """ resp, body = self.api.client.get("/servers/%s/ips" % @@ -1269,6 +1448,19 @@ def diagnostics(self, server): base.getid(server)) return base.TupleWithMeta((resp, body), resp) + @api_versions.wraps("2.78") + def topology(self, server): + """ + Retrieve server topology. + + :param server: The :class:`Server` (or its ID) for which + topology to be returned + :returns: An instance of novaclient.base.DictWithMeta + """ + resp, body = self.api.client.get("/servers/%s/topology" % + base.getid(server)) + return base.DictWithMeta(body, resp) + def _validate_create_nics(self, nics): # nics are required with microversion 2.37+ and can be a string or list if self.api_version > api_versions.APIVersion('2.36'): @@ -1278,73 +1470,103 @@ def _validate_create_nics(self, nics): raise ValueError('nics must be a list or a tuple, not %s' % type(nics)) - def create(self, name, image, flavor, meta=None, files=None, - reservation_id=False, min_count=None, - max_count=None, security_groups=None, userdata=None, - key_name=None, availability_zone=None, - block_device_mapping=None, block_device_mapping_v2=None, - nics=None, scheduler_hints=None, - config_drive=None, disk_config=None, admin_pass=None, - access_ip_v4=None, access_ip_v6=None, **kwargs): - # TODO(anthony): indicate in doc string if param is an extension - # and/or optional + # NOTE(stephenfin): It would be nice to make everything bar name, image and + # flavor a kwarg-only argument but there are backwards-compatbility + # concerns + def create( + self, + name, + image, + flavor, + meta=None, + files=None, + reservation_id=False, + min_count=None, + max_count=None, + security_groups=None, + userdata=None, + key_name=None, + availability_zone=None, + block_device_mapping=None, + block_device_mapping_v2=None, + nics=None, + scheduler_hints=None, + config_drive=None, + disk_config=None, + admin_pass=None, + access_ip_v4=None, + access_ip_v6=None, + description=None, + tags=None, + trusted_image_certificates=None, + host=None, + hypervisor_hostname=None, + hostname=None, + ): """ Create (boot) a new server. + In order to create a server with pre-existing ports that contain a + ``resource_request`` value, such as for guaranteed minimum bandwidth + quality of service support, microversion ``2.72`` is required. + :param name: Something to name the server. :param image: The :class:`Image` to boot with. :param flavor: The :class:`Flavor` to boot onto. :param meta: A dict of arbitrary key/value metadata to store for this - server. Both keys and values must be <=255 characters. + server. Both keys and values must be <=255 characters. :param files: A dict of files to overwrite on the server upon boot. - Keys are file names (i.e. ``/etc/passwd``) and values - are the file contents (either as a string or as a - file-like object). A maximum of five entries is allowed, - and each file must be 10k or less. - :param reservation_id: return a reservation_id for the set of - servers being requested, boolean. - :param min_count: (optional extension) The minimum number of - servers to launch. - :param max_count: (optional extension) The maximum number of - servers to launch. + Keys are file names (i.e. ``/etc/passwd``) and values + are the file contents (either as a string or as a + file-like object). A maximum of five entries is allowed, + and each file must be 10k or less. + (deprecated starting with microversion 2.57) + :param reservation_id: Return a reservation_id for the set of + servers being requested, boolean. + :param min_count: The minimum number of servers to launch. + :param max_count: The maximum number of servers to launch. :param security_groups: A list of security group names - :param userdata: user data to pass to be exposed by the metadata - server this can be a file type object as well or a - string. - :param key_name: (optional extension) name of previously created - keypair to inject into the instance. + :param userdata: User data to pass to be exposed by the metadata + server this can be a file type object as well or a string. + :param key_name: Name of previously created keypair to inject into the + instance. :param availability_zone: Name of the availability zone for instance - placement. - :param block_device_mapping: (optional extension) A dict of block - device mappings for this server. - :param block_device_mapping_v2: (optional extension) A dict of block - device mappings for this server. - :param nics: An ordered list of nics (dicts) to be added to this - server, with information about connected networks, - fixed IPs, port etc. - Beginning in microversion 2.37 this field is required and - also supports a single string value of 'auto' or 'none'. - The 'auto' value means the Compute service will - automatically allocate a network for the project if one - is not available. This is the same behavior as not - passing anything for nics before microversion 2.37. The - 'none' value tells the Compute service to not allocate - any networking for the server. - :param scheduler_hints: (optional extension) arbitrary key-value pairs - specified by the client to help boot an instance - :param config_drive: (optional extension) value for config drive - either boolean, or volume-id - :param disk_config: (optional extension) control how the disk is - partitioned when the server is created. possible - values are 'AUTO' or 'MANUAL'. - :param admin_pass: (optional extension) add a user supplied admin - password. - :param access_ip_v4: (optional extension) add alternative access ip v4 - :param access_ip_v6: (optional extension) add alternative access ip v6 - :param description: optional description of the server (allowed since - microversion 2.19) - :param tags: A list of arbitrary strings to be added to the - server as tags (allowed since microversion 2.52) + placement. + :param block_device_mapping: A dict of block device mappings for this + server. + :param block_device_mapping_v2: A list of block device mappings (dicts) + for this server. + :param nics: An ordered list of nics (dicts) to be added to this + server, with information about connected networks, fixed IPs, port + etc. Beginning in microversion 2.37 this field is required and also + supports a single string value of 'auto' or 'none'. The 'auto' + value means the Compute service will automatically allocate a + network for the project if one is not available. This is the same + behavior as not passing anything for nics before microversion 2.37. + The 'none' value tells the Compute service to not allocate any + networking for the server. + :param scheduler_hints: Arbitrary key-value pairs specified by the + client to help boot an instance. + :param config_drive: A boolean value to enable config drive. + :param disk_config: Control how the disk is partitioned when the server + is created. Possible values are 'AUTO' or 'MANUAL'. + :param admin_pass: Add a user supplied admin password. + :param access_ip_v4: Add alternative access IP (v4) + :param access_ip_v6: Add alternative access IP (v6) + :param description: Optional description of the server + (allowed since microversion 2.19) + :param tags: A list of arbitrary strings to be added to the server as + tags + (allowed since microversion 2.52) + :param trusted_image_certificates: A list of trusted certificate IDs + (allowed since microversion 2.63) + :param host: Requested host to create servers + (allowed since microversion 2.74) + :param hypervisor_hostname: Requested hypervisor hostname to create + servers + (allowed since microversion 2.74) + :param hostname: Requested hostname of server + (allowed since microversion 2.90) """ if not min_count: min_count = 1 @@ -1355,8 +1577,7 @@ def create(self, name, image, flavor, meta=None, files=None, boot_args = [name, image, flavor] - descr_microversion = api_versions.APIVersion("2.19") - if "description" in kwargs and self.api_version < descr_microversion: + if description and self.api_version < api_versions.APIVersion("2.19"): raise exceptions.UnsupportedAttribute("description", "2.19") self._validate_create_nics(nics) @@ -1377,10 +1598,40 @@ def create(self, name, image, flavor, meta=None, files=None, "unsupported before microversion " "2.32") - boot_tags_microversion = api_versions.APIVersion("2.52") - if "tags" in kwargs and self.api_version < boot_tags_microversion: + if tags and self.api_version < api_versions.APIVersion("2.52"): raise exceptions.UnsupportedAttribute("tags", "2.52") + personality_files_deprecation = api_versions.APIVersion('2.57') + if files and self.api_version >= personality_files_deprecation: + raise exceptions.UnsupportedAttribute('files', '2.0', '2.56') + + trusted_certs_microversion = api_versions.APIVersion("2.63") + if (trusted_image_certificates and + self.api_version < trusted_certs_microversion): + raise exceptions.UnsupportedAttribute("trusted_image_certificates", + "2.63") + + if (block_device_mapping_v2 and + self.api_version < api_versions.APIVersion('2.67')): + for bdm in block_device_mapping_v2: + if bdm.get('volume_type'): + raise ValueError( + "Block device volume_type is not supported before " + "microversion 2.67") + + host_microversion = api_versions.APIVersion("2.74") + if host and self.api_version < host_microversion: + raise exceptions.UnsupportedAttribute("host", "2.74") + hypervisor_hostname_microversion = api_versions.APIVersion("2.74") + if (hypervisor_hostname and + self.api_version < hypervisor_hostname_microversion): + raise exceptions.UnsupportedAttribute( + "hypervisor_hostname", "2.74") + + hostname_microversion = api_versions.APIVersion("2.90") + if hostname and self.api_version < hostname_microversion: + raise exceptions.UnsupportedAttribute("hostname", "2.90") + boot_kwargs = dict( meta=meta, files=files, userdata=userdata, reservation_id=reservation_id, min_count=min_count, @@ -1388,30 +1639,31 @@ def create(self, name, image, flavor, meta=None, files=None, key_name=key_name, availability_zone=availability_zone, scheduler_hints=scheduler_hints, config_drive=config_drive, disk_config=disk_config, admin_pass=admin_pass, - access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6, **kwargs) + access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6, + description=description, tags=tags, + trusted_image_certificates=trusted_image_certificates, + host=host, hypervisor_hostname=hypervisor_hostname, + hostname=hostname) if block_device_mapping: - resource_url = "/os-volumes_boot" boot_kwargs['block_device_mapping'] = block_device_mapping elif block_device_mapping_v2: - resource_url = "/os-volumes_boot" boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2 - else: - resource_url = "/servers" + if nics: boot_kwargs['nics'] = nics response_key = "server" if not reservation_id else "reservation_id" - return self._boot(resource_url, response_key, *boot_args, - **boot_kwargs) + return self._boot(response_key, *boot_args, **boot_kwargs) @api_versions.wraps("2.0", "2.18") def update(self, server, name=None): """ - Update the name for a server. + Update attributes of a server. :param server: The :class:`Server` (or its ID) to update. :param name: Update the server's name. + :returns: :class:`Server` """ if name is None: return @@ -1424,15 +1676,16 @@ def update(self, server, name=None): return self._update("/servers/%s" % base.getid(server), body, "server") - @api_versions.wraps("2.19") + @api_versions.wraps("2.19", "2.89") def update(self, server, name=None, description=None): """ - Update the name or the description for a server. + Update attributes of a server. :param server: The :class:`Server` (or its ID) to update. :param name: Update the server's name. :param description: Update the server's description. If it equals to empty string(i.g. ""), the server description will be removed. + :returns: :class:`Server` """ if name is None and description is None: return @@ -1447,6 +1700,36 @@ def update(self, server, name=None, description=None): return self._update("/servers/%s" % base.getid(server), body, "server") + @api_versions.wraps("2.90") + def update(self, server, name=None, description=None, hostname=None): + """ + Update attributes of a server. + + :param server: The :class:`Server` (or its ID) to update. + :param name: Update the server's name. + :param description: Update the server's description. If it equals to + empty string(i.g. ""), the server description will be removed. + :param hostname: Update the server's hostname as recorded by the + metadata service. Note that a separate utility running on the + guest will be necessary to reflect these changes in the guest + itself. + :returns: :class:`Server` + """ + if name is None and description is None and hostname is None: + return + + body = {"server": {}} + if name: + body["server"]["name"] = name + if description == "": + body["server"]["description"] = None + elif description: + body["server"]["description"] = description + if hostname: + body["server"]["hostname"] = hostname + + return self._update("/servers/%s" % base.getid(server), body, "server") + def change_password(self, server, password): """ Update the password for a server. @@ -1477,52 +1760,135 @@ def reboot(self, server, reboot_type=REBOOT_SOFT): """ return self._action('reboot', server, {'type': reboot_type}) - def rebuild(self, server, image, password=None, disk_config=None, - preserve_ephemeral=False, name=None, meta=None, files=None, - **kwargs): + # NOTE(stephenfin): It would be nice to make everything bar server and + # image a kwarg-only argument but there are backwards-compatbility concerns + def rebuild( + self, + server, + image, + password=None, + disk_config=None, + preserve_ephemeral=False, + name=None, + meta=None, + files=None, + *, + description=_SENTINEL, + key_name=_SENTINEL, + userdata=_SENTINEL, + trusted_image_certificates=_SENTINEL, + hostname=_SENTINEL, + ): """ Rebuild -- shut down and then re-image -- a server. :param server: The :class:`Server` (or its ID) to share onto. - :param image: the :class:`Image` (or its ID) to re-image with. - :param password: string to set as password on the rebuilt server. - :param disk_config: partitioning mode to use on the rebuilt server. - Valid values are 'AUTO' or 'MANUAL' + :param image: The :class:`Image` (or its ID) to re-image with. + :param password: String to set as password on the rebuilt server. + :param disk_config: Partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL' :param preserve_ephemeral: If True, request that any ephemeral device be preserved when rebuilding the instance. Defaults to False. :param name: Something to name the server. :param meta: A dict of arbitrary key/value metadata to store for this - server. Both keys and values must be <=255 characters. + server. Both keys and values must be <=255 characters. :param files: A dict of files to overwrite on the server upon boot. - Keys are file names (i.e. ``/etc/passwd``) and values - are the file contents (either as a string or as a - file-like object). A maximum of five entries is allowed, - and each file must be 10k or less. - :param description: optional description of the server (allowed since - microversion 2.19) + Keys are file names (i.e. ``/etc/passwd``) and values are the file + contents (either as a string or as a file-like object). A maximum + of five entries is allowed, and each file must be 10k or less. + (deprecated starting with microversion 2.57) + :param description: Optional description of the server. If None is + specified, the existing description will be unset. + (starting from microversion 2.19) + :param key_name: Optional key pair name for rebuild operation. If None + is specified, the existing key will be unset. + (starting from microversion 2.54) + :param userdata: Optional user data to pass to be exposed by the + metadata server; this can be a file type object as well or a + string. If None is specified, the existing user_data is unset. + (starting from microversion 2.57) + :param trusted_image_certificates: A list of trusted certificate IDs + or None to unset/reset the servers trusted image certificates + (starting from microversion 2.63) + :param hostname: Optional hostname to configure for the instance. If + None is specified, the existing hostname will be unset. + (starting from microversion 2.90) :returns: :class:`Server` """ - descr_microversion = api_versions.APIVersion("2.19") - if "description" in kwargs and self.api_version < descr_microversion: - raise exceptions.UnsupportedAttribute("description", "2.19") + # Microversion 2.19 adds the optional 'description' parameter + if ( + description is not _SENTINEL and + self.api_version < api_versions.APIVersion('2.19') + ): + raise exceptions.UnsupportedAttribute('description', '2.19') + + # Microversion 2.54 adds the optional 'key_name' parameter + if ( + key_name is not _SENTINEL and + self.api_version < api_versions.APIVersion('2.54') + ): + raise exceptions.UnsupportedAttribute('key_name', '2.54') + + # Microversion 2.57 deprecates personality files and adds support + # for user_data. + if files and self.api_version >= api_versions.APIVersion('2.57'): + raise exceptions.UnsupportedAttribute('files', '2.0', '2.56') + + if ( + userdata is not _SENTINEL and + self.api_version < api_versions.APIVersion('2.57') + ): + raise exceptions.UnsupportedAttribute('userdata', '2.57') + + # Microversion 2.63 adds trusted image certificate support + if ( + trusted_image_certificates is not _SENTINEL and + self.api_version < api_versions.APIVersion('2.63') + ): + raise exceptions.UnsupportedAttribute( + 'trusted_image_certificates', '2.63') + + # Microversion 2.90 adds the optional 'hostname' parameter + if ( + hostname is not _SENTINEL and + self.api_version < api_versions.APIVersion('2.90') + ): + raise exceptions.UnsupportedAttribute('hostname', '2.90') body = {'imageRef': base.getid(image)} + if password is not None: body['adminPass'] = password + if disk_config is not None: body['OS-DCF:diskConfig'] = disk_config + if preserve_ephemeral is not False: body['preserve_ephemeral'] = True + if name is not None: body['name'] = name - if "description" in kwargs: - body["description"] = kwargs["description"] + + if description is not _SENTINEL: + body["description"] = description + + if key_name is not _SENTINEL: + body['key_name'] = key_name + + if trusted_image_certificates is not _SENTINEL: + body["trusted_image_certificates"] = trusted_image_certificates + + if hostname is not _SENTINEL: + body["hostname"] = hostname + if meta: body['metadata'] = meta + if files: personality = body['personality'] = [] - for filepath, file_or_string in sorted(files.items(), - key=lambda x: x[0]): + for filepath, file_or_string in sorted( + files.items(), key=lambda x: x[0], + ): if hasattr(file_or_string, 'read'): data = file_or_string.read() else: @@ -1534,10 +1900,19 @@ def rebuild(self, server, image, password=None, disk_config=None, 'contents': cont, }) - resp, body = self._action_return_resp_and_body('rebuild', server, - body, **kwargs) + if userdata is not _SENTINEL: + # If userdata is specified but None, it means unset the existing + # user_data on the instance. + userdata = userdata + body['user_data'] = (userdata if userdata is None else + self.transform_userdata(userdata)) + + resp, body = self._action_return_resp_and_body( + 'rebuild', server, body, + ) return Server(self, body['server'], resp=resp) + @api_versions.wraps("2.0", "2.55") def migrate(self, server): """ Migrate a server to a new host. @@ -1547,26 +1922,44 @@ def migrate(self, server): """ return self._action('migrate', server) - def resize(self, server, flavor, disk_config=None, **kwargs): + @api_versions.wraps("2.56") + def migrate(self, server, host=None): + """ + Migrate a server to a new host. + + :param server: The :class:`Server` (or its ID). + :param host: (Optional) The target host. + :returns: An instance of novaclient.base.TupleWithMeta + """ + info = {} + + if host: + info['host'] = host + + return self._action('migrate', server, info) + + # NOTE(stephenfin): It would be nice to make disk_config a kwarg-only + # argument but there are backwards-compatbility concerns + def resize(self, server, flavor, disk_config=None): """ Resize a server's resources. :param server: The :class:`Server` (or its ID) to share onto. - :param flavor: the :class:`Flavor` (or its ID) to resize to. - :param disk_config: partitioning mode to use on the rebuilt server. - Valid values are 'AUTO' or 'MANUAL' + :param flavor: The :class:`Flavor` (or its ID) to resize to. + :param disk_config: Partitioning mode to use on the rebuilt server. + Valid values are 'AUTO' or 'MANUAL'. :returns: An instance of novaclient.base.TupleWithMeta Until a resize event is confirmed with :meth:`confirm_resize`, the old server will be kept around and you'll be able to roll back to the old flavor quickly with :meth:`revert_resize`. All resizes are - automatically confirmed after 24 hours. + automatically confirmed after 24 hours by default. """ info = {'flavorRef': base.getid(flavor)} if disk_config is not None: info['OS-DCF:diskConfig'] = disk_config - return self._action('resize', server, info=info, **kwargs) + return self._action('resize', server, info=info) def confirm_resize(self, server): """ @@ -1646,6 +2039,26 @@ def set_meta_item(self, server, key, value): return self._update("/servers/%s/metadata/%s" % (base.getid(server), key), body) + def list_meta(self, server): + """ + Lists all metadata for a server. + :param server: The :class:`Server` (or its ID). + :returns: A dict of metadata. + """ + resp, body = self.api.client.get("/servers/%s/metadata" % + base.getid(server)) + return base.DictWithMeta(body, resp) + + def get_meta(self, server, key): + """ + Shows details for a metadata item, by key, for a server. + :param server: The :class:`Server` (or its ID). + :param key: metadata key to get + """ + resp, body = self.api.client.get("/servers/%s/metadata/%s" % + (base.getid(server), key)) + return base.DictWithMeta(body, resp) + def get_console_output(self, server, length=None): """ Get text console log output from Server. @@ -1677,6 +2090,24 @@ def delete_meta(self, server, keys): return result + def _live_migrate(self, server, host, block_migration, disk_over_commit, + force): + """Inner function to abstract changes in live migration API.""" + body = { + 'host': host, + 'block_migration': block_migration, + } + + if disk_over_commit is not None: + body['disk_over_commit'] = disk_over_commit + + # NOTE(stephenfin): For some silly reason, we don't set this if it's + # False, hence why we're not explicitly checking against None + if force: + body['force'] = force + + return self._action('os-migrateLive', server, body) + @api_versions.wraps('2.0', '2.24') def live_migrate(self, server, host, block_migration, disk_over_commit): """ @@ -1688,10 +2119,10 @@ def live_migrate(self, server, host, block_migration, disk_over_commit): :param disk_over_commit: if True, allow disk overcommit. :returns: An instance of novaclient.base.TupleWithMeta """ - return self._action('os-migrateLive', server, - {'host': host, - 'block_migration': block_migration, - 'disk_over_commit': disk_over_commit}) + return self._live_migrate(server, host, + block_migration=block_migration, + disk_over_commit=disk_over_commit, + force=None) @api_versions.wraps('2.25', '2.29') def live_migrate(self, server, host, block_migration): @@ -1704,11 +2135,12 @@ def live_migrate(self, server, host, block_migration): 'auto' :returns: An instance of novaclient.base.TupleWithMeta """ - return self._action('os-migrateLive', server, - {'host': host, - 'block_migration': block_migration}) + return self._live_migrate(server, host, + block_migration=block_migration, + disk_over_commit=None, + force=None) - @api_versions.wraps('2.30') + @api_versions.wraps('2.30', '2.67') def live_migrate(self, server, host, block_migration, force=None): """ Migrates a running instance to a new machine. @@ -1720,10 +2152,26 @@ def live_migrate(self, server, host, block_migration, force=None): :param force: forces to bypass the scheduler if host is provided. :returns: An instance of novaclient.base.TupleWithMeta """ - body = {'host': host, 'block_migration': block_migration} - if force: - body['force'] = force - return self._action('os-migrateLive', server, body) + return self._live_migrate(server, host, + block_migration=block_migration, + disk_over_commit=None, + force=force) + + @api_versions.wraps('2.68') + def live_migrate(self, server, host, block_migration): + """ + Migrates a running instance to a new machine. + + :param server: instance id which comes from nova list. + :param host: destination host name. + :param block_migration: if True, do block_migration, can be set as + 'auto' + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._live_migrate(server, host, + block_migration=block_migration, + disk_over_commit=None, + force=None) def reset_state(self, server, state='error'): """ @@ -1778,78 +2226,89 @@ def list_security_group(self, server): base.getid(server), 'security_groups', SecurityGroup) + def _evacuate(self, server, host, on_shared_storage, password, force): + """Inner function to abstract changes in evacuate API.""" + body = {} + + if on_shared_storage is not None: + body['onSharedStorage'] = on_shared_storage + + if host is not None: + body['host'] = host + + if password is not None: + body['adminPass'] = password + + if force: + body['force'] = force + + resp, body = self._action_return_resp_and_body('evacuate', server, + body) + return base.TupleWithMeta((resp, body), resp) + @api_versions.wraps("2.0", "2.13") def evacuate(self, server, host=None, on_shared_storage=True, password=None): """ Evacuate a server instance. - :param server: The :class:`Server` (or its ID) to share onto. + :param server: The :class:`Server` (or its ID) to evacuate to. :param host: Name of the target host. :param on_shared_storage: Specifies whether instance files located on shared storage :param password: string to set as password on the evacuated server. :returns: An instance of novaclient.base.TupleWithMeta """ - - body = {'onSharedStorage': on_shared_storage} - if host is not None: - body['host'] = host - - if password is not None: - body['adminPass'] = password - - resp, body = self._action_return_resp_and_body('evacuate', server, - body) - return base.TupleWithMeta((resp, body), resp) + return self._evacuate(server, host, + on_shared_storage=on_shared_storage, + password=password, + force=None) @api_versions.wraps("2.14", "2.28") def evacuate(self, server, host=None, password=None): """ Evacuate a server instance. - :param server: The :class:`Server` (or its ID) to share onto. + :param server: The :class:`Server` (or its ID) to evacuate to. :param host: Name of the target host. :param password: string to set as password on the evacuated server. :returns: An instance of novaclient.base.TupleWithMeta """ + return self._evacuate(server, host, + on_shared_storage=None, + password=password, + force=None) - body = {} - if host is not None: - body['host'] = host - - if password is not None: - body['adminPass'] = password - - resp, body = self._action_return_resp_and_body('evacuate', server, - body) - return base.TupleWithMeta((resp, body), resp) - - @api_versions.wraps("2.29") + @api_versions.wraps("2.29", "2.67") def evacuate(self, server, host=None, password=None, force=None): """ Evacuate a server instance. - :param server: The :class:`Server` (or its ID) to share onto. + :param server: The :class:`Server` (or its ID) to evacuate to. :param host: Name of the target host. :param password: string to set as password on the evacuated server. :param force: forces to bypass the scheduler if host is provided. :returns: An instance of novaclient.base.TupleWithMeta """ + return self._evacuate(server, host, + on_shared_storage=None, + password=password, + force=force) - body = {} - if host is not None: - body['host'] = host - - if password is not None: - body['adminPass'] = password - - if force: - body['force'] = force + @api_versions.wraps("2.68") + def evacuate(self, server, host=None, password=None): + """ + Evacuate a server instance. - resp, body = self._action_return_resp_and_body('evacuate', server, - body) - return base.TupleWithMeta((resp, body), resp) + :param server: The :class:`Server` (or its ID) to evacuate to. + :param host: Name of the target host. + :param password: string to set as password on the evacuated server. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._evacuate(server, host, + on_shared_storage=None, + password=password, + force=None) def interface_list(self, server): """ @@ -1879,7 +2338,8 @@ def interface_attach(self, server, port_id, net_id, fixed_ip): {'ip_address': fixed_ip}] return self._create('/servers/%s/os-interface' % base.getid(server), - body, 'interfaceAttachment') + body, 'interfaceAttachment', + obj_class=NetworkInterface) @api_versions.wraps("2.49") def interface_attach(self, server, port_id, net_id, fixed_ip, tag=None): @@ -1908,7 +2368,8 @@ def interface_attach(self, server, port_id, net_id, fixed_ip, tag=None): body['interfaceAttachment']['tag'] = tag return self._create('/servers/%s/os-interface' % base.getid(server), - body, 'interfaceAttachment') + body, 'interfaceAttachment', + obj_class=NetworkInterface) def interface_detach(self, server, port_id): """ diff --git a/novaclient/v2/services.py b/novaclient/v2/services.py index f3d1255dc..7adbf1376 100644 --- a/novaclient/v2/services.py +++ b/novaclient/v2/services.py @@ -14,9 +14,10 @@ # under the License. """ -service interface +Service interface. """ -from six.moves import urllib + +from urllib import parse from novaclient import api_versions from novaclient import base @@ -48,7 +49,7 @@ def list(self, host=None, binary=None): if binary: filters.append(("binary", binary)) if filters: - url = "%s?%s" % (url, urllib.parse.urlencode(filters)) + url = "%s?%s" % (url, parse.urlencode(filters)) return self._list(url, "services") @api_versions.wraps("2.0", "2.10") diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 918984d0a..7242dbc13 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -16,8 +16,6 @@ # License for the specific language governing permissions and limitations # under the License. -from __future__ import print_function - import argparse import collections import datetime @@ -31,7 +29,6 @@ from oslo_utils import netutils from oslo_utils import strutils from oslo_utils import timeutils -import six import novaclient from novaclient import api_versions @@ -48,47 +45,22 @@ logger = logging.getLogger(__name__) -CERT_DEPRECATION_WARNING = ( - _('The nova-cert service is deprecated. This command will be removed ' - 'in the first major release after the Nova server 16.0.0 Pike release.') -) - -CLOUDPIPE_DEPRECATION_WARNING = ( - _('The os-cloudpipe Nova API has been removed. This command will be ' - 'removed in the first major release after the Nova server 16.0.0 Pike ' - 'release.') -) - - -# NOTE(mriedem): Remove this along with the deprecated commands in the first -# major python-novaclient release AFTER the nova server 16.0.0 Pike release. -def emit_hosts_deprecation_warning(command_name, replacement=None): - if replacement is None: - print(_('WARNING: Command %s is deprecated and will be removed ' - 'in the first major release after the Nova server 16.0.0 ' - 'Pike release. There is no replacement or alternative for ' - 'this command. Specify --os-compute-api-version less than ' - '2.43 to continue using this command until it is removed.') % - command_name, file=sys.stderr) - else: - print(_('WARNING: Command %(command)s is deprecated and will be ' - 'removed in the first major release after the Nova server ' - '16.0.0 Pike release. Use %(replacement)s instead. Specify ' - '--os-compute-api-version less than 2.43 to continue using ' - 'this command until it is removed.') % - {'command': command_name, 'replacement': replacement}, - file=sys.stderr) - - -# NOTE(mriedem): Remove this along with the deprecated commands in the first -# major python-novaclient release AFTER the nova server 16.0.0 Pike release. -def emit_fixed_floating_deprecation_warning(command_name): - print(_('WARNING: Command %s is deprecated and will be removed ' - 'in the first major release after the Nova server 16.0.0 ' - 'Pike release. Use python-neutronclient or python-openstackclient' - 'instead. Specify --os-compute-api-version less than 2.44 ' - 'to continue using this command until it is removed.') % - command_name, file=sys.stderr) + +def emit_duplicated_image_with_warning(img, image_with): + img_uuid_list = [str(image.id) for image in img] + print(_('WARNING: Multiple matching images: %(img_uuid_list)s\n' + 'Using image: %(chosen_one)s') % + {'img_uuid_list': img_uuid_list, + 'chosen_one': img_uuid_list[0]}, + file=sys.stderr) + + +# TODO(takashin): Remove this along with the deprecated commands in the first +# major python-novaclient release AFTER the nova server 24.0.0 X release. +def _emit_agent_deprecation_warning(): + print('This command has been deprecated since 23.0.0 Wallaby Release ' + 'and will be removed in the first major release ' + 'after the Nova server 24.0.0 X release.', file=sys.stderr) CLIENT_BDM2_KEYS = { @@ -103,6 +75,7 @@ def emit_fixed_floating_deprecation_warning(command_name): 'type': 'device_type', 'shutdown': 'delete_on_termination', 'tag': 'tag', + 'volume_type': 'volume_type' # added in 2.67 } @@ -177,6 +150,8 @@ def _parse_block_device_mapping_v2(cs, args, image): 'delete_on_termination': False} bdm.append(bdm_dict) + supports_volume_type = cs.api_version >= api_versions.APIVersion('2.67') + for device_spec in args.block_device: spec_dict = _parse_device_spec(device_spec) bdm_dict = {} @@ -187,6 +162,12 @@ def _parse_block_device_mapping_v2(cs, args, image): "in API version %(version)s.") % {'version': cs.api_version.get_string()}) + if 'volume_type' in spec_dict and not supports_volume_type: + raise exceptions.CommandError( + _("'volume_type' in block device mapping is not supported " + "in API version %(version)s.") + % {'version': cs.api_version.get_string()}) + for key, value in spec_dict.items(): bdm_dict[CLIENT_BDM2_KEYS[key]] = value @@ -396,10 +377,13 @@ def _boot(cs, args): if not image and args.image_with: images = _match_image(cs, args.image_with) + if len(images) > 1: + emit_duplicated_image_with_warning(images, args.image_with) if images: - # TODO(harlowja): log a warning that we - # are selecting the first of many? image = images[0] + else: + raise exceptions.CommandError(_("No images match the property " + "expected by --image-with")) min_count = 1 max_count = 1 @@ -421,19 +405,23 @@ def _boot(cs, args): meta = _meta_parsing(args.meta) - files = {} - for f in args.files: - try: - dst, src = f.split('=', 1) - files[dst] = open(src) - except IOError as e: - raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % - {'src': src, 'exc': e}) - except ValueError: - raise exceptions.CommandError(_("Invalid file argument '%s'. " - "File arguments must be of the " - "form '--file " - "'") % f) + include_files = cs.api_version < api_versions.APIVersion('2.57') + if include_files: + files = {} + for f in args.files: + try: + dst, src = f.split('=', 1) + with open(src) as fo: + files[dst] = fo.read() + except IOError as e: + raise exceptions.CommandError( + _("Can't open '%(src)s': %(exc)s") % + {'src': src, 'exc': e}) + except ValueError: + raise exceptions.CommandError( + _("Invalid file argument '%s'. " + "File arguments must be of the " + "form '--file '") % f) # use the os-keypair extension key_name = None @@ -442,7 +430,8 @@ def _boot(cs, args): if args.user_data: try: - userdata = open(args.user_data) + with open(args.user_data) as f: + userdata = f.read() except IOError as e: raise exceptions.CommandError(_("Can't open '%(user_data)s': " "%(exc)s") % @@ -490,12 +479,11 @@ def _boot(cs, args): hints = {} if args.scheduler_hints: - for hint in args.scheduler_hints: - key, _sep, value = hint.partition('=') + for key, value in args.scheduler_hints: # NOTE(vish): multiple copies of the same hint will # result in a list of values if key in hints: - if isinstance(hints[key], six.string_types): + if isinstance(hints[key], str): hints[key] = [hints[key]] hints[key] += [value] else: @@ -507,11 +495,12 @@ def _boot(cs, args): elif str(args.config_drive).lower() in ("false", "0", "", "none"): config_drive = None else: - config_drive = args.config_drive + raise exceptions.CommandError( + _("The value of the '--config-drive' option must be " + "a boolean value.")) boot_kwargs = dict( meta=meta, - files=files, key_name=key_name, min_count=min_count, max_count=max_count, @@ -534,6 +523,33 @@ def _boot(cs, args): if 'tags' in args and args.tags: boot_kwargs["tags"] = args.tags.split(',') + if 'host' in args and args.host: + boot_kwargs["host"] = args.host + + if 'hypervisor_hostname' in args and args.hypervisor_hostname: + boot_kwargs["hypervisor_hostname"] = args.hypervisor_hostname + + if include_files: + boot_kwargs['files'] = files + + if ( + 'trusted_image_certificates' in args and + args.trusted_image_certificates + ): + boot_kwargs['trusted_image_certificates'] = ( + args.trusted_image_certificates) + elif utils.env('OS_TRUSTED_IMAGE_CERTIFICATE_IDS'): + if cs.api_version >= api_versions.APIVersion('2.63'): + boot_kwargs["trusted_image_certificates"] = utils.env( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS').split(',') + else: + raise exceptions.UnsupportedAttribute( + "OS_TRUSTED_IMAGE_CERTIFICATE_IDS", + "2.63") + + if 'hostname' in args and args.hostname: + boot_kwargs['hostname'] = args.hostname + return boot_args, boot_kwargs @@ -590,7 +606,11 @@ def _boot(cs, args): dest='files', default=[], help=_("Store arbitrary files from locally to " - "on the new server. Limited by the injected_files quota value.")) + "on the new server. More files can be injected using multiple " + "'--file' options. Limited by the 'injected_files' quota value. " + "The default value is 5. You can get the current quota value by " + "'Personality' limit from 'nova limits' command."), + start_version='2.0', end_version='2.56') @utils.arg( '--key-name', default=os.environ.get('NOVACLIENT_DEFAULT_KEY_NAME'), @@ -619,7 +639,7 @@ def _boot(cs, args): action='append', default=[], help=_("Block device mapping in the format " - "=:::.")) + "=:::.")) @utils.arg( '--block-device', metavar="key1=value1[,key2=value2...]", @@ -640,8 +660,8 @@ def _boot(cs, args): "if omitted, hypervisor driver chooses suitable device " "depending on selected bus; note the libvirt driver always " "uses default device names), " - "size=size of the block device in MB(for swap) and in " - "GB(for other formats) " + "size=size of the block device in MiB(for swap) and in " + "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " "bootindex=integer used for ordering the boot disks " @@ -670,8 +690,8 @@ def _boot(cs, args): "if omitted, hypervisor driver chooses suitable device " "depending on selected bus; note the libvirt driver always " "uses default device names), " - "size=size of the block device in MB(for swap) and in " - "GB(for other formats) " + "size=size of the block device in MiB(for swap) and in " + "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " "bootindex=integer used for ordering the boot disks " @@ -699,8 +719,8 @@ def _boot(cs, args): "if omitted, hypervisor driver chooses suitable device " "depending on selected bus; note the libvirt driver always " "uses default device names), " - "size=size of the block device in MB(for swap) and in " - "GB(for other formats) " + "size=size of the block device in MiB(for swap) and in " + "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " "bootindex=integer used for ordering the boot disks " @@ -714,6 +734,7 @@ def _boot(cs, args): action='append', default=[], start_version='2.42', + end_version='2.66', help=_("Block device mapping with the keys: " "id=UUID (image_id, snapshot_id or volume_id only if using source " "image, snapshot or volume) " @@ -727,8 +748,8 @@ def _boot(cs, args): "if omitted, hypervisor driver chooses suitable device " "depending on selected bus; note the libvirt driver always " "uses default device names), " - "size=size of the block device in MB(for swap) and in " - "GB(for other formats) " + "size=size of the block device in MiB(for swap) and in " + "GiB(for other formats) " "(if omitted, hypervisor driver calculates size), " "format=device will be formatted (e.g. swap, ntfs, ...; optional), " "bootindex=integer used for ordering the boot disks " @@ -737,22 +758,55 @@ def _boot(cs, args): "shutdown=shutdown behaviour (either preserve or remove, " "for local destination set to remove) and " "tag=device metadata tag (optional).")) +@utils.arg( + '--block-device', + metavar="key1=value1[,key2=value2...]", + action='append', + default=[], + start_version='2.67', + help=_("Block device mapping with the keys: " + "id=UUID (image_id, snapshot_id or volume_id only if using source " + "image, snapshot or volume) " + "source=source type (image, snapshot, volume or blank), " + "dest=destination type of the block device (volume or local), " + "bus=device's bus (e.g. uml, lxc, virtio, ...; if omitted, " + "hypervisor driver chooses a suitable default, " + "honoured only if device type is supplied) " + "type=device type (e.g. disk, cdrom, ...; defaults to 'disk') " + "device=name of the device (e.g. vda, xda, ...; " + "if omitted, hypervisor driver chooses suitable device " + "depending on selected bus; note the libvirt driver always " + "uses default device names), " + "size=size of the block device in MiB(for swap) and in " + "GiB(for other formats) " + "(if omitted, hypervisor driver calculates size), " + "format=device will be formatted (e.g. swap, ntfs, ...; optional), " + "bootindex=integer used for ordering the boot disks " + "(for image backed instances it is equal to 0, " + "for others need to be specified), " + "shutdown=shutdown behaviour (either preserve or remove, " + "for local destination set to remove) and " + "tag=device metadata tag (optional), " + "volume_type=type of volume to create (either ID or name) when " + "source is blank, image or snapshot and dest is volume (optional)." + )) @utils.arg( '--swap', metavar="", default=None, - help=_("Create and attach a local swap block device of MB.")) + help=_("Create and attach a local swap block device of MiB.")) @utils.arg( '--ephemeral', metavar="size=[,format=]", action='append', default=[], - help=_("Create and attach a local ephemeral block device of GB " + help=_("Create and attach a local ephemeral block device of GiB " "and format it to .")) @utils.arg( '--hint', action='append', dest='scheduler_hints', + type=_key_value_pairing, default=[], metavar='', help=_("Send arbitrary key/value pairs to the scheduler for custom " @@ -849,7 +903,7 @@ def _boot(cs, args): metavar="", dest='config_drive', default=False, - help=_("Enable config drive.")) + help=_("Enable config drive. The value must be a boolean value.")) @utils.arg( '--poll', dest='poll', @@ -894,13 +948,45 @@ def _boot(cs, args): action="store_true", default=False, help=_("Return a reservation id bound to created servers.")) +@utils.arg( + '--trusted-image-certificate-id', + metavar='', + action='append', + dest='trusted_image_certificates', + default=[], + help=_('Trusted image certificate IDs used to validate certificates ' + 'during the image signature verification process. ' + 'Defaults to env[OS_TRUSTED_IMAGE_CERTIFICATE_IDS]. ' + 'May be specified multiple times to pass multiple trusted image ' + 'certificate IDs.'), + start_version="2.63") +@utils.arg( + '--host', + metavar='', + dest='host', + default=None, + help=_('Requested host to create servers. Admin only by default.'), + start_version="2.74") +@utils.arg( + '--hypervisor-hostname', + metavar='', + dest='hypervisor_hostname', + default=None, + help=_('Requested hypervisor hostname to create servers. Admin only by ' + 'default.'), + start_version="2.74") +@utils.arg( + '--hostname', + help=_( + 'Hostname for the instance. This sets the hostname stored in the ' + 'metadata server: a utility such as cloud-init running on the guest ' + 'is required to propagate these changes to the guest.' + ), + start_version='2.90') def do_boot(cs, args): """Boot a new server.""" boot_args, boot_kwargs = _boot(cs, args) - extra_boot_kwargs = utils.get_resource_manager_extra_kwargs(do_boot, args) - boot_kwargs.update(extra_boot_kwargs) - server = cs.servers.create(*boot_args, **boot_kwargs) if boot_kwargs['reservation_id']: new_server = {'reservation_id': server} @@ -913,38 +999,6 @@ def do_boot(cs, args): _poll_for_status(cs.servers.get, server.id, 'building', ['active']) -def do_cloudpipe_list(cs, _args): - """DEPRECATED Print a list of all cloudpipe instances.""" - - print(CLOUDPIPE_DEPRECATION_WARNING, file=sys.stderr) - - cloudpipes = cs.cloudpipe.list() - columns = ['Project Id', "Public IP", "Public Port", "Internal IP"] - utils.print_list(cloudpipes, columns) - - -@utils.arg( - 'project', - metavar='', - help=_('UUID of the project to create the cloudpipe for.')) -def do_cloudpipe_create(cs, args): - """DEPRECATED Create a cloudpipe instance for the given project.""" - - print(CLOUDPIPE_DEPRECATION_WARNING, file=sys.stderr) - - cs.cloudpipe.create(args.project) - - -@utils.arg('address', metavar='', help=_('New IP Address.')) -@utils.arg('port', metavar='', help=_('New Port.')) -def do_cloudpipe_configure(cs, args): - """DEPRECATED Update the VPN IP/port of a cloudpipe instance.""" - - print(CLOUDPIPE_DEPRECATION_WARNING, file=sys.stderr) - - cs.cloudpipe.update(args.address, args.port) - - def _poll_for_status(poll_fn, obj_id, action, final_ok_states, poll_period=5, show_progress=True, status_field="status", silent=False): @@ -1006,6 +1060,7 @@ def _expand_dict_attr(collection, attr): delattr(item, attr) for subkey in field.keys(): setattr(item, attr + ':' + subkey, field[subkey]) + item.set_info(attr + ':' + subkey, field[subkey]) def _translate_keys(collection, convert): @@ -1015,6 +1070,7 @@ def _translate_keys(collection, convert): for from_key, to_key in convert: if from_key in keys and to_key not in keys: setattr(item, to_key, item_dict[from_key]) + item.set_info(to_key, item_dict[from_key]) def _translate_extended_states(collection): @@ -1039,10 +1095,12 @@ def _translate_extended_states(collection): getattr(item, 'task_state') except AttributeError: setattr(item, 'task_state', "N/A") + item.set_info('power_state', item.power_state) + item.set_info('task_state', item.task_state) def _translate_flavor_keys(collection): - _translate_keys(collection, [('ram', 'memory_mb')]) + _translate_keys(collection, [('ram', 'memory_mib')]) def _print_flavor_extra_specs(flavor): @@ -1052,13 +1110,13 @@ def _print_flavor_extra_specs(flavor): return "N/A" -def _print_flavor_list(flavors, show_extra_specs=False): +def _print_flavor_list(cs, flavors, show_extra_specs=False): _translate_flavor_keys(flavors) headers = [ 'ID', 'Name', - 'Memory_MB', + 'Memory_MiB', 'Disk', 'Ephemeral', 'Swap', @@ -1067,11 +1125,16 @@ def _print_flavor_list(flavors, show_extra_specs=False): 'Is_Public', ] + formatters = {} if show_extra_specs: - formatters = {'extra_specs': _print_flavor_extra_specs} + # Starting with microversion 2.61, extra specs are included + # in the flavor details response. + if cs.api_version < api_versions.APIVersion('2.61'): + formatters = {'extra_specs': _print_flavor_extra_specs} headers.append('extra_specs') - else: - formatters = {} + + if cs.api_version >= api_versions.APIVersion('2.55'): + headers.append('Description') utils.print_list(flavors, headers, formatters) @@ -1106,7 +1169,7 @@ def _print_flavor_list(flavors, show_extra_specs=False): dest='min_ram', metavar='', default=None, - help=_('Filters the flavors by a minimum RAM, in MB.')) + help=_('Filters the flavors by a minimum RAM, in MiB.')) @utils.arg( '--limit', dest='limit', @@ -1138,7 +1201,7 @@ def do_flavor_list(cs, args): flavors = cs.flavors.list(marker=args.marker, min_disk=args.min_disk, min_ram=args.min_ram, sort_key=args.sort_key, sort_dir=args.sort_dir, limit=args.limit) - _print_flavor_list(flavors, args.extra_specs) + _print_flavor_list(cs, flavors, args.extra_specs) @utils.arg( @@ -1147,9 +1210,8 @@ def do_flavor_list(cs, args): help=_("Name or ID of the flavor to delete.")) def do_flavor_delete(cs, args): """Delete a specific flavor""" - flavorid = _find_flavor(cs, args.flavor) - cs.flavors.delete(flavorid) - _print_flavor_list([flavorid]) + flavor = _find_flavor(cs, args.flavor) + cs.flavors.delete(flavor) @utils.arg( @@ -1174,15 +1236,15 @@ def do_flavor_show(cs, args): @utils.arg( 'ram', metavar='', - help=_("Memory size in MB.")) + help=_("Memory size in MiB.")) @utils.arg( 'disk', metavar='', - help=_("Disk size in GB.")) + help=_("Disk size in GiB.")) @utils.arg( '--ephemeral', metavar='', - help=_("Ephemeral space size in GB (default 0)."), + help=_("Ephemeral space size in GiB (default 0)."), default=0) @utils.arg( 'vcpus', @@ -1191,7 +1253,7 @@ def do_flavor_show(cs, args): @utils.arg( '--swap', metavar='', - help=_("Swap space size in MB (default 0)."), + help=_("Additional swap space size in MiB (default 0)."), default=0) @utils.arg( '--rxtx-factor', @@ -1204,12 +1266,39 @@ def do_flavor_show(cs, args): help=_("Make flavor accessible to the public (default true)."), type=lambda v: strutils.bool_from_string(v, True), default=True) +@utils.arg( + '--description', + metavar='', + help=_('A free form description of the flavor. Limited to 65535 ' + 'characters in length. Only printable characters are allowed.'), + start_version='2.55') def do_flavor_create(cs, args): """Create a new flavor.""" + if cs.api_version >= api_versions.APIVersion('2.55'): + description = args.description + else: + description = None f = cs.flavors.create(args.name, args.ram, args.vcpus, args.disk, args.id, args.ephemeral, args.swap, args.rxtx_factor, - args.is_public) - _print_flavor_list([f]) + args.is_public, description) + _print_flavor_list(cs, [f]) + + +@api_versions.wraps('2.55') +@utils.arg( + 'flavor', + metavar='', + help=_('Name or ID of the flavor to update.')) +@utils.arg( + 'description', + metavar='', + help=_('A free form description of the flavor. Limited to 65535 ' + 'characters in length. Only printable characters are allowed.')) +def do_flavor_update(cs, args): + """Update the description of an existing flavor.""" + flavorid = _find_flavor(cs, args.flavor) + flavor = cs.flavors.update(flavorid, args.description) + _print_flavor_list(cs, [flavor]) @utils.arg( @@ -1338,7 +1427,10 @@ def _print_flavor(flavor): info = flavor.to_dict() # ignore links, we don't need to present those info.pop('links') - info.update({"extra_specs": _print_flavor_extra_specs(flavor)}) + # Starting with microversion 2.61, extra specs are included + # in the flavor details response. + if 'extra_specs' not in info: + info.update({"extra_specs": _print_flavor_extra_specs(flavor)}) utils.print_dict(info) @@ -1366,12 +1458,6 @@ def _print_flavor(flavor): metavar='', default=None, help=_('Search with regular expression match by name.')) -@utils.arg( - '--instance-name', - dest='instance_name', - metavar='', - default=None, - help=_('Search with regular expression match by server name.')) @utils.arg( '--status', dest='status', @@ -1419,7 +1505,8 @@ def _print_flavor(flavor): dest='user', metavar='', nargs='?', - help=_('Display information from single user (Admin only).')) + help=_('Display information from single user (Admin only until ' + 'microversion 2.82).')) @utils.arg( '--deleted', dest='deleted', @@ -1462,14 +1549,83 @@ def _print_flavor(flavor): "will be displayed. If limit is bigger than 'CONF.api.max_limit' " "option of Nova API, limit 'CONF.api.max_limit' will be used " "instead.")) +@utils.arg( + '--availability-zone', + dest='availability_zone', + metavar='', + default=None, + help=_('Display servers based on their availability zone (Admin only ' + 'until microversion 2.82).')) +@utils.arg( + '--key-name', + dest='key_name', + metavar='', + default=None, + help=_('Display servers based on their keypair name (Admin only until ' + 'microversion 2.82).')) +@utils.arg( + '--config-drive', + action='store_true', + group='config_drive', + default=None, + help=_('Display servers that have a config drive attached. (Admin only ' + 'until microversion 2.82).')) +# NOTE(gibi): this won't actually do anything until bug 1871409 is fixed +# and the REST API is cleaned up regarding the values of config_drive +@utils.arg( + '--no-config-drive', + action='store_false', + group='config_drive', + help=_('Display servers that do not have a config drive attached (Admin ' + 'only until microversion 2.82)')) +@utils.arg( + '--progress', + dest='progress', + metavar='', + default=None, + help=_('Display servers based on their progress value (Admin only until ' + 'microversion 2.82).')) +@utils.arg( + '--vm-state', + dest='vm_state', + metavar='', + default=None, + help=_('Display servers based on their vm_state value (Admin only until ' + 'microversion 2.82).')) +@utils.arg( + '--task-state', + dest='task_state', + metavar='', + default=None, + help=_('Display servers based on their task_state value (Admin only until ' + 'microversion 2.82).')) +# TODO(gibi): this is now only work with the integer power state values. +# Later on we can extend this to accept the string values of the power state +# and translate it to integers towards the REST API. +@utils.arg( + '--power-state', + dest='power_state', + metavar='', + default=None, + help=_('Display servers based on their power_state value (Admin only ' + 'until microversion 2.82).')) @utils.arg( '--changes-since', dest='changes_since', metavar='', default=None, - help=_("List only servers changed after a certain point of time." - "The provided time should be an ISO 8061 formatted time." - "ex 2016-03-04T06:27:59Z .")) + help=_("List only servers changed later or equal to a certain point of " + "time. The provided time should be an ISO 8061 formatted time. " + "e.g. 2016-03-04T06:27:59Z .")) +@utils.arg( + '--changes-before', + dest='changes_before', + metavar='', + default=None, + help=_("List only servers changed earlier or equal to a certain point of " + "time. The provided time should be an ISO 8061 formatted time. " + "e.g. 2016-03-04T06:27:59Z ."), + start_version="2.66") @utils.arg( '--tags', dest='tags', @@ -1509,6 +1665,15 @@ def _print_flavor(flavor): "case is 'NOT(t1 OR t2)'. Tags must be separated by commas: " "--not-tags-any "), start_version="2.26") +@utils.arg( + '--locked', + dest='locked', + metavar='', + default=None, + help=_("Display servers based on their locked value. A value must be " + "specified; eg. 'true' will list only locked servers and 'false' " + "will list only unlocked servers."), + start_version="2.73") def do_list(cs, args): """List servers.""" imageid = None @@ -1518,8 +1683,9 @@ def do_list(cs, args): if args.flavor: flavorid = _find_flavor(cs, args.flavor).id # search by tenant or user only works with all_tenants - if args.tenant or args.user: + if args.tenant: args.all_tenants = 1 + search_opts = { 'all_tenants': args.all_tenants, 'reservation_id': args.reservation_id, @@ -1533,8 +1699,15 @@ def do_list(cs, args): 'user_id': args.user, 'host': args.host, 'deleted': args.deleted, - 'instance_name': args.instance_name, - 'changes-since': args.changes_since} + 'changes-since': args.changes_since, + 'availability_zone': args.availability_zone, + 'config_drive': args.config_drive, + 'key_name': args.key_name, + 'progress': args.progress, + 'vm_state': args.vm_state, + 'task_state': args.task_state, + 'power_state': args.power_state, + } for arg in ('tags', "tags-any", 'not-tags', 'not-tags-any'): if arg in args: @@ -1574,6 +1747,23 @@ def do_list(cs, args): raise exceptions.CommandError(_('Invalid changes-since value: %s') % search_opts['changes-since']) + # In microversion 2.66 we added ``changes-before`` option + # in server details. + have_added_changes_before = ( + cs.api_version >= api_versions.APIVersion('2.66')) + if have_added_changes_before and args.changes_before: + search_opts['changes-before'] = args.changes_before + try: + timeutils.parse_isotime(search_opts['changes-before']) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-before value: %s') + % search_opts['changes-before']) + + # In microversion 2.73 we added ``locked`` option in server details. + have_added_locked = cs.api_version >= api_versions.APIVersion('2.73') + if have_added_locked and args.locked: + search_opts['locked'] = args.locked + servers = cs.servers.list(detailed=detailed, search_opts=search_opts, sort_keys=sort_keys, @@ -1595,7 +1785,18 @@ def do_list(cs, args): # For detailed lists, if we have embedded flavor information then replace # the "flavor" attribute with more detailed information. if detailed and have_embedded_flavor_info: - _expand_dict_attr(servers, 'flavor') + if cs.api_version >= api_versions.APIVersion('2.69'): + # NOTE(tssurya): From 2.69, we will have the key 'flavor' missing + # in the server response during infrastructure failure situations. + # For those servers with partial constructs we just skip the + # process of expanding the flavor information. + servers_final = [] + for server in servers: + if hasattr(server, 'flavor'): + servers_final.append(server) + _expand_dict_attr(servers_final, 'flavor') + else: + _expand_dict_attr(servers, 'flavor') if servers: cols, fmts = _get_list_table_columns_and_formatters( @@ -1621,7 +1822,7 @@ def do_list(cs, args): # Tenant ID as well if search_opts['all_tenants']: columns.insert(2, 'Tenant ID') - if search_opts['changes-since']: + if search_opts['changes-since'] or search_opts.get('changes-before'): columns.append('Updated') formatters['Networks'] = utils.format_servers_list_networks sortby_index = 1 @@ -1673,12 +1874,19 @@ def _get_list_table_columns_and_formatters(fields, objs, exclude_fields=(), columns = [] formatters = {} + existing_fields = set() non_existent_fields = [] exclude_fields = set(exclude_fields) + # NOTE(ttsiouts): Bug #1733917. Validating the fields using the keys of + # the Resource.to_dict(). Adding also the 'networks' field. + if obj: + obj_dict = obj.to_dict() + existing_fields = set(['networks']) | set(obj_dict.keys()) + for field in fields.split(','): - if not hasattr(obj, field): + if field not in existing_fields: non_existent_fields.append(field) continue if field in exclude_fields: @@ -1785,41 +1993,160 @@ def do_reboot(cs, args): dest='files', default=[], help=_("Store arbitrary files from locally to " - "on the new server. You may store up to 5 files.")) + "on the new server. More files can be injected using multiple " + "'--file' options. You may store up to 5 files by default. " + "The maximum number of files is specified by the 'Personality' " + "limit reported by the 'nova limits' command."), + start_version='2.0', end_version='2.56') +@utils.arg( + '--key-name', + metavar='', + default=None, + help=_("Keypair name to set in the server. " + "Cannot be specified with the '--key-unset' option."), + start_version='2.54') +@utils.arg( + '--key-unset', + action='store_true', + default=False, + help=_("Unset keypair in the server. " + "Cannot be specified with the '--key-name' option."), + start_version='2.54') +@utils.arg( + '--user-data', + default=None, + metavar='', + help=_("User data file to pass to be exposed by the metadata server."), + start_version='2.57') +@utils.arg( + '--user-data-unset', + action='store_true', + default=False, + help=_("Unset user_data in the server. Cannot be specified with the " + "'--user-data' option."), + start_version='2.57') +@utils.arg( + '--trusted-image-certificate-id', + metavar='', + action='append', + dest='trusted_image_certificates', + default=[], + help=_('Trusted image certificate IDs used to validate certificates ' + 'during the image signature verification process. ' + 'Defaults to env[OS_TRUSTED_IMAGE_CERTIFICATE_IDS]. ' + 'May be specified multiple times to pass multiple trusted image ' + 'certificate IDs.'), + start_version="2.63") +@utils.arg( + '--trusted-image-certificates-unset', + action='store_true', + default=False, + help=_("Unset trusted_image_certificates in the server. Cannot be " + "specified with the '--trusted-image-certificate-id' option."), + start_version="2.63") +@utils.arg( + '--hostname', + help=_( + 'New hostname for the instance. This only updates the hostname ' + 'stored in the metadata server: a utility running on the guest ' + 'is required to propagate these changes to the guest.' + ), + start_version='2.90') def do_rebuild(cs, args): """Shutdown, re-image, and re-boot a server.""" server = _find_server(cs, args.server) image = _find_image(cs, args.image) + kwargs = {'preserve_ephemeral': args.preserve_ephemeral, + 'name': args.name, + 'meta': _meta_parsing(args.meta)} + if args.rebuild_password is not False: - _password = args.rebuild_password + kwargs['password'] = args.rebuild_password else: - _password = None + kwargs['password'] = None - kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) - kwargs['preserve_ephemeral'] = args.preserve_ephemeral - kwargs['name'] = args.name if 'description' in args: kwargs['description'] = args.description - meta = _meta_parsing(args.meta) - kwargs['meta'] = meta - files = {} - for f in args.files: - try: - dst, src = f.split('=', 1) - with open(src, 'r') as s: - files[dst] = s.read() - except IOError as e: - raise exceptions.CommandError(_("Can't open '%(src)s': %(exc)s") % - {'src': src, 'exc': e}) - except ValueError: - raise exceptions.CommandError(_("Invalid file argument '%s'. " - "File arguments must be of the " - "form '--file " - "'") % f) - kwargs['files'] = files - server = server.rebuild(image, _password, **kwargs) + # 2.57 deprecates the --file option and adds the --user-data and + # --user-data-unset options. + if cs.api_version < api_versions.APIVersion('2.57'): + files = {} + for f in args.files: + try: + dst, src = f.split('=', 1) + with open(src, 'r') as s: + files[dst] = s.read() + except IOError as e: + raise exceptions.CommandError( + _("Can't open '%(src)s': %(exc)s") % + {'src': src, 'exc': e}) + except ValueError: + raise exceptions.CommandError( + _("Invalid file argument '%s'. " + "File arguments must be of the " + "form '--file '") % f) + kwargs['files'] = files + else: + if args.user_data_unset: + kwargs['userdata'] = None + if args.user_data: + raise exceptions.CommandError( + _("Cannot specify '--user-data-unset' with " + "'--user-data'.")) + elif args.user_data: + try: + with open(args.user_data) as f: + kwargs['userdata'] = f.read() + except IOError as e: + raise exceptions.CommandError( + _("Can't open '%(user_data)s': %(exc)s") % { + 'user_data': args.user_data, + 'exc': e, + } + ) + if cs.api_version >= api_versions.APIVersion('2.54'): + if args.key_unset: + kwargs['key_name'] = None + if args.key_name: + raise exceptions.CommandError( + _("Cannot specify '--key-unset' with '--key-name'.")) + elif args.key_name: + kwargs['key_name'] = args.key_name + + if cs.api_version >= api_versions.APIVersion('2.63'): + # First determine if the user specified anything via the command line + # or the environment variable. + trusted_image_certificates = None + if ('trusted_image_certificates' in args and + args.trusted_image_certificates): + trusted_image_certificates = args.trusted_image_certificates + elif utils.env('OS_TRUSTED_IMAGE_CERTIFICATE_IDS'): + trusted_image_certificates = utils.env( + 'OS_TRUSTED_IMAGE_CERTIFICATE_IDS').split(',') + + if args.trusted_image_certificates_unset: + kwargs['trusted_image_certificates'] = None + # Check for conflicts in option usage. + if trusted_image_certificates: + raise exceptions.CommandError( + _("Cannot specify '--trusted-image-certificates-unset' " + "with '--trusted-image-certificate-id' or with " + "OS_TRUSTED_IMAGE_CERTIFICATE_IDS env variable set.")) + elif trusted_image_certificates: + # Only specify the kwarg if there is a value specified to avoid + # confusion with unsetting the value. + kwargs['trusted_image_certificates'] = trusted_image_certificates + elif utils.env('OS_TRUSTED_IMAGE_CERTIFICATE_IDS'): + raise exceptions.UnsupportedAttribute( + "OS_TRUSTED_IMAGE_CERTIFICATE_IDS", + "2.63") + + if 'hostname' in args and args.hostname is not None: + kwargs['hostname'] = args.hostname + + server = server.rebuild(image, **kwargs) _print_server(cs, args, server) if args.poll: @@ -1843,17 +2170,23 @@ def do_rebuild(cs, args): help=_('New description for the server. If it equals to empty string ' '(i.g. ""), the server description will be removed.'), start_version="2.19") +@utils.arg( + '--hostname', + help=_( + 'New hostname for the instance. This only updates the hostname ' + 'stored in the metadata server: a utility running on the guest ' + 'is required to propagate these changes to the guest.' + ), + start_version='2.90') def do_update(cs, args): """Update the name or the description for a server.""" update_kwargs = {} if args.name: update_kwargs["name"] = args.name - # NOTE(andreykurilin): `do_update` method is used by `do_rename` method, - # which do not have description argument at all. When `do_rename` will be - # removed after deprecation period, feel free to change the check below to: - # `if args.description:` if "description" in args and args.description is not None: update_kwargs["description"] = args.description + if "hostname" in args and args.hostname is not None: + update_kwargs["hostname"] = args.hostname _find_server(cs, args.server).update(**update_kwargs) @@ -1872,8 +2205,7 @@ def do_resize(cs, args): """Resize a server.""" server = _find_server(cs, args.server) flavor = _find_flavor(cs, args.flavor) - kwargs = utils.get_resource_manager_extra_kwargs(do_resize, args) - server.resize(flavor, **kwargs) + server.resize(flavor) if args.poll: _poll_for_status(cs.servers.get, server.id, 'resizing', ['active', 'verify_resize']) @@ -1892,6 +2224,12 @@ def do_resize_revert(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--host', + metavar='', + default=None, + help=_('Destination host name.'), + start_version='2.56') @utils.arg( '--poll', dest='poll', @@ -1899,9 +2237,13 @@ def do_resize_revert(cs, args): default=False, help=_('Report the server migration progress until it completes.')) def do_migrate(cs, args): - """Migrate a server. The new host will be selected by the scheduler.""" + """Migrate a server.""" + update_kwargs = {} + if 'host' in args and args.host: + update_kwargs['host'] = args.host + server = _find_server(cs, args.server) - server.migrate() + server.migrate(**update_kwargs) if args.poll: _poll_for_status(cs.servers.get, server.id, 'migrating', @@ -1960,12 +2302,23 @@ def do_start(cs, args): _("Unable to start the specified server(s).")) +# From microversion 2.73, we can specify a reason for locking the server. @utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--reason', + metavar='', + help=_('Reason for locking the server.'), + start_version='2.73') def do_lock(cs, args): """Lock a server. A normal (non-admin) user will not be able to execute actions on a locked server. """ - _find_server(cs, args.server).lock() + update_kwargs = {} + if 'reason' in args and args.reason is not None: + update_kwargs['reason'] = args.reason + + server = _find_server(cs, args.server) + server.lock(**update_kwargs) @utils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -2029,9 +2382,25 @@ def do_shelve_offload(cs, args): @utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--availability-zone', + metavar='', + default=None, + dest='availability_zone', + help=_('Name of the availability zone in which to unshelve a ' + 'SHELVED_OFFLOADED server.'), + start_version='2.77') def do_unshelve(cs, args): """Unshelve a server.""" - _find_server(cs, args.server).unshelve() + update_kwargs = {} + # Microversion >= 2.77 will support user to specify an + # availability_zone to unshelve a shelve offloaded server. + if cs.api_version >= api_versions.APIVersion('2.77'): + if 'availability_zone' in args and args.availability_zone is not None: + update_kwargs['availability_zone'] = args.availability_zone + + server = _find_server(cs, args.server) + server.unshelve(**update_kwargs) @utils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -2041,6 +2410,17 @@ def do_diagnostics(cs, args): utils.print_dict(cs.servers.diagnostics(server)[1], wrap=80) +@api_versions.wraps("2.78") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_server_topology(cs, args): + """Retrieve server topology.""" + server = _find_server(cs, args.server) + # This prints a dict with only two properties: nodes and pagesize_kb + # nodes is a list of dicts so it does not print very well, it's just a + # json blob in the output. + utils.print_dict(cs.servers.topology(server), wrap=80) + + @utils.arg( 'server', metavar='', help=_('Name or ID of a server for which the network cache should ' @@ -2174,7 +2554,11 @@ def _print_server(cs, args, server=None, wrap=0): minimal = getattr(args, "minimal", False) - networks = server.networks + try: + networks = server.networks + except Exception as e: + raise exceptions.CommandError(str(e)) + info = server.to_dict() for network_label, address_list in networks.items(): info['%s network' % network_label] = ', '.join(address_list) @@ -2229,6 +2613,7 @@ def _print_server(cs, args, server=None, wrap=0): info.pop('links', None) info.pop('addresses', None) + info.pop('OS-EXT-SRV-ATTR:user_data', None) utils.print_dict(info, wrap=wrap) @@ -2282,7 +2667,7 @@ def _find_server(cs, server, raise_if_notfound=True, **find_args): return utils.find_resource(cs.servers, server, wrap_exception=False) except exceptions.NoUniqueMatch as e: - raise exceptions.CommandError(six.text_type(e)) + raise exceptions.CommandError(str(e)) except exceptions.NotFound: # The server can be deleted return server @@ -2293,7 +2678,15 @@ def _find_image(cs, image): try: return cs.glance.find_image(image) except (exceptions.NotFound, exceptions.NoUniqueMatch) as e: - raise exceptions.CommandError(six.text_type(e)) + raise exceptions.CommandError(str(e)) + + +def _find_images(cs, images): + """Get images by name or ID.""" + try: + return cs.glance.find_images(images) + except (exceptions.NotFound, exceptions.NoUniqueMatch) as e: + raise exceptions.CommandError(str(e)) def _find_flavor(cs, flavor): @@ -2309,28 +2702,7 @@ def _find_network_id(cs, net_name): try: return cs.neutron.find_network(net_name).id except (exceptions.NotFound, exceptions.NoUniqueMatch) as e: - raise exceptions.CommandError(six.text_type(e)) - - -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg( - 'network_id', - metavar='', - help=_('Network ID.')) -def do_add_fixed_ip(cs, args): - """DEPRECATED Add new IP address on a network to server.""" - emit_fixed_floating_deprecation_warning('add-fixed-ip') - server = _find_server(cs, args.server) - server.add_fixed_ip(args.network_id) - - -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('address', metavar='
', help=_('IP Address.')) -def do_remove_fixed_ip(cs, args): - """DEPRECATED Remove an IP address from a server.""" - emit_fixed_floating_deprecation_warning('remove-fixed-ip') - server = _find_server(cs, args.server) - server.remove_fixed_ip(args.address) + raise exceptions.CommandError(str(e)) def _print_volume(volume): @@ -2367,6 +2739,13 @@ def _translate_volume_attachments_keys(collection): default=None, help=_('Tag for the attached volume.'), start_version="2.49") +@utils.arg( + '--delete-on-termination', + action='store_true', + default=False, + help=_('Specify if the attached volume should be deleted ' + 'when the server is destroyed.'), + start_version="2.79") def do_volume_attach(cs, args): """Attach a volume to a server.""" if args.device == 'auto': @@ -2376,6 +2755,9 @@ def do_volume_attach(cs, args): if 'tag' in args and args.tag: update_kwargs['tag'] = args.tag + if 'delete_on_termination' in args and args.delete_on_termination: + update_kwargs['delete_on_termination'] = args.delete_on_termination + volume = cs.volumes.create_server_volume(_find_server(cs, args.server).id, args.volume, args.device, @@ -2389,22 +2771,44 @@ def do_volume_attach(cs, args): help=_('Name or ID of server.')) @utils.arg( 'src_volume', - metavar='', + metavar='', help=_('ID of the source (original) volume.')) @utils.arg( 'dest_volume', - metavar='', + metavar='', help=_('ID of the destination volume.')) +@utils.arg( + '--delete-on-termination', + default=None, + group='delete_on_termination', + action='store_true', + help=_('Specify that the volume should be deleted ' + 'when the server is destroyed.'), + start_version='2.85') +@utils.arg( + '--no-delete-on-termination', + group='delete_on_termination', + action='store_false', + help=_('Specify that the volume should not be deleted ' + 'when the server is destroyed.'), + start_version='2.85') def do_volume_update(cs, args): """Update the attachment on the server. - Migrates the data from an attached volume to the - specified available volume and swaps out the active - attachment to the new volume. + If dest_volume is the same as the src_volume then the command migrates + the data from the attached volume to the specified available volume + and swaps out the active attachment to the new volume. Otherwise it + only updates the parameters of the existing attachment. """ + kwargs = dict() + if (cs.api_version >= api_versions.APIVersion('2.85') and + args.delete_on_termination is not None): + kwargs['delete_on_termination'] = args.delete_on_termination + cs.volumes.update_server_volume(_find_server(cs, args.server).id, args.src_volume, - args.dest_volume) + args.dest_volume, + **kwargs) @utils.arg( @@ -2429,7 +2833,18 @@ def do_volume_attachments(cs, args): """List all the volumes attached to a server.""" volumes = cs.volumes.get_server_volumes(_find_server(cs, args.server).id) _translate_volume_attachments_keys(volumes) - utils.print_list(volumes, ['ID', 'DEVICE', 'SERVER ID', 'VOLUME ID']) + # Microversion >= 2.70 returns the tag value. + fields = ['ID', 'DEVICE', 'SERVER ID', 'VOLUME ID'] + if cs.api_version >= api_versions.APIVersion('2.89'): + fields.remove('ID') + fields.append('ATTACHMENT ID') + fields.append('BDM UUID') + if cs.api_version >= api_versions.APIVersion('2.70'): + fields.append('TAG') + # Microversion >= 2.79 returns the delete_on_termination value. + if cs.api_version >= api_versions.APIVersion('2.79'): + fields.append('DELETE ON TERMINATION') + utils.print_list(volumes, fields) @api_versions.wraps('2.0', '2.5') @@ -2570,37 +2985,6 @@ def do_console_log(cs, args): print(data) -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('address', metavar='
', help=_('IP Address.')) -@utils.arg( - '--fixed-address', - metavar='', - default=None, - help=_('Fixed IP Address to associate with.')) -def do_floating_ip_associate(cs, args): - """DEPRECATED Associate a floating IP address to a server.""" - emit_fixed_floating_deprecation_warning('floating-ip-associate') - _associate_floating_ip(cs, args) - - -def _associate_floating_ip(cs, args): - server = _find_server(cs, args.server) - server.add_floating_ip(args.address, args.fixed_address) - - -@utils.arg('server', metavar='', help=_('Name or ID of server.')) -@utils.arg('address', metavar='
', help=_('IP Address.')) -def do_floating_ip_disassociate(cs, args): - """DEPRECATED Disassociate a floating IP address from a server.""" - emit_fixed_floating_deprecation_warning('floating-ip-disassociate') - _disassociate_floating_ip(cs, args) - - -def _disassociate_floating_ip(cs, args): - server = _find_server(cs, args.server) - server.remove_floating_ip(args.address) - - @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg( 'secgroup', @@ -2848,16 +3232,17 @@ def __init__(self, name, used, max, other): other = {} limit_names = [] columns = ['Name', 'Used', 'Max'] - for l in limits: - map = limit_map.get(l.name, {'name': l.name, 'type': 'other'}) + for limit in limits: + map = limit_map.get(limit.name, {'name': limit.name, 'type': 'other'}) name = map['name'] if map['type'] == 'max': - max[name] = l.value + max[name] = limit.value elif map['type'] == 'used': - used[name] = l.value + used[name] = limit.value else: - other[name] = l.value - columns.append('Other') + other[name] = limit.value + if 'Other' not in columns: + columns.append('Other') if name not in limit_names: limit_names.append(name) @@ -2865,11 +3250,10 @@ def __init__(self, name, used, max, other): limit_list = [] for name in limit_names: - l = Limit(name, - used.get(name, "-"), - max.get(name, "-"), - other.get(name, "-")) - limit_list.append(l) + limit_list.append(Limit( + name, used.get(name, '-'), max.get(name, '-'), + other.get(name, '-'), + )) utils.print_list(limit_list, columns) @@ -2943,8 +3327,8 @@ def _merge_usage_list(usages, next_usage_list): def do_usage_list(cs, args): """List usage data for all tenants.""" dateformat = "%Y-%m-%d" - rows = ["Tenant ID", "Servers", "RAM MB-Hours", "CPU Hours", - "Disk GB-Hours"] + rows = ["Tenant ID", "Servers", "RAM MiB-Hours", "CPU Hours", + "Disk GiB-Hours"] now = timeutils.utcnow() @@ -3012,7 +3396,7 @@ def simplify_usage(u): def do_usage(cs, args): """Show usage data for a single tenant.""" dateformat = "%Y-%m-%d" - rows = ["Servers", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] + rows = ["Servers", "RAM MiB-Hours", "CPU Hours", "Disk GiB-Hours"] now = timeutils.utcnow() @@ -3068,70 +3452,14 @@ def simplify_usage(u): print(_('None')) -@utils.arg( - 'pk_filename', - metavar='', - nargs='?', - default='pk.pem', - help=_('Filename for the private key. [Default: pk.pem]')) -@utils.arg( - 'cert_filename', - metavar='', - nargs='?', - default='cert.pem', - help=_('Filename for the X.509 certificate. [Default: cert.pem]')) -def do_x509_create_cert(cs, args): - """DEPRECATED Create x509 cert for a user in tenant.""" - print(CERT_DEPRECATION_WARNING, file=sys.stderr) - - if os.path.exists(args.pk_filename): - raise exceptions.CommandError(_("Unable to write privatekey - %s " - "exists.") % args.pk_filename) - if os.path.exists(args.cert_filename): - raise exceptions.CommandError(_("Unable to write x509 cert - %s " - "exists.") % args.cert_filename) - - certs = cs.certs.create() - - try: - old_umask = os.umask(0o377) - with open(args.pk_filename, 'w') as private_key: - private_key.write(certs.private_key) - print(_("Wrote private key to %s") % args.pk_filename) - finally: - os.umask(old_umask) - - with open(args.cert_filename, 'w') as cert: - cert.write(certs.data) - print(_("Wrote x509 certificate to %s") % args.cert_filename) - - -@utils.arg( - 'filename', - metavar='', - nargs='?', - default='cacert.pem', - help=_('Filename to write the x509 root cert.')) -def do_x509_get_root_cert(cs, args): - """DEPRECATED Fetch the x509 root cert.""" - print(CERT_DEPRECATION_WARNING, file=sys.stderr) - if os.path.exists(args.filename): - raise exceptions.CommandError(_("Unable to write x509 root cert - \ - %s exists.") % args.filename) - - with open(args.filename, 'w') as cert: - cacert = cs.certs.get() - cert.write(cacert.data) - print(_("Wrote x509 root cert to %s") % args.filename) - - @utils.arg( '--hypervisor', metavar='', default=None, help=_('Type of hypervisor.')) def do_agent_list(cs, args): - """List all builds.""" + """DEPRECATED List all builds.""" + _emit_agent_deprecation_warning() result = cs.agents.list(args.hypervisor) columns = ["Agent_id", "Hypervisor", "OS", "Architecture", "Version", 'Md5hash', 'Url'] @@ -3152,7 +3480,8 @@ def do_agent_list(cs, args): default='xen', help=_('Type of hypervisor.')) def do_agent_create(cs, args): - """Create new agent build.""" + """DEPRECATED Create new agent build.""" + _emit_agent_deprecation_warning() result = cs.agents.create(args.os, args.architecture, args.version, args.url, args.md5hash, args.hypervisor) @@ -3161,7 +3490,8 @@ def do_agent_create(cs, args): @utils.arg('id', metavar='', help=_('ID of the agent-build.')) def do_agent_delete(cs, args): - """Delete existing agent build.""" + """DEPRECATED Delete existing agent build.""" + _emit_agent_deprecation_warning() cs.agents.delete(args.id) @@ -3170,7 +3500,8 @@ def do_agent_delete(cs, args): @utils.arg('url', metavar='', help=_('URL')) @utils.arg('md5hash', metavar='', help=_('MD5 hash.')) def do_agent_modify(cs, args): - """Modify existing agent build.""" + """DEPRECATED Modify existing agent build.""" + _emit_agent_deprecation_warning() result = cs.agents.update(args.id, args.version, args.url, args.md5hash) utils.print_dict(result.to_dict()) @@ -3220,6 +3551,7 @@ def do_aggregate_delete(cs, args): help=_('Name or ID of aggregate to update.')) @utils.arg( '--name', + metavar='', dest='name', help=_('New name for aggregate.')) @utils.arg( @@ -3236,6 +3568,11 @@ def do_aggregate_update(cs, args): if args.availability_zone: updates["availability_zone"] = args.availability_zone + if not updates: + raise exceptions.CommandError(_( + "Either '--name ' or '--availability-zone " + "' must be specified.")) + aggregate = cs.aggregates.update(aggregate.id, updates) print(_("Aggregate %s has been successfully updated.") % aggregate.id) _print_aggregate_details(cs, aggregate) @@ -3329,10 +3666,26 @@ def parser_hosts(fields): utils.print_list([aggregate], columns, formatters=formatters) +@api_versions.wraps("2.81") +@utils.arg( + 'aggregate', metavar='', + help=_('Name or ID of the aggregate.')) +@utils.arg( + 'images', metavar='', nargs='+', + help=_('Name or ID of image(s) to cache on the hosts within ' + 'the aggregate.')) +def do_aggregate_cache_images(cs, args): + """Request images be cached.""" + aggregate = _find_aggregate(cs, args.aggregate) + images = _find_images(cs, args.images) + cs.aggregates.cache_images(aggregate, images) + + @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg( 'host', metavar='', default=None, nargs='?', - help=_('Destination host name.')) + help=_('Destination host name. If no host is specified, the scheduler ' + 'will choose one.')) @utils.arg( '--block-migrate', action='store_true', @@ -3359,8 +3712,13 @@ def parser_hosts(fields): dest='force', action='store_true', default=False, - help=_('Force to not verify the scheduler if a host is provided.'), - start_version='2.30') + help=_('Force a live-migration by not verifying the provided destination ' + 'host by the scheduler. WARNING: This could result in failures to ' + 'actually live migrate the server to the specified host. It is ' + 'recommended to either not specify a host so that the scheduler ' + 'will pick one, or specify a host without --force.'), + start_version='2.30', + end_version='2.67') def do_live_migration(cs, args): """Migrate running server to a new machine.""" @@ -3402,6 +3760,10 @@ def do_server_migration_list(cs, args): "memory_remaining_bytes", "disk_total_bytes", "disk_processed_bytes", "disk_remaining_bytes"] + if cs.api_version >= api_versions.APIVersion("2.80"): + fields.append("Project ID") + fields.append("User ID") + formatters = map(lambda field: utils.make_field_formatter(field)[1], format_key) formatters = dict(zip(format_name, formatters)) @@ -3493,13 +3855,9 @@ def do_service_list(cs, args): # values. @api_versions.wraps('2.0', '2.52') @utils.arg('host', metavar='', help=_('Name of host.')) -# TODO(mriedem): Eventually just hard-code the binary to "nova-compute". -@utils.arg('binary', metavar='', help=_('Service binary. The only ' - 'meaningful binary is "nova-compute". (Deprecated)'), - default='nova-compute', nargs='?') def do_service_enable(cs, args): """Enable the service.""" - result = cs.services.enable(args.host, args.binary) + result = cs.services.enable(args.host, 'nova-compute') utils.print_list([result], ['Host', 'Binary', 'Status']) @@ -3516,10 +3874,6 @@ def do_service_enable(cs, args): # values. @api_versions.wraps('2.0', '2.52') @utils.arg('host', metavar='', help=_('Name of host.')) -# TODO(mriedem): Eventually just hard-code the binary to "nova-compute". -@utils.arg('binary', metavar='', help=_('Service binary. The only ' - 'meaningful binary is "nova-compute". (Deprecated)'), - default='nova-compute', nargs='?') @utils.arg( '--reason', metavar='', @@ -3527,12 +3881,12 @@ def do_service_enable(cs, args): def do_service_disable(cs, args): """Disable the service.""" if args.reason: - result = cs.services.disable_log_reason(args.host, args.binary, + result = cs.services.disable_log_reason(args.host, 'nova-compute', args.reason) utils.print_list([result], ['Host', 'Binary', 'Status', 'Disabled Reason']) else: - result = cs.services.disable(args.host, args.binary) + result = cs.services.disable(args.host, 'nova-compute') utils.print_list([result], ['Host', 'Binary', 'Status']) @@ -3558,10 +3912,6 @@ def do_service_disable(cs, args): # values. @api_versions.wraps("2.11", "2.52") @utils.arg('host', metavar='', help=_('Name of host.')) -# TODO(mriedem): Eventually just hard-code the binary to "nova-compute". -@utils.arg('binary', metavar='', help=_('Service binary. The only ' - 'meaningful binary is "nova-compute". (Deprecated)'), - default='nova-compute', nargs='?') @utils.arg( '--unset', dest='force_down', @@ -3570,7 +3920,7 @@ def do_service_disable(cs, args): default=True) def do_service_force_down(cs, args): """Force service to down.""" - result = cs.services.force_down(args.host, args.binary, args.force_down) + result = cs.services.force_down(args.host, 'nova-compute', args.force_down) utils.print_list([result], ['Host', 'Binary', 'Forced down']) @@ -3596,85 +3946,28 @@ def do_service_force_down(cs, args): help=_('ID of service as an integer. Note that this may not ' 'uniquely identify a service in a multi-cell deployment.')) def do_service_delete(cs, args): - """Delete the service by integer ID.""" - cs.services.delete(args.id) + """Delete the service by integer ID. + + If deleting a nova-compute service, be sure to stop the actual + nova-compute process on the physical host before deleting the + service with this command. Failing to do so can lead to the running + service re-creating orphaned compute_nodes table records in the database. + """ + cs.services.delete(args.id) # Starting in microversion 2.53, the service is identified by UUID ID. @api_versions.wraps('2.53') @utils.arg('id', metavar='', help=_('ID of service as a UUID.')) def do_service_delete(cs, args): - """Delete the service by UUID ID.""" - cs.services.delete(args.id) - - -@utils.arg('host', metavar='', help=_('Name of host.')) -def do_host_describe(cs, args): - """DEPRECATED Describe a specific host.""" - emit_hosts_deprecation_warning('host-describe', 'hypervisor-show') - - result = cs.hosts.get(args.host) - columns = ["HOST", "PROJECT", "cpu", "memory_mb", "disk_gb"] - utils.print_list(result, columns) - - -@utils.arg( - '--zone', - metavar='', - default=None, - help=_('Filters the list, returning only those hosts in the availability ' - 'zone .')) -def do_host_list(cs, args): - """DEPRECATED List all hosts by service.""" - emit_hosts_deprecation_warning('host-list', 'hypervisor-list') - - columns = ["host_name", "service", "zone"] - result = cs.hosts.list(args.zone) - utils.print_list(result, columns) - - -@utils.arg('host', metavar='', help=_('Name of host.')) -@utils.arg( - '--status', metavar='', default=None, dest='status', - help=_('Either enable or disable a host.')) -@utils.arg( - '--maintenance', - metavar='', - default=None, - dest='maintenance', - help=_('Either put or resume host to/from maintenance.')) -def do_host_update(cs, args): - """DEPRECATED Update host settings.""" - if args.status == 'enable': - emit_hosts_deprecation_warning('host-update', 'service-enable') - elif args.status == 'disable': - emit_hosts_deprecation_warning('host-update', 'service-disable') - else: - emit_hosts_deprecation_warning('host-update') - - updates = {} - columns = ["HOST"] - if args.status: - updates['status'] = args.status - columns.append("status") - if args.maintenance: - updates['maintenance_mode'] = args.maintenance - columns.append("maintenance_mode") - result = cs.hosts.update(args.host, updates) - utils.print_list([result], columns) - - -@utils.arg('host', metavar='', help=_('Name of host.')) -@utils.arg( - '--action', metavar='', dest='action', - choices=['startup', 'shutdown', 'reboot'], - help=_('A power action: startup, reboot, or shutdown.')) -def do_host_action(cs, args): - """DEPRECATED Perform a power action on a host.""" - emit_hosts_deprecation_warning('host-action') + """Delete the service by UUID ID. - result = cs.hosts.host_action(args.host, args.action) - utils.print_list([result], ['HOST', 'power_action']) + If deleting a nova-compute service, be sure to stop the actual + nova-compute process on the physical host before deleting the + service with this command. Failing to do so can lead to the running + service re-creating orphaned compute_nodes table records in the database. + """ + cs.services.delete(args.id) def _find_hypervisor(cs, hypervisor): @@ -3795,12 +4088,22 @@ def do_hypervisor_uptime(cs, args): utils.print_dict(hyper.to_dict()) +@api_versions.wraps('2.0', '2.87') def do_hypervisor_stats(cs, args): """Get hypervisor statistics over all compute nodes.""" stats = cs.hypervisor_stats.statistics() utils.print_dict(stats.to_dict()) +@api_versions.wraps('2.88') +def do_hypervisor_stats(cs, args): + msg = _( + "The hypervisor-stats command is not supported in API version 2.88 " + "or later." + ) + raise exceptions.CommandError(msg) + + @utils.arg('server', metavar='', help=_('Name or ID of server.')) @utils.arg( '--port', @@ -3905,7 +4208,7 @@ def do_ssh(cs, args): cmd = "ssh -%d -p%d %s %s@%s %s" % (version, args.port, identity, args.login, ip_address, args.extra) logger.debug("Executing cmd '%s'", cmd) - os.system(cmd) + os.system(cmd) # nosec: B605 # NOTE(mriedem): In the 2.50 microversion, the os-quota-class-sets API @@ -3913,6 +4216,10 @@ def do_ssh(cs, args): # return floating_ips, fixed_ips, security_groups or security_group_members # as those are deprecated as networking service proxies and/or because # nova-network is deprecated. Similar to the 2.36 microversion. +# NOTE(mriedem): In the 2.57 microversion, the os-quota-sets and +# os-quota-class-sets APIs will no longer return injected_files, +# injected_file_content_bytes or injected_file_content_bytes since personality +# files (file injection) is deprecated starting with v2.57. _quota_resources = ['instances', 'cores', 'ram', 'floating_ips', 'fixed_ips', 'metadata_items', 'injected_files', 'injected_file_content_bytes', @@ -4116,6 +4423,7 @@ def do_quota_update(cs, args): # 2.36 does not support updating quota for floating IPs, fixed IPs, security # groups or security group rules. +# 2.57 does not support updating injected_file* quotas. @api_versions.wraps("2.36") @utils.arg( 'tenant', @@ -4152,19 +4460,22 @@ def do_quota_update(cs, args): metavar='', type=int, default=None, - help=_('New value for the "injected-files" quota.')) + help=_('New value for the "injected-files" quota.'), + start_version='2.36', end_version='2.56') @utils.arg( '--injected-file-content-bytes', metavar='', type=int, default=None, - help=_('New value for the "injected-file-content-bytes" quota.')) + help=_('New value for the "injected-file-content-bytes" quota.'), + start_version='2.36', end_version='2.56') @utils.arg( '--injected-file-path-bytes', metavar='', type=int, default=None, - help=_('New value for the "injected-file-path-bytes" quota.')) + help=_('New value for the "injected-file-path-bytes" quota.'), + start_version='2.36', end_version='2.56') @utils.arg( '--key-pairs', metavar='', @@ -4321,6 +4632,7 @@ def do_quota_class_update(cs, args): # 2.50 does not support updating quota class values for floating IPs, # fixed IPs, security groups or security group rules. +# 2.57 does not support updating injected_file* quotas. @api_versions.wraps("2.50") @utils.arg( 'class_name', @@ -4352,19 +4664,22 @@ def do_quota_class_update(cs, args): metavar='', type=int, default=None, - help=_('New value for the "injected-files" quota.')) + help=_('New value for the "injected-files" quota.'), + start_version='2.50', end_version='2.56') @utils.arg( '--injected-file-content-bytes', metavar='', type=int, default=None, - help=_('New value for the "injected-file-content-bytes" quota.')) + help=_('New value for the "injected-file-content-bytes" quota.'), + start_version='2.50', end_version='2.56') @utils.arg( '--injected-file-path-bytes', metavar='', type=int, default=None, - help=_('New value for the "injected-file-path-bytes" quota.')) + help=_('New value for the "injected-file-path-bytes" quota.'), + start_version='2.50', end_version='2.56') @utils.arg( '--key-pairs', metavar='', @@ -4413,11 +4728,17 @@ def do_quota_class_update(cs, args): dest='force', action='store_true', default=False, - help=_('Force to not verify the scheduler if a host is provided.'), - start_version='2.29') + help=_('Force an evacuation by not verifying the provided destination ' + 'host by the scheduler. WARNING: This could result in failures to ' + 'actually evacuate the server to the specified host. It is ' + 'recommended to either not specify a host so that the scheduler ' + 'will pick one, or specify a host without --force.'), + start_version='2.29', + end_version='2.67') def do_evacuate(cs, args): """Evacuate server from failed host.""" + # TODO(stephenfin): Simply call '_server_evacuate' instead? server = _find_server(cs, args.server) on_shared_storage = getattr(args, 'on_shared_storage', None) force = getattr(args, 'force', None) @@ -4432,9 +4753,11 @@ def do_evacuate(cs, args): utils.print_dict(res) -def _print_interfaces(interfaces): +def _print_interfaces(interfaces, show_tag=False): columns = ['Port State', 'Port ID', 'Net ID', 'IP addresses', 'MAC Addr'] + if show_tag: + columns.append('Tag') class FormattedInterface(object): def __init__(self, interface): @@ -4453,8 +4776,10 @@ def do_interface_list(cs, args): server = _find_server(cs, args.server) res = server.interface_list() - if isinstance(res, list): - _print_interfaces(res) + + # The "tag" field is in the response starting with microversion 2.70. + show_tag = cs.api_version >= api_versions.APIVersion('2.70') + _print_interfaces(res, show_tag=show_tag) @utils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -4488,10 +4813,21 @@ def do_interface_attach(cs, args): if 'tag' in args and args.tag: update_kwargs['tag'] = args.tag - res = server.interface_attach(args.port_id, args.net_id, args.fixed_ip, - **update_kwargs) - if isinstance(res, dict): - utils.print_dict(res) + network_interface = server.interface_attach( + args.port_id, args.net_id, args.fixed_ip, **update_kwargs) + + _print_interface(network_interface) + + +def _print_interface(interface): + ni_dict = interface.to_dict() + + fixed_ips = ni_dict.pop('fixed_ips', None) + ni_dict['ip_address'] = (",".join( + [fip['ip_address'] for fip in fixed_ips]) + if fixed_ips is not None else None) + + utils.print_dict(ni_dict) @utils.arg('server', metavar='', help=_('Name or ID of server.')) @@ -4499,10 +4835,7 @@ def do_interface_attach(cs, args): def do_interface_detach(cs, args): """Detach a network interface from a server.""" server = _find_server(cs, args.server) - - res = server.interface_detach(args.port_id) - if isinstance(res, dict): - utils.print_dict(res) + server.interface_detach(args.port_id) @api_versions.wraps("2.17") @@ -4574,16 +4907,15 @@ def do_availability_zone_list(cs, _args): sortby_index=None) -@api_versions.wraps("2.0", "2.12") def _print_server_group_details(cs, server_group): - columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] - utils.print_list(server_group, columns) - - -@api_versions.wraps("2.13") -def _print_server_group_details(cs, server_group): # noqa - columns = ['Id', 'Name', 'Project Id', 'User Id', - 'Policies', 'Members', 'Metadata'] + if cs.api_version < api_versions.APIVersion('2.13'): + columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata'] + elif cs.api_version < api_versions.APIVersion('2.64'): + columns = ['Id', 'Name', 'Project Id', 'User Id', + 'Policies', 'Members', 'Metadata'] + else: + columns = ['Id', 'Name', 'Project Id', 'User Id', + 'Policy', 'Rules', 'Members'] utils.print_list(server_group, columns) @@ -4618,12 +4950,12 @@ def do_server_group_list(cs, args): _print_server_group_details(cs, server_groups) +@api_versions.wraps("2.0", "2.63") @utils.arg('name', metavar='', help=_('Server group name.')) @utils.arg( 'policy', metavar='', - nargs='+', - help=_('Policies for the server groups.')) + help=_('Policy for the server group.')) def do_server_group_create(cs, args): """Create a new server group with the specified details.""" server_group = cs.server_groups.create(name=args.name, @@ -4631,6 +4963,30 @@ def do_server_group_create(cs, args): _print_server_group_details(cs, [server_group]) +@api_versions.wraps("2.64") +@utils.arg('name', metavar='', help=_('Server group name.')) +@utils.arg( + 'policy', + metavar='', + help=_('Policy for the server group.')) +@utils.arg( + '--rule', + metavar="", + dest='rules', + action='append', + default=[], + help=_('A rule for the policy. Currently, only the ' + '"max_server_per_host" rule is supported for the ' + '"anti-affinity" policy.')) +def do_server_group_create(cs, args): + """Create a new server group with the specified details.""" + rules = _meta_parsing(args.rules) + server_group = cs.server_groups.create(name=args.name, + policy=args.policy, + rules=rules) + _print_server_group_details(cs, [server_group]) + + @utils.arg( 'id', metavar='', @@ -4681,33 +5037,6 @@ def do_version_list(cs, args): utils.print_list(result, columns) -@api_versions.wraps("2.0", "2.11") -def _print_virtual_interface_list(cs, interface_list): - columns = ['Id', 'Mac address'] - utils.print_list(interface_list, columns) - - -@api_versions.wraps("2.12") -def _print_virtual_interface_list(cs, interface_list): - columns = ['Id', 'Mac address', 'Network ID'] - formatters = {"Network ID": lambda o: o.net_id} - utils.print_list(interface_list, columns, formatters) - - -@utils.arg('server', metavar='', help=_('ID of server.')) -def do_virtual_interface_list(cs, args): - """DEPRECATED Show virtual interface info about the given server.""" - print(_('WARNING: Command virtual-interface-list is deprecated and will ' - 'be removed in the first major release after the Nova server ' - '16.0.0 Pike release. There is no replacement or alternative for ' - 'this command. Specify --os-compute-api-version less than 2.44 ' - 'to continue using this command until it is removed.'), - file=sys.stderr) - server = _find_server(cs, args.server) - interface_list = cs.virtual_interfaces.list(base.getid(server)) - _print_virtual_interface_list(cs, interface_list) - - @api_versions.wraps("2.26") @utils.arg('server', metavar='', help=_('Name or ID of server.')) def do_server_tag_list(cs, args): @@ -4761,33 +5090,6 @@ def do_server_tag_delete_all(cs, args): server.delete_all_tags() -@utils.arg( - 'cell', - metavar='', - help=_('Name of the cell.')) -def do_cell_show(cs, args): - """Show details of a given cell.""" - cell = cs.cells.get(args.cell) - utils.print_dict(cell.to_dict()) - - -@utils.arg( - '--cell', - metavar='', - help=_("Name of the cell to get the capacities."), - default=None) -def do_cell_capacities(cs, args): - """Get cell capacities for all cells or a given cell.""" - cell = cs.cells.capacities(args.cell) - print(_("Ram Available: %s MB") % cell.capacities['ram_free']['total_mb']) - utils.print_dict(cell.capacities['ram_free']['units_by_mb'], - dict_property='Ram(MB)', dict_value="Units") - print(_("\nDisk Available: %s MB") % - cell.capacities['disk_free']['total_mb']) - utils.print_dict(cell.capacities['disk_free']['units_by_mb'], - dict_property='Disk(MB)', dict_value="Units") - - @utils.arg('server', metavar='', help='Name or ID of server.') def do_force_delete(cs, args): """Force delete a server.""" @@ -4808,8 +5110,11 @@ def _server_evacuate(cs, server, args): success = True error_message = "" try: - if api_versions.APIVersion("2.29") <= cs.api_version: - # if microversion >= 2.29 + if api_versions.APIVersion('2.68') <= cs.api_version: + # if microversion >= 2.68 + cs.servers.evacuate(server=server['uuid'], host=args.target_host) + elif api_versions.APIVersion('2.29') <= cs.api_version: + # if microversion 2.29 - 2.67 force = getattr(args, 'force', None) cs.servers.evacuate(server=server['uuid'], host=args.target_host, force=force) @@ -4830,6 +5135,23 @@ def _server_evacuate(cs, server, args): "error_message": error_message}) +def _hyper_servers(cs, host, strict): + hypervisors = cs.hypervisors.search(host, servers=True) + for hyper in hypervisors: + if strict and hyper.hypervisor_hostname != host: + continue + if hasattr(hyper, 'servers'): + for server in hyper.servers: + yield server + if strict: + break + else: + if strict: + msg = (_("No hypervisor matching '%s' could be found.") % + host) + raise exceptions.NotFound(404, msg) + + @utils.arg('host', metavar='', help='The hypervisor hostname (or pattern) to search for. ' 'WARNING: Use a fully qualified domain name if you only ' @@ -4853,20 +5175,29 @@ def _server_evacuate(cs, server, args): dest='force', action='store_true', default=False, - help=_('Force to not verify the scheduler if a host is provided.'), - start_version='2.29') + help=_('Force an evacuation by not verifying the provided destination ' + 'host by the scheduler. WARNING: This could result in failures to ' + 'actually evacuate the server to the specified host. It is ' + 'recommended to either not specify a host so that the scheduler ' + 'will pick one, or specify a host without --force.'), + start_version='2.29', + end_version='2.67') +@utils.arg( + '--strict', + dest='strict', + action='store_true', + default=False, + help=_('Evacuate host with exact hypervisor hostname match')) def do_host_evacuate(cs, args): """Evacuate all instances from failed host.""" - - hypervisors = cs.hypervisors.search(args.host, servers=True) response = [] - for hyper in hypervisors: - if hasattr(hyper, 'servers'): - for server in hyper.servers: - response.append(_server_evacuate(cs, server, args)) - - utils.print_list(response, - ["Server UUID", "Evacuate Accepted", "Error Message"]) + for server in _hyper_servers(cs, args.host, args.strict): + response.append(_server_evacuate(cs, server, args)) + utils.print_list(response, [ + "Server UUID", + "Evacuate Accepted", + "Error Message", + ]) def _server_live_migrate(cs, server, args): @@ -4904,7 +5235,8 @@ def __init__(self, server_uuid, live_migration_accepted, '--target-host', metavar='', default=None, - help=_('Name of target host.')) + help=_('Name of target host. If no host is specified, the scheduler will ' + 'choose one.')) @utils.arg( '--block-migrate', action='store_true', @@ -4934,24 +5266,36 @@ def __init__(self, server_uuid, live_migration_accepted, dest='force', action='store_true', default=False, - help=_('Force to not verify the scheduler if a host is provided.'), - start_version='2.30') + help=_('Force a live-migration by not verifying the provided destination ' + 'host by the scheduler. WARNING: This could result in failures to ' + 'actually live migrate the servers to the specified host. It is ' + 'recommended to either not specify a host so that the scheduler ' + 'will pick one, or specify a host without --force.'), + start_version='2.30', + end_version='2.67') +@utils.arg( + '--strict', + dest='strict', + action='store_true', + default=False, + help=_('live Evacuate host with exact hypervisor hostname match')) def do_host_evacuate_live(cs, args): - """Live migrate all instances of the specified host + """Live migrate all instances off the specified host to other available hosts. """ - hypervisors = cs.hypervisors.search(args.host, servers=True) response = [] migrating = 0 - for hyper in hypervisors: - for server in getattr(hyper, 'servers', []): - response.append(_server_live_migrate(cs, server, args)) - migrating += 1 - if args.max_servers is not None and migrating >= args.max_servers: - break - - utils.print_list(response, ["Server UUID", "Live Migration Accepted", - "Error Message"]) + for server in _hyper_servers(cs, args.host, args.strict): + response.append(_server_live_migrate(cs, server, args)) + migrating = migrating + 1 + if (args.max_servers is not None and + migrating >= args.max_servers): + break + utils.print_list(response, [ + "Server UUID", + "Live Migration Accepted", + "Error Message", + ]) class HostServersMigrateResponse(base.Resource): @@ -4976,20 +5320,24 @@ def _server_migrate(cs, server): help='The hypervisor hostname (or pattern) to search for. ' 'WARNING: Use a fully qualified domain name if you only ' 'want to cold migrate from a specific host.') +@utils.arg( + '--strict', + dest='strict', + action='store_true', + default=False, + help=_('Migrate host with exact hypervisor hostname match')) def do_host_servers_migrate(cs, args): """Cold migrate all instances off the specified host to other available hosts. """ - - hypervisors = cs.hypervisors.search(args.host, servers=True) response = [] - for hyper in hypervisors: - if hasattr(hyper, 'servers'): - for server in hyper.servers: - response.append(_server_migrate(cs, server)) - - utils.print_list(response, - ["Server UUID", "Migration Accepted", "Error Message"]) + for server in _hyper_servers(cs, args.host, args.strict): + response.append(_server_migrate(cs, server)) + utils.print_list(response, [ + "Server UUID", + "Migration Accepted", + "Error Message", + ]) @utils.arg( @@ -5020,6 +5368,7 @@ def do_instance_action(cs, args): utils.print_dict(action) +@api_versions.wraps("2.0", "2.57") @utils.arg( 'server', metavar='', @@ -5043,13 +5392,123 @@ def do_instance_action_list(cs, args): sortby_index=3) -def do_list_extensions(cs, _args): - """ - List all the os-api extensions that are available. - """ - extensions = cs.list_extensions.show_all() - fields = ["Name", "Summary", "Alias", "Updated"] - utils.print_list(extensions, fields) +@api_versions.wraps("2.58", "2.65") +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to list actions for. Only UUID can be ' + 'used to list actions on a deleted server.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last instance action of the previous page; displays list of ' + 'actions after "marker".')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_('Maximum number of instance actions to display. Note that there ' + 'is a configurable max limit on the server, and the limit that is ' + 'used will be the minimum of what is requested here and what ' + 'is configured in the server.')) +@utils.arg( + '--changes-since', + dest='changes_since', + metavar='', + default=None, + help=_('List only instance actions changed later or equal to a certain ' + 'point of time. The provided time should be an ISO 8061 formatted ' + 'time. e.g. 2016-03-04T06:27:59Z.')) +def do_instance_action_list(cs, args): + """List actions on a server.""" + server = _find_server(cs, args.server, raise_if_notfound=False) + if args.changes_since: + try: + timeutils.parse_isotime(args.changes_since) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-since value: %s') + % args.changes_since) + actions = cs.instance_action.list(server, marker=args.marker, + limit=args.limit, + changes_since=args.changes_since) + # TODO(yikun): Output a "Marker" column if there is a next link? + utils.print_list(actions, + ['Action', 'Request_ID', 'Message', 'Start_Time', + 'Updated_At'], + sortby_index=3) + + +@api_versions.wraps("2.66") +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to list actions for. Only UUID can be ' + 'used to list actions on a deleted server.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last instance action of the previous page; displays list of ' + 'actions after "marker".')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_('Maximum number of instance actions to display. Note that there ' + 'is a configurable max limit on the server, and the limit that is ' + 'used will be the minimum of what is requested here and what ' + 'is configured in the server.')) +@utils.arg( + '--changes-since', + dest='changes_since', + metavar='', + default=None, + help=_('List only instance actions changed later or equal to a certain ' + 'point of time. The provided time should be an ISO 8061 formatted ' + 'time. e.g. 2016-03-04T06:27:59Z.')) +@utils.arg( + '--changes-before', + dest='changes_before', + metavar='', + default=None, + help=_('List only instance actions changed earlier or equal to a certain ' + 'point of time. The provided time should be an ISO 8061 formatted ' + 'time. e.g. 2016-03-04T06:27:59Z.'), + start_version="2.66") +def do_instance_action_list(cs, args): + """List actions on a server.""" + server = _find_server(cs, args.server, raise_if_notfound=False) + if args.changes_since: + try: + timeutils.parse_isotime(args.changes_since) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-since value: %s') + % args.changes_since) + + # In microversion 2.66 we added ``changes-before`` option + # in instance actions. + if args.changes_before: + try: + timeutils.parse_isotime(args.changes_before) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-before value: %s') + % args.changes_before) + + actions = cs.instance_action.list(server, marker=args.marker, + limit=args.limit, + changes_since=args.changes_since, + changes_before=args.changes_before) + utils.print_list(actions, + ['Action', 'Request_ID', 'Message', 'Start_Time', + 'Updated_At'], + sortby_index=3) @utils.arg('host', metavar='', @@ -5068,17 +5527,20 @@ def do_list_extensions(cs, _args): action='append', default=[], help=_('Metadata to set or delete (only key is necessary on delete)')) +@utils.arg( + '--strict', + dest='strict', + action='store_true', + default=False, + help=_('Set host-meta to the hypervisor with exact hostname match')) def do_host_meta(cs, args): """Set or Delete metadata on all instances of a host.""" - hypervisors = cs.hypervisors.search(args.host, servers=True) - for hyper in hypervisors: + for server in _hyper_servers(cs, args.host, args.strict): metadata = _extract_metadata(args) - if hasattr(hyper, 'servers'): - for server in hyper.servers: - if args.action == 'set': - cs.servers.set_meta(server['uuid'], metadata) - elif args.action == 'delete': - cs.servers.delete_meta(server['uuid'], metadata.keys()) + if args.action == 'set': + cs.servers.set_meta(server['uuid'], metadata) + elif args.action == 'delete': + cs.servers.delete_meta(server['uuid'], metadata.keys()) def _print_migrations(cs, migrations): @@ -5097,14 +5559,131 @@ def migration_type(migration): formatters = {'Old Flavor': old_flavor, 'New Flavor': new_flavor} + # Insert migrations UUID after ID + if cs.api_version >= api_versions.APIVersion("2.59"): + fields.insert(0, "UUID") + if cs.api_version >= api_versions.APIVersion("2.23"): fields.insert(0, "Id") fields.append("Type") formatters.update({"Type": migration_type}) + if cs.api_version >= api_versions.APIVersion("2.80"): + fields.append("Project ID") + fields.append("User ID") + utils.print_list(migrations, fields, formatters) +@api_versions.wraps("2.0", "2.58") +@utils.arg( + '--instance-uuid', + dest='instance_uuid', + metavar='', + help=_('Fetch migrations for the given instance.')) +@utils.arg( + '--host', + dest='host', + metavar='', + help=_('Fetch migrations for the given source or destination host.')) +@utils.arg( + '--status', + dest='status', + metavar='', + help=_('Fetch migrations for the given status.')) +@utils.arg( + '--migration-type', + dest='migration_type', + metavar='', + help=_('Filter migrations by type. Valid values are: evacuation, ' + 'live-migration, migration (cold), resize')) +@utils.arg( + '--source-compute', + dest='source_compute', + metavar='', + help=_('Filter migrations by source compute host name.')) +def do_migration_list(cs, args): + """Print a list of migrations.""" + migrations = cs.migrations.list(args.host, args.status, + instance_uuid=args.instance_uuid, + migration_type=args.migration_type, + source_compute=args.source_compute) + _print_migrations(cs, migrations) + + +@api_versions.wraps("2.59", "2.65") +@utils.arg( + '--instance-uuid', + dest='instance_uuid', + metavar='', + help=_('Fetch migrations for the given instance.')) +@utils.arg( + '--host', + dest='host', + metavar='', + help=_('Fetch migrations for the given source or destination host.')) +@utils.arg( + '--status', + dest='status', + metavar='', + help=_('Fetch migrations for the given status.')) +@utils.arg( + '--migration-type', + dest='migration_type', + metavar='', + help=_('Filter migrations by type. Valid values are: evacuation, ' + 'live-migration, migration (cold), resize')) +@utils.arg( + '--source-compute', + dest='source_compute', + metavar='', + help=_('Filter migrations by source compute host name.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last migration of the previous page; displays list of ' + 'migrations after "marker". Note that the marker is the ' + 'migration UUID.')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_('Maximum number of migrations to display. Note that there is a ' + 'configurable max limit on the server, and the limit that is used ' + 'will be the minimum of what is requested here and what ' + 'is configured in the server.')) +@utils.arg( + '--changes-since', + dest='changes_since', + metavar='', + default=None, + help=_('List only migrations changed later or equal to a certain point ' + 'of time. The provided time should be an ISO 8061 formatted time. ' + 'e.g. 2016-03-04T06:27:59Z .')) +def do_migration_list(cs, args): + """Print a list of migrations.""" + if args.changes_since: + try: + timeutils.parse_isotime(args.changes_since) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-since value: %s') + % args.changes_since) + + migrations = cs.migrations.list(args.host, args.status, + instance_uuid=args.instance_uuid, + marker=args.marker, limit=args.limit, + changes_since=args.changes_since, + migration_type=args.migration_type, + source_compute=args.source_compute) + # TODO(yikun): Output a "Marker" column if there is a next link? + _print_migrations(cs, migrations) + + +@api_versions.wraps("2.66") @utils.arg( '--instance-uuid', dest='instance_uuid', @@ -5114,14 +5693,117 @@ def migration_type(migration): '--host', dest='host', metavar='', - help=_('Fetch migrations for the given host.')) + help=_('Fetch migrations for the given source or destination host.')) @utils.arg( '--status', dest='status', metavar='', help=_('Fetch migrations for the given status.')) +@utils.arg( + '--migration-type', + dest='migration_type', + metavar='', + help=_('Filter migrations by type. Valid values are: evacuation, ' + 'live-migration, migration (cold), resize')) +@utils.arg( + '--source-compute', + dest='source_compute', + metavar='', + help=_('Filter migrations by source compute host name.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last migration of the previous page; displays list of ' + 'migrations after "marker". Note that the marker is the ' + 'migration UUID.')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_('Maximum number of migrations to display. Note that there is a ' + 'configurable max limit on the server, and the limit that is used ' + 'will be the minimum of what is requested here and what ' + 'is configured in the server.')) +@utils.arg( + '--changes-since', + dest='changes_since', + metavar='', + default=None, + help=_('List only migrations changed later or equal to a certain point ' + 'of time. The provided time should be an ISO 8061 formatted time. ' + 'e.g. 2016-03-04T06:27:59Z .')) +@utils.arg( + '--changes-before', + dest='changes_before', + metavar='', + default=None, + help=_('List only migrations changed earlier or equal to a certain point ' + 'of time. The provided time should be an ISO 8061 formatted time. ' + 'e.g. 2016-03-04T06:27:59Z .'), + start_version="2.66") +@utils.arg( + '--project-id', + dest='project_id', + metavar='', + default=None, + help=_('Filter the migrations by the given project ID.'), + start_version='2.80') +@utils.arg( + '--user-id', + dest='user_id', + metavar='', + default=None, + help=_('Filter the migrations by the given user ID.'), + start_version='2.80') def do_migration_list(cs, args): """Print a list of migrations.""" - migrations = cs.migrations.list(args.host, args.status, None, - instance_uuid=args.instance_uuid) + if args.changes_since: + try: + timeutils.parse_isotime(args.changes_since) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-since value: %s') + % args.changes_since) + + if args.changes_before: + try: + timeutils.parse_isotime(args.changes_before) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-before value: %s') + % args.changes_before) + + kwargs = dict( + instance_uuid=args.instance_uuid, + marker=args.marker, + limit=args.limit, + changes_since=args.changes_since, + changes_before=args.changes_before, + migration_type=args.migration_type, + source_compute=args.source_compute) + + if cs.api_version >= api_versions.APIVersion('2.80'): + kwargs['project_id'] = args.project_id + kwargs['user_id'] = args.user_id + + migrations = cs.migrations.list(args.host, args.status, **kwargs) _print_migrations(cs, migrations) + + +@utils.arg( + '--before', + dest='before', + metavar='', + default=None, + help=_("Filters the response by the date and time before which to list " + "usage audits. The date and time stamp format is as follows: " + "CCYY-MM-DD hh:mm:ss.NNNNNN ex 2015-08-27 09:49:58 or " + "2015-08-27 09:49:58.123456.")) +def do_instance_usage_audit_log(cs, args): + """List/Get server usage audits.""" + audit_log = cs.instance_usage_audit_log.get(before=args.before).to_dict() + if 'hosts_not_run' in audit_log: + audit_log['hosts_not_run'] = pprint.pformat(audit_log['hosts_not_run']) + utils.print_dict(audit_log) diff --git a/novaclient/v2/usage.py b/novaclient/v2/usage.py index abbeca692..32fc55085 100644 --- a/novaclient/v2/usage.py +++ b/novaclient/v2/usage.py @@ -87,7 +87,9 @@ def list(self, start, end, detailed=False, marker=None, limit=None): later in the instance list than that represented by this instance UUID (optional). :param limit: Maximum number of instances to include in the usage - (optional). + (optional). Note the API server has a configurable + default limit. If no limit is specified here or limit + is larger than default, the default limit will be used. :rtype: list of :class:`Usage`. """ query_string = self._usage_query(start, end, marker, limit, detailed) @@ -120,7 +122,9 @@ def get(self, tenant_id, start, end, marker=None, limit=None): later in the instance list than that represented by this instance UUID (optional). :param limit: Maximum number of instances to include in the usage - (optional). + (optional). Note the API server has a configurable + default limit. If no limit is specified here or limit + is larger than default, the default limit will be used. :rtype: :class:`Usage` """ query_string = self._usage_query(start, end, marker, limit) diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py index dd157d9f2..b9895f911 100644 --- a/novaclient/v2/versions.py +++ b/novaclient/v2/versions.py @@ -16,7 +16,7 @@ version interface """ -from six.moves import urllib +from urllib import parse from novaclient import base from novaclient import exceptions as exc @@ -79,7 +79,7 @@ def list(self): """List all versions.""" endpoint = self.api.client.get_endpoint() - url = urllib.parse.urlparse(endpoint) + url = parse.urlparse(endpoint) # NOTE(andreykurilin): endpoint URL has at least 3 formats: # 1. the classic (legacy) endpoint: # http://{host}:{optional_port}/v{2 or 2.1}/{project-id} diff --git a/novaclient/v2/virtual_interfaces.py b/novaclient/v2/virtual_interfaces.py deleted file mode 100644 index caf2e3a0e..000000000 --- a/novaclient/v2/virtual_interfaces.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -DEPRECATED Virtual Interfaces -""" - -import warnings - -from novaclient import api_versions -from novaclient import base -from novaclient.i18n import _ - - -class VirtualInterface(base.Resource): - def __repr__(self): - return "" - - -class VirtualInterfaceManager(base.ManagerWithFind): - """DEPRECATED""" - resource_class = VirtualInterface - - @api_versions.wraps('2.0', '2.43') - def list(self, instance_id): - """DEPRECATED""" - warnings.warn(_('The os-virtual-interfaces API is deprecated. This ' - 'API binding will be removed in the first major ' - 'release after the Nova server 16.0.0 Pike release.'), - DeprecationWarning) - return self._list('/servers/%s/os-virtual-interfaces' % instance_id, - 'virtual_interfaces') diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py index d6208cbd1..93fdd8682 100644 --- a/novaclient/v2/volumes.py +++ b/novaclient/v2/volumes.py @@ -39,6 +39,20 @@ class VolumeManager(base.Manager): """ resource_class = Volume + @staticmethod + def _get_request_body_for_create(volume_id, device=None, tag=None, + delete_on_termination=False): + body = {'volumeAttachment': {'volumeId': volume_id}} + if device is not None: + body['volumeAttachment']['device'] = device + if tag is not None: + body['volumeAttachment']['tag'] = tag + if delete_on_termination: + body['volumeAttachment']['delete_on_termination'] = ( + delete_on_termination) + + return body + @api_versions.wraps("2.0", "2.48") def create_server_volume(self, server_id, volume_id, device=None): """ @@ -49,13 +63,12 @@ def create_server_volume(self, server_id, volume_id, device=None): :param device: The device name (optional) :rtype: :class:`Volume` """ - body = {'volumeAttachment': {'volumeId': volume_id}} - if device is not None: - body['volumeAttachment']['device'] = device - return self._create("/servers/%s/os-volume_attachments" % server_id, - body, "volumeAttachment") + return self._create( + "/servers/%s/os-volume_attachments" % server_id, + VolumeManager._get_request_body_for_create(volume_id, device), + "volumeAttachment") - @api_versions.wraps("2.49") + @api_versions.wraps("2.49", "2.78") def create_server_volume(self, server_id, volume_id, device=None, tag=None): """ @@ -67,14 +80,33 @@ def create_server_volume(self, server_id, volume_id, device=None, :param tag: The tag (optional) :rtype: :class:`Volume` """ - body = {'volumeAttachment': {'volumeId': volume_id}} - if device is not None: - body['volumeAttachment']['device'] = device - if tag is not None: - body['volumeAttachment']['tag'] = tag - return self._create("/servers/%s/os-volume_attachments" % server_id, - body, "volumeAttachment") + return self._create( + "/servers/%s/os-volume_attachments" % server_id, + VolumeManager._get_request_body_for_create(volume_id, device, tag), + "volumeAttachment") + + @api_versions.wraps("2.79") + def create_server_volume(self, server_id, volume_id, device=None, + tag=None, delete_on_termination=False): + """ + Attach a volume identified by the volume ID to the given server ID + + :param server_id: The ID of the server. + :param volume_id: The ID of the volume to attach. + :param device: The device name (optional). + :param tag: The tag (optional). + :param delete_on_termination: Marked whether to delete the attached + volume when the server is deleted + (optional). + :rtype: :class:`Volume` + """ + return self._create( + "/servers/%s/os-volume_attachments" % server_id, + VolumeManager._get_request_body_for_create(volume_id, device, tag, + delete_on_termination), + "volumeAttachment") + @api_versions.wraps("2.0", "2.84") def update_server_volume(self, server_id, src_volid, dest_volid): """ Swaps the existing volume attachment to point to a new volume. @@ -96,6 +128,35 @@ def update_server_volume(self, server_id, src_volid, dest_volid): (server_id, src_volid,), body, "volumeAttachment") + @api_versions.wraps("2.85") + def update_server_volume(self, server_id, src_volid, dest_volid, + delete_on_termination=None): + """ + Swaps the existing volume attachment to point to a new volume. + + Takes a server, a source (attached) volume and a destination volume and + performs a hypervisor assisted data migration from src to dest volume, + detaches the original (source) volume and attaches the new destination + volume. Note that not all backing hypervisor drivers support this + operation and it may be disabled via policy. + + + :param server_id: The ID of the server + :param source_volume: The ID of the src volume + :param dest_volume: The ID of the destination volume + :param delete_on_termination: Marked whether to delete the attached + volume when the server is deleted + (optional). + :rtype: :class:`Volume` + """ + body = {'volumeAttachment': {'volumeId': dest_volid}} + if delete_on_termination is not None: + body['volumeAttachment']['delete_on_termination'] = ( + delete_on_termination) + return self._update("/servers/%s/os-volume_attachments/%s" % + (server_id, src_volid), + body, "volumeAttachment") + def get_server_volume(self, server_id, volume_id=None, attachment_id=None): """ Get the volume identified by the volume ID, that is attached to diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..4f536f6aa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["pbr>=5.7.0", "setuptools>=64.0.0", "wheel"] +build-backend = "pbr.build" diff --git a/releasenotes/notes/add-filter-to-nova-list-831dcbb34420fb29.yaml b/releasenotes/notes/add-filter-to-nova-list-831dcbb34420fb29.yaml new file mode 100644 index 000000000..77acb7573 --- /dev/null +++ b/releasenotes/notes/add-filter-to-nova-list-831dcbb34420fb29.yaml @@ -0,0 +1,19 @@ +--- + features: + - | + Added the following filters support for the ``nova list`` command, + these filters are admin-only restricted until microversion 2.82: + + * --availability-zone + * --config-drive + * --no-config-drive + * --key-name + * --power-state + * --task-state + * --vm-state + * --progress + + Existing user filter will be available to non admin since + `microversion 2.83`_. + + .. _microversion 2.83: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id76 diff --git a/releasenotes/notes/add-support-for-volume-backed-rebuild-6a32d9d88fed6b4a.yaml b/releasenotes/notes/add-support-for-volume-backed-rebuild-6a32d9d88fed6b4a.yaml new file mode 100644 index 000000000..b9e2054e8 --- /dev/null +++ b/releasenotes/notes/add-support-for-volume-backed-rebuild-6a32d9d88fed6b4a.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added support for `microversion 2.93`_. + This microversion provides the ability to rebuild a volume + backed instance. + + .. _microversion 2.93: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-93 diff --git a/releasenotes/notes/bp-add-locked-reason-3f136db97b820c73.yaml b/releasenotes/notes/bp-add-locked-reason-3f136db97b820c73.yaml new file mode 100644 index 000000000..60e19bfcf --- /dev/null +++ b/releasenotes/notes/bp-add-locked-reason-3f136db97b820c73.yaml @@ -0,0 +1,6 @@ +--- +features: + - Added a ``--reason`` option to ``nova lock`` command that enables users + to specify a reason when locking a server and a ``locked`` + filtering/sorting option to ``nova list`` command which enables users to + filter/sort servers based on their ``locked`` value in microversion 2.73. diff --git a/releasenotes/notes/bp-cold-migration-with-target-queens-e361d4ae977aa396.yaml b/releasenotes/notes/bp-cold-migration-with-target-queens-e361d4ae977aa396.yaml new file mode 100644 index 000000000..00317ec6a --- /dev/null +++ b/releasenotes/notes/bp-cold-migration-with-target-queens-e361d4ae977aa396.yaml @@ -0,0 +1,8 @@ +--- +features: + - Added a new ``--host`` option to ``nova migrate`` command + in microversion 2.56. It enables administrators to specify + a target host when cold migating a server. The target host will be + validated by the scheduler. The target host cannot be the same as + the current host on which the server is running and must be in the + same cell that the server is currently in. diff --git a/releasenotes/notes/bp-handling-down-cell-728cdb1efd1ea75b.yaml b/releasenotes/notes/bp-handling-down-cell-728cdb1efd1ea75b.yaml new file mode 100644 index 000000000..0403d80f3 --- /dev/null +++ b/releasenotes/notes/bp-handling-down-cell-728cdb1efd1ea75b.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + From microversion 2.69 the results of ``nova list``, ``nova show`` and + ``nova service-list`` may contain missing information in their outputs + when there are partial infrastructure failure periods in the deployment. + See `Handling Down Cells`_ for more information on the missing keys/info. + + .. _Handling Down Cells: https://developer.openstack.org/api-guide/compute/down_cells.html diff --git a/releasenotes/notes/bp-keypair-generation-removal-1b5d84a8906d3918.yaml b/releasenotes/notes/bp-keypair-generation-removal-1b5d84a8906d3918.yaml new file mode 100644 index 000000000..c7cdb6424 --- /dev/null +++ b/releasenotes/notes/bp-keypair-generation-removal-1b5d84a8906d3918.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Support has been added for `microversion 2.92`_. This microversion only + accepts to import a public key and no longer to generate one, hence now the + public_key parameter be mandatory. + + .. _microversion 2.92: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-92 diff --git a/releasenotes/notes/bp-more-migration-list-filters-6c801896c7ee5cdc.yaml b/releasenotes/notes/bp-more-migration-list-filters-6c801896c7ee5cdc.yaml new file mode 100644 index 000000000..14e028d38 --- /dev/null +++ b/releasenotes/notes/bp-more-migration-list-filters-6c801896c7ee5cdc.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + The ``--migration-type`` and ``--source-compute`` options are added to the + ``nova migration-list`` CLI and related kwargs are added to the + ``novaclient.v2.migrations.MigrationManager.list`` method. These can be + used to filter the list of migrations by type (evacuation, live-migration, + migration, resize) and the name of the source compute service host involved + in the migration. diff --git a/releasenotes/notes/bp-unshelve-to-host-b220131a00dff8a2.yaml b/releasenotes/notes/bp-unshelve-to-host-b220131a00dff8a2.yaml new file mode 100644 index 000000000..98fe2c9f9 --- /dev/null +++ b/releasenotes/notes/bp-unshelve-to-host-b220131a00dff8a2.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Support has been added for `microversion 2.91`_. This microversion + allows specifying a destination host to unshelve a shelve + offloaded server. And availability zone can be set to None to unpin + the availability zone of a server. + + .. _microversion 2.91: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-91 diff --git a/releasenotes/notes/bug-1669140-c21d045491201352.yaml b/releasenotes/notes/bug-1669140-c21d045491201352.yaml new file mode 100644 index 000000000..1c950c1f5 --- /dev/null +++ b/releasenotes/notes/bug-1669140-c21d045491201352.yaml @@ -0,0 +1,5 @@ +--- +issues: + - | + The ``nova show`` command will no longer output the ``user_data`` column. + This is traditionally binary data of limited value from a CLI perspective. diff --git a/releasenotes/notes/bug-1744118-0b064d7062117317.yaml b/releasenotes/notes/bug-1744118-0b064d7062117317.yaml new file mode 100644 index 000000000..3a9688c22 --- /dev/null +++ b/releasenotes/notes/bug-1744118-0b064d7062117317.yaml @@ -0,0 +1,15 @@ +--- +fixes: + - | + A fix is made for `bug 1744118`_ which adds the below missing CLI + arguments. + + * OS_PROJECT_DOMAIN_ID + + * OS_PROJECT_DOMAIN_NAME + + * OS_USER_DOMAIN_ID + + * OS_USER_DOMAIN_NAME + + .. _bug 1744118: https://bugs.launchpad.net/python-novaclient/+bug/1744118 \ No newline at end of file diff --git a/releasenotes/notes/bug-1764420-flavor-delete-output-7b80f73deee5a869.yaml b/releasenotes/notes/bug-1764420-flavor-delete-output-7b80f73deee5a869.yaml new file mode 100644 index 000000000..7d95eb391 --- /dev/null +++ b/releasenotes/notes/bug-1764420-flavor-delete-output-7b80f73deee5a869.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + The ``flavor-delete`` command no longer prints out the details of the + deleted flavor. On successful deletion, there is no output. diff --git a/releasenotes/notes/bug-1767287-cc28d60d9e59f9bd.yaml b/releasenotes/notes/bug-1767287-cc28d60d9e59f9bd.yaml new file mode 100644 index 000000000..76dce988b --- /dev/null +++ b/releasenotes/notes/bug-1767287-cc28d60d9e59f9bd.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The ``nova server-group-create`` command now only supports specifying + a single policy name when creating the server group. This is to match + the server-side API validation. diff --git a/releasenotes/notes/bug-1778536-a1b5d65a0d4ad622.yaml b/releasenotes/notes/bug-1778536-a1b5d65a0d4ad622.yaml new file mode 100644 index 000000000..3e5410aad --- /dev/null +++ b/releasenotes/notes/bug-1778536-a1b5d65a0d4ad622.yaml @@ -0,0 +1,12 @@ +--- +upgrade: + - The deprecated ``--bypass-url`` command line argument has been removed. +deprecations: + - | + The ``--endpoint-override`` command line argument has been deprecated. + It is renamed to ``--os-endpoint-override`` to avoid misinterpreting + command line arguments. + It defaults to the ``OS_ENDPOINT_OVERRIDE`` environment variable. + See `bug 1778536`_ for more details. + + .. _bug 1778536: https://bugs.launchpad.net/python-novaclient/+bug/1778536 diff --git a/releasenotes/notes/bug-1825061-2beb95db4d6df0cb.yaml b/releasenotes/notes/bug-1825061-2beb95db4d6df0cb.yaml new file mode 100644 index 000000000..573ad7fc7 --- /dev/null +++ b/releasenotes/notes/bug-1825061-2beb95db4d6df0cb.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + A check for a value of the '--config-drive' option has been added on the + ``nova boot`` command. A boolean value is only allowed in the option now. diff --git a/releasenotes/notes/bug-1845322-463ee407b60131c9.yaml b/releasenotes/notes/bug-1845322-463ee407b60131c9.yaml new file mode 100644 index 000000000..9c60e72fb --- /dev/null +++ b/releasenotes/notes/bug-1845322-463ee407b60131c9.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The ``--hint`` option for the ``boot`` command expects a key-value + argument. Previously, if this was not the case, the argument would be + silently ignored. It will now raise an error. diff --git a/releasenotes/notes/deprecate-agent-d0f58718ad1782f6.yaml b/releasenotes/notes/deprecate-agent-d0f58718ad1782f6.yaml new file mode 100644 index 000000000..c43d92cec --- /dev/null +++ b/releasenotes/notes/deprecate-agent-d0f58718ad1782f6.yaml @@ -0,0 +1,12 @@ +--- +deprecations: + - | + The following CLIs are deprecated. + + - ``nova agent-create`` + - ``nova agent-delete`` + - ``nova agent-list`` + - ``nova agent-modify`` + + The CLIs will be removed in the first major release after Nova 24.0.0 X + is released. diff --git a/releasenotes/notes/deprecate-cellsv1-extension-16482759993d112f.yaml b/releasenotes/notes/deprecate-cellsv1-extension-16482759993d112f.yaml new file mode 100644 index 000000000..a26ad849e --- /dev/null +++ b/releasenotes/notes/deprecate-cellsv1-extension-16482759993d112f.yaml @@ -0,0 +1,11 @@ +--- +deprecations: + - | + The following CLIs and their backing API bindings are deprecated. + + - ``nova list-extensions`` + - ``nova cell-capacities`` + - ``nova cell-show`` + + The CLIs and API bindings will be removed in the first major release after + Nova 20.0.0 Train is released. diff --git a/releasenotes/notes/deprecate-cli-75074850847a8452.yaml b/releasenotes/notes/deprecate-cli-75074850847a8452.yaml new file mode 100644 index 000000000..6d51ce03c --- /dev/null +++ b/releasenotes/notes/deprecate-cli-75074850847a8452.yaml @@ -0,0 +1,9 @@ +--- +deprecations: + - | + The ``nova`` CLI is now deprecated. This is the signal that it is + time to start using the openstack CLI. No new features will be + added to the ``nova`` CLI, though fixes to the CLI will be assessed + on a case by case basis. Fixes to the API bindings, development of + new API bindings, and changes to the compute commands in the openstack + CLI are exempt from this deprecation. diff --git a/releasenotes/notes/deprecate-force-option-7116d792bba17f09.yaml b/releasenotes/notes/deprecate-force-option-7116d792bba17f09.yaml new file mode 100644 index 000000000..89e5ffed6 --- /dev/null +++ b/releasenotes/notes/deprecate-force-option-7116d792bba17f09.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + Added support for `microversion 2.68`_, which removes the ``--force`` option + from the ``nova evacuate``, ``nova live-migration``, ``nova host-evacuate`` + and ``nova host-evacuate-live`` commands. + + .. _microversion 2.68: https://docs.openstack.org/nova/latest/api_microversion_history.html#id61 diff --git a/releasenotes/notes/deprecate-instance-name-option-bc76629d28f1d456.yaml b/releasenotes/notes/deprecate-instance-name-option-bc76629d28f1d456.yaml new file mode 100644 index 000000000..270d8dcfe --- /dev/null +++ b/releasenotes/notes/deprecate-instance-name-option-bc76629d28f1d456.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - | + The ``--instance-name`` option has been deprecated from the ``nova list`` + command because the instance name query parameter is ignored by the + compute REST API. + diff --git a/releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml b/releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml new file mode 100644 index 000000000..9fd7dee19 --- /dev/null +++ b/releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + Python 2 is no longer supported. Python 3 is required. diff --git a/releasenotes/notes/fix-raw-python-error-debd3edb17c2f675.yaml b/releasenotes/notes/fix-raw-python-error-debd3edb17c2f675.yaml new file mode 100644 index 000000000..7686ecbeb --- /dev/null +++ b/releasenotes/notes/fix-raw-python-error-debd3edb17c2f675.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + `Bug #1903727 `_: + Fixed raw Python error message when using ``nova`` without + a subcommand while passing an optional argument, such as + ``--os-compute-api-version 2.87``. diff --git a/releasenotes/notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml b/releasenotes/notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml new file mode 100644 index 000000000..32065a113 --- /dev/null +++ b/releasenotes/notes/fix-rebuild-userdata-9315e5784feb8ba9.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + The user data argument in the ``nova rebuild`` command was passing + the filename as userdata. Now this passes the contents of the file + as intended. diff --git a/releasenotes/notes/get-list-metadata-8afcc8f32ad82dda.yaml b/releasenotes/notes/get-list-metadata-8afcc8f32ad82dda.yaml new file mode 100644 index 000000000..c67cd18a6 --- /dev/null +++ b/releasenotes/notes/get-list-metadata-8afcc8f32ad82dda.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds supports for Nova Server get and list metadata API. diff --git a/releasenotes/notes/get-rid-off-redundant-methods-47e679c13e88f28a.yaml b/releasenotes/notes/get-rid-off-redundant-methods-47e679c13e88f28a.yaml new file mode 100644 index 000000000..954ae395d --- /dev/null +++ b/releasenotes/notes/get-rid-off-redundant-methods-47e679c13e88f28a.yaml @@ -0,0 +1,10 @@ +--- +deprecations: + - | + ``novaclient.utils.add_resource_manager_extra_kwargs_hook`` and + ``novaclient.utils.get_resource_manager_extra_kwargs`` were designed for + supporting extensions in nova/novaclient. Nowadays, this "extensions" + feature is abandoned and both ``add_resource_manager_extra_kwargs_hook``, + ``add_resource_manager_extra_kwargs_hook`` are not used in novaclient's + code. These methods are not documented, so we are removing them without + standard deprecation cycle. diff --git a/releasenotes/notes/interface-attach-output-02d633d9b2a60da1.yaml b/releasenotes/notes/interface-attach-output-02d633d9b2a60da1.yaml new file mode 100644 index 000000000..af855a1ea --- /dev/null +++ b/releasenotes/notes/interface-attach-output-02d633d9b2a60da1.yaml @@ -0,0 +1,11 @@ +--- +upgrade: + - The ``nova interface-attach`` command shows output of its result + when it is successful. + - | + The following methods return a ``NetworkInterface`` object + instead of a ``Server`` object. + + * The ``interface_attach`` method in the ``novaclient.v2.Server`` class + * The ``interface_attach`` method in the ``novaclient.v2.ServerManager`` + class diff --git a/releasenotes/notes/microversion-v2_54-6c7ccb61eff6cb6d.yaml b/releasenotes/notes/microversion-v2_54-6c7ccb61eff6cb6d.yaml new file mode 100644 index 000000000..5321c486f --- /dev/null +++ b/releasenotes/notes/microversion-v2_54-6c7ccb61eff6cb6d.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds support for microversion 2.54 which adds resetting keypair and + unsetting keypair in rebuild operation. + Adds optional ``--key-name`` and ``--key-unset`` options + in the ``nova rebuild`` command. diff --git a/releasenotes/notes/microversion-v2_55-flavor-description-a93718b31f1f0f39.yaml b/releasenotes/notes/microversion-v2_55-flavor-description-a93718b31f1f0f39.yaml new file mode 100644 index 000000000..9c3e6c00d --- /dev/null +++ b/releasenotes/notes/microversion-v2_55-flavor-description-a93718b31f1f0f39.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Support is added for compute API version 2.55. This adds the ability + to create a flavor with a description, show the description of a flavor, + and update the description on an existing flavor. + + A new ``nova flavor-update `` command is added. diff --git a/releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml b/releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml new file mode 100644 index 000000000..4625d506e --- /dev/null +++ b/releasenotes/notes/microversion-v2_57-acae2ee11ddae4fb.yaml @@ -0,0 +1,31 @@ +--- +features: + - | + Support is added for the 2.57 microversion: + + * A ``userdata`` keyword argument can be passed to the ``Server.rebuild`` + python API binding. If set to None, it will unset any existing userdata + on the server. + * The ``--user-data`` and ``--user-data-unset`` options are added to the + ``nova rebuild`` CLI. The options are mutually exclusive. Specifying + ``--user-data`` will overwrite the existing userdata in the server, and + ``--user-data-unset`` will unset any existing userdata on the server. +upgrade: + - | + Support is added for the 2.57 microversion: + + * The ``--file`` option for the ``nova boot`` and ``nova rebuild`` CLIs is + capped at the 2.56 microversion. Similarly, the ``file`` parameter to + the ``Server.create`` and ``Server.rebuild`` python API binding methods + is capped at 2.56. Users are recommended to use the ``--user-data`` + option instead. + * The ``--injected-files``, ``--injected-file-content-bytes`` and + ``--injected-file-path-bytes`` options are capped at the 2.56 + microversion in the ``nova quota-update`` and ``nova quota-class-update`` + commands. + * The ``maxPersonality`` and ``maxPersonalitySize`` fields are capped at + the 2.56 microversion in the ``nova limits`` command and API binding. + * The ``injected_files``, ``injected_file_content_bytes`` and + ``injected_file_path_bytes`` entries are capped at version 2.56 from + the output of the ``nova quota-show`` and ``nova quota-class-show`` + commands and related python API bindings. diff --git a/releasenotes/notes/microversion-v2_58-327c1031ebfe4a3a.yaml b/releasenotes/notes/microversion-v2_58-327c1031ebfe4a3a.yaml new file mode 100644 index 000000000..b2fb6a3da --- /dev/null +++ b/releasenotes/notes/microversion-v2_58-327c1031ebfe4a3a.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added support for microversion v2.58 which introduces pagination support + for instance actions with the help of new optional parameters ``limit``, + ``marker``, and also adds the new filter ``changes-since``. Users can use + ``changes-since`` filter to filter the results based on the last time the + instance action was updated. diff --git a/releasenotes/notes/microversion-v2_59-4160c852d7d8812d.yaml b/releasenotes/notes/microversion-v2_59-4160c852d7d8812d.yaml new file mode 100644 index 000000000..e90c87b81 --- /dev/null +++ b/releasenotes/notes/microversion-v2_59-4160c852d7d8812d.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added support for microversion v2.59 which introduces pagination support + for migrations with the help of new optional parameters ``limit``, + ``marker``, and also adds the new filter ``changes-since``. Users can use + ``changes-since`` filter to filter the results based on the last time the + migration was updated. diff --git a/releasenotes/notes/microversion-v2_61-9a8faa02fddf9ed6.yaml b/releasenotes/notes/microversion-v2_61-9a8faa02fddf9ed6.yaml new file mode 100644 index 000000000..a96b65d3a --- /dev/null +++ b/releasenotes/notes/microversion-v2_61-9a8faa02fddf9ed6.yaml @@ -0,0 +1,13 @@ +--- +other: + - | + Starting from microversion 2.61, the responses of the 'Flavor' APIs + include the 'extra_specs' parameter. Therefore 'Flavors extra-specs' + (os-extra_specs) API calls have been removed in the following commands + since microversion 2.61. + + * ``nova flavor-list`` + * ``nova flavor-show`` + + There are no behavior changes in the CLI. This is just a performance + optimization. diff --git a/releasenotes/notes/microversion-v2_62-479a23f0d4307500.yaml b/releasenotes/notes/microversion-v2_62-479a23f0d4307500.yaml new file mode 100644 index 000000000..ed2948b24 --- /dev/null +++ b/releasenotes/notes/microversion-v2_62-479a23f0d4307500.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Adds support for microversion 2.62 which adds ``host`` (hostname) + and ``hostId`` (an obfuscated hashed host id string) fields to the + instance action ``GET /servers/{server_id}/os-instance-actions/{req_id}`` + API. + + The event columns are already included in the result of + "nova instance-action " command, therefore does not + have any CLI or python API binding impacts in the client. diff --git a/releasenotes/notes/microversion-v2_63-cd058a9145550cae.yaml b/releasenotes/notes/microversion-v2_63-cd058a9145550cae.yaml new file mode 100644 index 000000000..f8299656f --- /dev/null +++ b/releasenotes/notes/microversion-v2_63-cd058a9145550cae.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + Added support for `microversion 2.63`_, which includes the following + changes: + + - New environment variable called ``OS_TRUSTED_IMAGE_CERTIFICATE_IDS`` + - New ``nova boot`` option called ``--trusted-image-certificate-id`` + - New ``nova rebuild`` options called ``--trusted-image-certificate-id`` + and ``--trusted-image-certificates-unset`` + - New kwarg called ``trusted_image_certificates`` added to python API + bindings: + + - ``novaclient.v2.servers.ServerManager.create()`` + - ``novaclient.v2.servers.ServerManager.rebuild()`` + + .. _microversion 2.63: https://docs.openstack.org/nova/latest/api_microversion_history.html#id57 diff --git a/releasenotes/notes/microversion-v2_64-66366829ec65bea4.yaml.yaml b/releasenotes/notes/microversion-v2_64-66366829ec65bea4.yaml.yaml new file mode 100644 index 000000000..0bd5d4a56 --- /dev/null +++ b/releasenotes/notes/microversion-v2_64-66366829ec65bea4.yaml.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Added support for `microversion 2.64`_, which includes the following + changes: + + * The ``--rule`` options is added to the ``nova server-group-create`` + CLI that enables user to create server group with specific policy rules. + * Remove ``metadata`` column in the output of ``nova server-group-create``, + ``nova server-group-get``, ``nova server-group-list``. + * Remove ``policies`` column, add ``policy`` and ``rules`` columns in + the output of ``nova server-group-create``, ``nova server-group-get``, + ``nova server-group-list``. + + .. _microversion 2.64: https://docs.openstack.org/nova/latest/api_microversion_history.html#id58 diff --git a/releasenotes/notes/microversion-v2_65-3c89c5932f4391cb.yaml b/releasenotes/notes/microversion-v2_65-3c89c5932f4391cb.yaml new file mode 100644 index 000000000..f581e3cf7 --- /dev/null +++ b/releasenotes/notes/microversion-v2_65-3c89c5932f4391cb.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Support has been added for the compute API `2.65`_ microversion. This + allows calling ``nova live-migration-abort`` on live migrations that are + in ``queued`` or ``preparing`` status in addition to the already accepted + ``running`` status. + + .. _2.65: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id59 diff --git a/releasenotes/notes/microversion-v2_66-cda5d6dc31b56b46.yaml b/releasenotes/notes/microversion-v2_66-cda5d6dc31b56b46.yaml new file mode 100644 index 000000000..69c649521 --- /dev/null +++ b/releasenotes/notes/microversion-v2_66-cda5d6dc31b56b46.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Added support for `microversion 2.66`_ which adds ``changes-before`` + parameter to the servers, os-instance-actions or os-migrations APIs. + + * This parameter (``changes-before``) does not change any read-deleted + behavior in the os-instance-actions or os-migrations APIs. + * Like the ``changes-since`` filter, the ``changes-before`` filter will + also return deleted servers. + * The ``--changes-before`` options is added to the ``nova list``, + ``nova instance-action-list`` and ``nova migration-list`` CLIs. + + .. _microversion 2.66: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id59 diff --git a/releasenotes/notes/microversion-v2_67-da6d9b12730b8562.yaml b/releasenotes/notes/microversion-v2_67-da6d9b12730b8562.yaml new file mode 100644 index 000000000..0c1e360a0 --- /dev/null +++ b/releasenotes/notes/microversion-v2_67-da6d9b12730b8562.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Support is added for the `2.67 microversion`_ which allows specifying + a ``volume_type`` with the ``--block-device`` option on the ``nova boot`` + command. The ``novaclient.v2.servers.ServerManager.create()`` method now + also supports a ``volume_type`` entry in the ``block_device_mapping_v2`` + parameter. + + .. _2.67 microversion: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id60 diff --git a/releasenotes/notes/microversion-v2_71-a87b4bb4205c46e2.yaml b/releasenotes/notes/microversion-v2_71-a87b4bb4205c46e2.yaml new file mode 100644 index 000000000..7a4bf9609 --- /dev/null +++ b/releasenotes/notes/microversion-v2_71-a87b4bb4205c46e2.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added support for `microversion 2.71`_ which outputs the `server_groups` + field in the following commands: + + * ``nova show`` + * ``nova rebuild`` + + .. _microversion 2.71: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id64 diff --git a/releasenotes/notes/microversion-v2_72-d910ce07ec3948d6.yaml b/releasenotes/notes/microversion-v2_72-d910ce07ec3948d6.yaml new file mode 100644 index 000000000..69ea49821 --- /dev/null +++ b/releasenotes/notes/microversion-v2_72-d910ce07ec3948d6.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Support has been added for `microversion 2.72`_. This microversion + allows creating a server using the ``nova boot`` command with + pre-existing ports having a ``resource_request`` value to enable + features such as guaranteed minimum bandwidth for `quality of service`_. + + .. _microversion 2.72: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id65 + .. _quality of service: https://docs.openstack.org/neutron/latest/admin/config-qos.html diff --git a/releasenotes/notes/microversion-v2_74-43b128fe6b84b630.yaml b/releasenotes/notes/microversion-v2_74-43b128fe6b84b630.yaml new file mode 100644 index 000000000..de98f8664 --- /dev/null +++ b/releasenotes/notes/microversion-v2_74-43b128fe6b84b630.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Support is added for the `2.74 microversion`_ which allows specifying the + ``--host`` and ``--hypervisor-hostname`` options on the ``nova boot`` + command. The ``novaclient.v2.servers.ServerManager.create()`` method now + also supports ``host`` and ``hypervisor_hostname`` parameters. + + .. _2.74 microversion: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id66 diff --git a/releasenotes/notes/microversion-v2_75-ea7fa3ba1396edea.yaml b/releasenotes/notes/microversion-v2_75-ea7fa3ba1396edea.yaml new file mode 100644 index 000000000..e1993f9dd --- /dev/null +++ b/releasenotes/notes/microversion-v2_75-ea7fa3ba1396edea.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Added support for `microversion 2.75`_. The following changes were made: + + - Return all fields of ``server`` in ``nova rebuild`` command which are + returned in ``nova show``. Both command will return the same set of + fields of ``server`` representation. + + - Default return value of ``swap`` field will be 0 (integer) in below + commands: + + - ``nova flavor-list`` + - ``nova flavor-show`` + - ``nova flavor-create`` + - ``nova flavor-update`` + + .. _microversion 2.75: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id67 diff --git a/releasenotes/notes/microversion-v2_77-ffee30c180aa4dbe.yaml b/releasenotes/notes/microversion-v2_77-ffee30c180aa4dbe.yaml new file mode 100644 index 000000000..6262f2207 --- /dev/null +++ b/releasenotes/notes/microversion-v2_77-ffee30c180aa4dbe.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Support has been added for `microversion 2.77`_. This microversion + allows specifying an availability zone to unshelve a shelve + offloaded server. + + .. _microversion 2.77: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id69 diff --git a/releasenotes/notes/microversion-v2_78-77a12630e668c2ae.yaml b/releasenotes/notes/microversion-v2_78-77a12630e668c2ae.yaml new file mode 100644 index 000000000..17e6289ec --- /dev/null +++ b/releasenotes/notes/microversion-v2_78-77a12630e668c2ae.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Added support for `microversion 2.78`_ which outputs the server NUMA + topology information in the following command: + + * ``nova server-topology`` + + And associated python API bindings: + + * ``novaclient.v2.servers.Server.topology`` + * ``novaclient.v2.servers.ServerManager.topology`` + + .. _microversion 2.78: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id70 diff --git a/releasenotes/notes/microversion-v2_79-f13bc0414743dc16.yaml b/releasenotes/notes/microversion-v2_79-f13bc0414743dc16.yaml new file mode 100644 index 000000000..ca81f58de --- /dev/null +++ b/releasenotes/notes/microversion-v2_79-f13bc0414743dc16.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Added support for `microversion 2.79`_ which includes the following + changes: + + - The ``--delete-on-termination`` option is added to the + ``nova volume-attach`` CLI. + - A ``DELETE ON TERMINATION`` column is added to the + ``nova volume-attachments`` table. + - New kwarg called ``delete_on_termination`` added to the python API + binding: + + - ``novaclient.v2.volumes.VolumeManager.create_server_volume()`` + + .. _microversion 2.79: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id71 diff --git a/releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml b/releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml new file mode 100644 index 000000000..cace47d18 --- /dev/null +++ b/releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + Added support for `microversion 2.80`_ which adds ``user_id`` + and ``project_id`` filter parameters to the ``GET /os-migrations`` API. + + New kwargs ``project_id`` and ``user_id`` have been added to + the following python API binding: + + - novaclient.v2.migrations.MigrationManager.list + + The following CLI changes have been made: + + - The ``--project-id`` and ``--user-id`` options are added to the + ``nova migration-list`` CLI. + - The ``nova server-migration-list`` and ``nova server-migration-show`` + commands will show the ``Project ID`` and ``User ID`` values when + using microversion 2.80 or greater. + + .. _microversion 2.80: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id72 diff --git a/releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml b/releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml new file mode 100644 index 000000000..a51336d60 --- /dev/null +++ b/releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added support for `microversion 2.81`_ which adds image pre-caching support by + aggregate. + + - The ``aggregate-cache-images`` command is added to the CLI + - The ``cache_images()`` method is added to the python API binding + + .. _microversion 2.81: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id73 diff --git a/releasenotes/notes/microversion-v2_85-230931f88c4f1d52.yaml b/releasenotes/notes/microversion-v2_85-230931f88c4f1d52.yaml new file mode 100644 index 000000000..859534c4b --- /dev/null +++ b/releasenotes/notes/microversion-v2_85-230931f88c4f1d52.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Support is added for compute API `microversion 2.85`_. This adds the + ability to update an attached volume with a ``delete_on_termination``, + which specify if the attached volume should be deleted when the server + is destroyed. + + - The ``--delete-on-termination`` and ``--no-delete-on-termination`` + options are added to the ``nova volume-update`` CLI. + - New kwarg called ``delete_on_termination`` added to the python API + binding: + + - ``novaclient.v2.volumes.VolumeManager.update_server_volume()`` + + .. _microversion 2.85: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id78 diff --git a/releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml b/releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml new file mode 100644 index 000000000..6d8666373 --- /dev/null +++ b/releasenotes/notes/microversion-v2_88-d91136020e3a3621.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + Added support for `microversion 2.88`_. The + ``novaclient.v2.hypervisors.HypervisorManager.uptime`` method will now + transparently switch between the ``/os-hypervisors/{id}/uptime`` API, + which is deprecated in 2.88, and the ``/os-hypervisors/{id}`` API, which + now includes uptime information, based on the microversion. + + .. _microversion 2.88: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-88 +deprecations: + - | + The ``nova hypervisor-stats`` command and underlying + ``novaclient.v2.hypervisors.HypervisorStatsManager.statistics`` API are + deprecated starting in microversion 2.88 and will return an error starting + on this version. diff --git a/releasenotes/notes/microversion-v2_90-259779668e67dfb5.yaml b/releasenotes/notes/microversion-v2_90-259779668e67dfb5.yaml new file mode 100644 index 000000000..8ab21c91d --- /dev/null +++ b/releasenotes/notes/microversion-v2_90-259779668e67dfb5.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Added support for `microversion 2.90`_. This microversion provides the + ability to manually configure the instance ``hostname`` attribute when + creating a new instance (``nova boot --hostname HOSTNAME ...``), updating + an existing instance (``nova update --hostname HOSTNAME ...``), or + rebuilding an existing instance (``nova rebuild --hostname HOSTNAME``). + This attribute is published via the metadata service and config drive and + can be used by init scripts such as ``cloud-init`` to configure the guest's + hostname. + + .. _microversion 2.90: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-90 diff --git a/releasenotes/notes/microversion-v2_94-5368d5dd7c5f6484.yaml b/releasenotes/notes/microversion-v2_94-5368d5dd7c5f6484.yaml new file mode 100644 index 000000000..587969dd7 --- /dev/null +++ b/releasenotes/notes/microversion-v2_94-5368d5dd7c5f6484.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Added support for `microversion 2.94`_. There are no client-side changes + for this microversion, but sending this microversion allows the + ``hostname`` parameter in the server create, server update, and server + rebuild APIs to be a fully qualified domain name (FQDN). Prior to this + microversion, server-side validation only allows short names as the + ``hostname``. + + .. _microversion 2.94: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id83 diff --git a/releasenotes/notes/microversion-v2_95-3c6ad46be2656684.yaml b/releasenotes/notes/microversion-v2_95-3c6ad46be2656684.yaml new file mode 100644 index 000000000..3452f926f --- /dev/null +++ b/releasenotes/notes/microversion-v2_95-3c6ad46be2656684.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added support for `microversion 2.95`_. There are no client-side changes + for this microversion, but sending this microversion triggers evacuated + instances to be stopped on the destination host. Prior to this + microversion, instances were keeping their state from source to + destination host. + + .. _microversion 2.95: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id84 diff --git a/releasenotes/notes/microversion-v2_96-a50af976133de0ab.yaml b/releasenotes/notes/microversion-v2_96-a50af976133de0ab.yaml new file mode 100644 index 000000000..510548720 --- /dev/null +++ b/releasenotes/notes/microversion-v2_96-a50af976133de0ab.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The `microversion 2.96`_ has been added. This microversion adds + pinned_availability_zone in ``server show`` and + ``server list --long`` responses. + + .. _microversion 2.96: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-96 diff --git a/releasenotes/notes/microversion_v2_70-09cbe0933b3a9335.yaml b/releasenotes/notes/microversion_v2_70-09cbe0933b3a9335.yaml new file mode 100644 index 000000000..93dee6e2d --- /dev/null +++ b/releasenotes/notes/microversion_v2_70-09cbe0933b3a9335.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Added support for `microversion 2.70`_ which outputs the `Tag` field in + the following commands: + + * ``nova interface-list`` + * ``nova interface-attach`` + * ``nova volume-attachments`` + * ``nova volume-attach`` + + .. _microversion 2.70: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id63 diff --git a/releasenotes/notes/microversion_v2_89-af6223273b2bdfb0.yaml b/releasenotes/notes/microversion_v2_89-af6223273b2bdfb0.yaml new file mode 100644 index 000000000..39682f59b --- /dev/null +++ b/releasenotes/notes/microversion_v2_89-af6223273b2bdfb0.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Added support for `microversion 2.89`_. This microversion removes the + ``id`` field while adding the ``attachment_id`` and ``bdm_uuid`` fields to + the responses of ``GET /servers/{server_id}/os-volume_attachments`` and + ``GET /servers/{server_id}/os-volume_attachments/{volume_id}`` with these + changes reflected in novaclient under the ``nova volume-attachments`` + command. + + .. _microversion 2.89: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#microversion-2-89 diff --git a/releasenotes/notes/remove-certs-4333342189200d91.yaml b/releasenotes/notes/remove-certs-4333342189200d91.yaml new file mode 100644 index 000000000..75b0f680e --- /dev/null +++ b/releasenotes/notes/remove-certs-4333342189200d91.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The ``nova x509-create-cert`` and ``nova x509-get-root-cert`` commands + and ``novaclient.v2.certs`` API binding were deprecated in the 9.0.0 + release and have now been removed. diff --git a/releasenotes/notes/remove-cloudpipe-6c790c57dc3796eb.yaml b/releasenotes/notes/remove-cloudpipe-6c790c57dc3796eb.yaml new file mode 100644 index 000000000..4ee7e8136 --- /dev/null +++ b/releasenotes/notes/remove-cloudpipe-6c790c57dc3796eb.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The deprecated ``nova cloudpipe-list``, ``nova cloudpipe-create``, and + ``nova cloudpipe-configure`` commands and the ``novaclient.v2.cloudpipe`` + API bindings have been removed. diff --git a/releasenotes/notes/remove-contrib-8b5e35ac8dddbab3.yaml b/releasenotes/notes/remove-contrib-8b5e35ac8dddbab3.yaml new file mode 100644 index 000000000..413a283d2 --- /dev/null +++ b/releasenotes/notes/remove-contrib-8b5e35ac8dddbab3.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - All modules of ``novaclient.v2.contrib`` have been removed. + - The ``only_contrib`` parameter for the + ``novaclient.client.discover_extensions`` method is no longer valid. diff --git a/releasenotes/notes/remove-deprecated-cellsv1-extentions-commands-4b26c826ad5194ca.yaml b/releasenotes/notes/remove-deprecated-cellsv1-extentions-commands-4b26c826ad5194ca.yaml new file mode 100644 index 000000000..eb452452e --- /dev/null +++ b/releasenotes/notes/remove-deprecated-cellsv1-extentions-commands-4b26c826ad5194ca.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + The following CLIs and their backing API bindings have been removed. + + - ``nova list-extensions`` + - ``nova cell-capacities`` + - ``nova cell-show`` diff --git a/releasenotes/notes/remove-deprecated-methods-train-c450fe317c90d7f0.yaml b/releasenotes/notes/remove-deprecated-methods-train-c450fe317c90d7f0.yaml new file mode 100644 index 000000000..b8a1d5311 --- /dev/null +++ b/releasenotes/notes/remove-deprecated-methods-train-c450fe317c90d7f0.yaml @@ -0,0 +1,24 @@ +--- +upgrade: + - | + The following properties have been removed. + + - ``novaclient.client.SessionClient`` + + - ``management_url`` + + - ``novaclient.v2.client.Client`` + + - ``projectid`` + - ``tenant_id`` + + The following methods have been removed. + + - ``novaclient.client.get_client_class`` + - ``novaclient.v2.client.Client`` + + - ``set_management_url`` + - ``authenticate`` + + The ``novaclient.v2.client.Client.__enter__`` method now raises + the ``InvalidUsage`` runtime error. diff --git a/releasenotes/notes/remove-deprecated-option-14.0.0-c6d7189938f5f063.yaml b/releasenotes/notes/remove-deprecated-option-14.0.0-c6d7189938f5f063.yaml new file mode 100644 index 000000000..b91ff58fd --- /dev/null +++ b/releasenotes/notes/remove-deprecated-option-14.0.0-c6d7189938f5f063.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The following deprecated options have been removed. + + * ``--endpoint-override`` (Authentication option) + * ``--instance-name`` (``nova list`` command) diff --git a/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml b/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml index 89d5afabc..a98272c80 100644 --- a/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml +++ b/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml @@ -6,3 +6,6 @@ upgrade: - ``--tenant`` (from ``flavor access list``) - ``--cell_name`` (from ``migration list``) - ``--volume-service-name`` (global option) + + As a result, the ``novaclient.v2.migrations.MigrationManager.list`` + python API binding method no longer takes a ``cell_name`` kwarg. diff --git a/releasenotes/notes/remove-hosts-d08855550c40b9c6.yaml b/releasenotes/notes/remove-hosts-d08855550c40b9c6.yaml new file mode 100644 index 000000000..06f466d44 --- /dev/null +++ b/releasenotes/notes/remove-hosts-d08855550c40b9c6.yaml @@ -0,0 +1,13 @@ +--- +upgrade: + - | + The following CLIs and their backing API bindings were deprecated and + capped at microversion 2.43: + + * ``nova host-describe`` - superseded by ``nova hypervisor-show`` + * ``nova host-list`` - superseded by ``nova hypervisor-list`` + * ``nova host-update`` - superseded by ``nova service-enable`` and + ``nova service-disable`` + * ``nova host-action`` - no alternative by design + + The CLIs and API bindings have now been removed. diff --git a/releasenotes/notes/remove-py38-ae196c568a1577db.yaml b/releasenotes/notes/remove-py38-ae196c568a1577db.yaml new file mode 100644 index 000000000..040316360 --- /dev/null +++ b/releasenotes/notes/remove-py38-ae196c568a1577db.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Support for Python 3.8 has been removed. Now the minimum python version + supported is 3.9 . diff --git a/releasenotes/notes/remove-service-binary-arg-ec2838214c8c7abc.yaml b/releasenotes/notes/remove-service-binary-arg-ec2838214c8c7abc.yaml new file mode 100644 index 000000000..2a136a23d --- /dev/null +++ b/releasenotes/notes/remove-service-binary-arg-ec2838214c8c7abc.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The deprecated ``binary`` argument to the ``nova service-enable``, + ``nova service-disable``, and ``nova service-force-down`` commands has been + removed. diff --git a/releasenotes/notes/remove-virt-interfaces-add-rm-fixed-floating-398c905d9c91cca8.yaml b/releasenotes/notes/remove-virt-interfaces-add-rm-fixed-floating-398c905d9c91cca8.yaml new file mode 100644 index 000000000..dddfa7da9 --- /dev/null +++ b/releasenotes/notes/remove-virt-interfaces-add-rm-fixed-floating-398c905d9c91cca8.yaml @@ -0,0 +1,15 @@ +--- +upgrade: + - | + The following CLIs and their backing API bindings were deprecated and + capped at microversion 2.44: + + * ``nova add-fixed-ip``: use python-neutronclient or openstacksdk + * ``nova remove-fixed-ip``: use python-neutronclient or openstacksdk + * ``nova floating-ip-associate``: use python-neutronclient or openstacksdk + * ``nova floating-ip-disassociate``: use python-neutronclient or + openstacksdk + * ``nova virtual-interface-list``: there is no replacement as this is + only implemented for nova-network which is deprecated + + The CLIs and API bindings have now been removed. diff --git a/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml b/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml index 5748c5e67..4b32d376b 100644 --- a/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml +++ b/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml @@ -2,4 +2,3 @@ upgrade: - remove version 1.1 API support as we only support v2 and v2.1 API in nova side now. - diff --git a/releasenotes/notes/search-hypervisor-detailed-352f3ac70d42fe6e.yaml b/releasenotes/notes/search-hypervisor-detailed-352f3ac70d42fe6e.yaml new file mode 100644 index 000000000..026e1a283 --- /dev/null +++ b/releasenotes/notes/search-hypervisor-detailed-352f3ac70d42fe6e.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The ``novaclient.v2.hypervisors.HypervisorManager.search`` method now + accepts a ``detailed`` boolean kwarg which defaults to False but when + True will search for the given hypervisor hostname match and return + details about any matching hypervisors. Specifying ``detailed=True`` + requires compute API version 2.53 or greater. diff --git a/releasenotes/notes/server-networks-sorted-1d3a7f1c1f88e846.yaml b/releasenotes/notes/server-networks-sorted-1d3a7f1c1f88e846.yaml new file mode 100644 index 000000000..d6b94706f --- /dev/null +++ b/releasenotes/notes/server-networks-sorted-1d3a7f1c1f88e846.yaml @@ -0,0 +1,7 @@ +--- +other: + - | + The ``novaclient.v2.servers.Server.networks`` property method now returns + an OrderedDict where the keys are sorted in natural (ascending) order. + This means the ``nova show`` and ``nova list`` output will have predictable + sort order on the networks attached to a server. diff --git a/releasenotes/notes/show-instance-usage-audit-logs-7826b411fac1283b.yaml b/releasenotes/notes/show-instance-usage-audit-logs-7826b411fac1283b.yaml new file mode 100644 index 000000000..57abbd307 --- /dev/null +++ b/releasenotes/notes/show-instance-usage-audit-logs-7826b411fac1283b.yaml @@ -0,0 +1,8 @@ +--- +features: + - Added new client API and CLI (``nova instance-usage-audit-log``) + to get server usage audit logs. + By default, it lists usage audits for all servers on all + compute hosts where usage auditing is configured. + If you specify the ``--before`` option, the result is filtered + by the date and time before which to list server usage audits. diff --git a/releasenotes/notes/strict_hostname_match-f37243f0520a09a2.yaml b/releasenotes/notes/strict_hostname_match-f37243f0520a09a2.yaml new file mode 100644 index 000000000..04d77d4aa --- /dev/null +++ b/releasenotes/notes/strict_hostname_match-f37243f0520a09a2.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Provides "--strict" option for "nova host-servers-migrate", "nova host-evacuate", + "nova host-evacuate-live" and "nova host-meta" commands. When "--strict" option is + used, the action will be applied to a single compute with the exact hypervisor + hostname string match rather than to the computes with hostname substring match. + When the specified hostname does not exist in the system, "NotFound" error code + will be returned. diff --git a/releasenotes/source/2023.1.rst b/releasenotes/source/2023.1.rst new file mode 100644 index 000000000..2c9a36fae --- /dev/null +++ b/releasenotes/source/2023.1.rst @@ -0,0 +1,6 @@ +=========================== +2023.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: unmaintained/2023.1 diff --git a/releasenotes/source/2023.2.rst b/releasenotes/source/2023.2.rst new file mode 100644 index 000000000..a4838d7d0 --- /dev/null +++ b/releasenotes/source/2023.2.rst @@ -0,0 +1,6 @@ +=========================== +2023.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2023.2 diff --git a/releasenotes/source/2024.1.rst b/releasenotes/source/2024.1.rst new file mode 100644 index 000000000..6896656be --- /dev/null +++ b/releasenotes/source/2024.1.rst @@ -0,0 +1,6 @@ +=========================== +2024.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: unmaintained/2024.1 diff --git a/releasenotes/source/2024.2.rst b/releasenotes/source/2024.2.rst new file mode 100644 index 000000000..aaebcbc8c --- /dev/null +++ b/releasenotes/source/2024.2.rst @@ -0,0 +1,6 @@ +=========================== +2024.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2024.2 diff --git a/releasenotes/source/2025.1.rst b/releasenotes/source/2025.1.rst new file mode 100644 index 000000000..3add0e53a --- /dev/null +++ b/releasenotes/source/2025.1.rst @@ -0,0 +1,6 @@ +=========================== +2025.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.1 diff --git a/releasenotes/source/2025.2.rst b/releasenotes/source/2025.2.rst new file mode 100644 index 000000000..4dae18d86 --- /dev/null +++ b/releasenotes/source/2025.2.rst @@ -0,0 +1,6 @@ +=========================== +2025.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.2 diff --git a/releasenotes/source/2026.1.rst b/releasenotes/source/2026.1.rst new file mode 100644 index 000000000..3d2861580 --- /dev/null +++ b/releasenotes/source/2026.1.rst @@ -0,0 +1,6 @@ +=========================== +2026.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2026.1 diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py index 7a743acac..a328a1af1 100644 --- a/releasenotes/source/conf.py +++ b/releasenotes/source/conf.py @@ -1,27 +1,9 @@ # -*- coding: utf-8 -*- # -# Nova Client Release Notes documentation build configuration file, created by -# sphinx-quickstart on Mon Nov 23 20:38:38 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('.')) +# python-novaclient Release Notes documentation build configuration file # -- 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. @@ -30,71 +12,9 @@ 'openstackdocstheme', ] -# 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'Nova Client Release Notes' -copyright = u'2015, Nova 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. -# -import pbr.version -nova_version = pbr.version.VersionInfo('python-novaclient') -# The short X.Y version. -version = nova_version.canonical_version_string() -# The full version, including alpha/beta/rc tags. -release = nova_version.version_string_with_vcs() - -# 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 ---------------------------------------------- @@ -102,162 +22,7 @@ # a list of builtin themes. html_theme = 'openstackdocs' -# 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 -# " v 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 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 = 'NovaClientReleaseNotestdoc' - - -# -- 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', 'PythonNovaClient.tex', u'Nova Client Release Notes Documentation', - u'Nova 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', 'pythonnovaclient', u'Nova Client Release Notes Documentation', - [u'Nova 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', 'PythonNovaClient', u'Nova Client Release Notes Documentation', - u'Nova developers', 'PythonNovaClient', '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 # -- Options for Internationalization output ------------------------------ + locale_dirs = ['locale/'] diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index cf41b5ca8..5588c8d02 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,23 @@ Contents :maxdepth: 2 unreleased + 2026.1 + 2025.2 + 2025.1 + 2024.2 + 2024.1 + 2023.2 + 2023.1 + zed + yoga + xena + wallaby + victoria + ussuri + train + stein + rocky + queens pike ocata newton diff --git a/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po new file mode 100644 index 000000000..2b450f7c2 --- /dev/null +++ b/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po @@ -0,0 +1,267 @@ +# Andi Chandler , 2017. #zanata +# Andi Chandler , 2018. #zanata +# Andi Chandler , 2022. #zanata +msgid "" +msgstr "" +"Project-Id-Version: python-novaclient\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-06-24 11:46+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2022-07-05 09:42+0000\n" +"Last-Translator: Andi Chandler \n" +"Language-Team: English (United Kingdom)\n" +"Language: en_GB\n" +"X-Generator: Zanata 4.3.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "" +"**auto**: This tells the Compute service to automatically allocate a network " +"for the project if one is not available and then associate an IP from that " +"network with the server. This is the same behavior as passing nics=None " +"before the 2.37 microversion." +msgstr "" +"**auto**: This tells the Compute service to automatically allocate a network " +"for the project if one is not available and then associate an IP from that " +"network with the server. This is the same behaviour as passing nics=None " +"before the 2.37 microversion." + +msgid "" +"**none**: This tells the Compute service to not allocate any networking for " +"the server." +msgstr "" +"**none**: This tells the Compute service to not allocate any networking for " +"the server." + +msgid "--all_tenants replaced by --all-tenants" +msgstr "--all_tenants replaced by --all-tenants" + +msgid "--availability-zone" +msgstr "--availability-zone" + +msgid "--availability_zone replaced by -- availability-zone" +msgstr "--availability_zone replaced by -- availability-zone" + +msgid "--availability_zone replaced by --availability-zone" +msgstr "--availability_zone replaced by --availability-zone" + +msgid "--block_device_mapping replaced by --block-device-mapping" +msgstr "--block_device_mapping replaced by --block-device-mapping" + +msgid "--block_migrate replaced by --block-migrate" +msgstr "--block_migrate replaced by --block-migrate" + +msgid "--bypass_url replaced by --bypass-url" +msgstr "--bypass_url replaced by --bypass-url" + +msgid "--config-drive" +msgstr "--config-drive" + +msgid "--console_type replaced by --console-type" +msgstr "--console_type replaced by --console-type" + +msgid "--disk_over_commit replaced by --disk-over-commit" +msgstr "--disk_over_commit replaced by --disk-over-commit" + +msgid "--endpoint-type replaced by --os-endpoint-type" +msgstr "--endpoint-type replaced by --os-endpoint-type" + +msgid "--floating_ips replaced by --floating-ips" +msgstr "--floating_ips replaced by --floating-ips" + +msgid "--injected_file_content_bytes replaced by --injected-file-content-bytes" +msgstr "" +"--injected_file_content_bytes replaced by --injected-file-content-bytes" + +msgid "--injected_files replaced by --injected-files" +msgstr "--injected_files replaced by --injected-files" + +msgid "--instance_name replaced by --instance-name" +msgstr "--instance_name replaced by --instance-name" + +msgid "--key-name" +msgstr "--key-name" + +msgid "--key_name replaced by --key-name" +msgstr "--key_name replaced by --key-name" + +msgid "--metadata_items replaced by --metadata-items" +msgstr "--metadata_items replaced by --metadata-items" + +msgid "--no-config-drive" +msgstr "--no-config-drive" + +msgid "--num-instance replaced by --min-count and --max-count" +msgstr "--num-instance replaced by --min-count and --max-count" + +msgid "--os_auth_system replaced by --os-auth-system" +msgstr "--os_auth_system replaced by --os-auth-system" + +msgid "--os_auth_url replaced by --os-auth-url" +msgstr "--os_auth_url replaced by --os-auth-url" + +msgid "--os_compute_api_version replaced by --os-compute-api-version" +msgstr "--os_compute_api_version replaced by --os-compute-api-version" + +msgid "--os_password replaced by --os-password" +msgstr "--os_password replaced by --os-password" + +msgid "--os_region_name replaced by --os-region-name" +msgstr "--os_region_name replaced by --os-region-name" + +msgid "--os_tenant_name replaced by --os-tenant-name" +msgstr "--os_tenant_name replaced by --os-tenant-name" + +msgid "--os_username replaced by --os-username" +msgstr "--os_username replaced by --os-username" + +msgid "--policy" +msgstr "--policy" + +msgid "--power-state" +msgstr "--power-state" + +msgid "--progress" +msgstr "--progress" + +msgid "--pub_key replaced by --pub-key" +msgstr "--pub_key replaced by --pub-key" + +msgid "--rebuild_password replaced by --rebuild-password" +msgstr "--rebuild_password replaced by --rebuild-password" + +msgid "--reservation_id replaced by --reservation-id" +msgstr "--reservation_id replaced by --reservation-id" + +msgid "--security_groups replaced by --sercurity-groups" +msgstr "--security_groups replaced by --security-groups" + +msgid "--service_name replaced by --service-name" +msgstr "--service_name replaced by --service-name" + +msgid "--service_type replaced by --service-type" +msgstr "--service_type replaced by --service-type" + +msgid "--task-state" +msgstr "--task-state" + +msgid "--user_data replaced by --user-data" +msgstr "--user_data replaced by --user-data" + +msgid "--vm-state" +msgstr "--vm-state" + +msgid "--volume_service_name replaced by --volume-service-name" +msgstr "--volume_service_name replaced by --volume-service-name" + +msgid "10.0.0" +msgstr "10.0.0" + +msgid "10.1.0" +msgstr "10.1.0" + +msgid "10.1.1" +msgstr "10.1.1" + +msgid "10.2.0" +msgstr "10.2.0" + +msgid "10.3.0" +msgstr "10.3.0" + +msgid "11.0.0" +msgstr "11.0.0" + +msgid "11.0.1" +msgstr "11.0.1" + +msgid "3.0.0" +msgstr "3.0.0" + +msgid "3.3.0" +msgstr "3.3.0" + +msgid "4.0.0" +msgstr "4.0.0" + +msgid "4.1.0" +msgstr "4.1.0" + +msgid "5.0.0" +msgstr "5.0.0" + +msgid "5.1.0" +msgstr "5.1.0" + +msgid "6.0.0" +msgstr "6.0.0" + +msgid ":ref:`search`" +msgstr ":ref:`search`" + +msgid "Current Series Release Notes" +msgstr "Current Series Release Notes" + +msgid "Liberty Series Release Notes" +msgstr "Liberty Series Release Notes" + +msgid "Mitaka Series Release Notes" +msgstr "Mitaka Series Release Notes" + +msgid "Newton Series Release Notes" +msgstr "Newton Series Release Notes" + +msgid "Ocata Series Release Notes" +msgstr "Ocata Series Release Notes" + +msgid "Pike Series Release Notes" +msgstr "Pike Series Release Notes" + +msgid "Queens Series Release Notes" +msgstr "Queens Series Release Notes" + +msgid "Rocky Series Release Notes" +msgstr "Rocky Series Release Notes" + +msgid "Stein Series Release Notes" +msgstr "Stein Series Release Notes" + +msgid "Train Series Release Notes" +msgstr "Train Series Release Notes" + +msgid "Ussuri Series Release Notes" +msgstr "Ussuri Series Release Notes" + +msgid "Victoria Series Release Notes" +msgstr "Victoria Series Release Notes" + +msgid "Wallaby Series Release Notes" +msgstr "Wallaby Series Release Notes" + +msgid "Xena Series Release Notes" +msgstr "Xena Series Release Notes" + +msgid "Yoga Series Release Notes" +msgstr "Yoga Series Release Notes" + +msgid "secgroup-create" +msgstr "secgroup-create" + +msgid "secgroup-delete" +msgstr "secgroup-delete" + +msgid "secgroup-delete-default-rule" +msgstr "secgroup-delete-default-rule" + +msgid "secgroup-delete-group-rule" +msgstr "secgroup-delete-group-rule" + +msgid "secgroup-delete-rule" +msgstr "secgroup-delete-rule" + +msgid "secgroup-list" +msgstr "secgroup-list" + +msgid "secgroup-list-default-rules" +msgstr "secgroup-list-default-rules" diff --git a/releasenotes/source/queens.rst b/releasenotes/source/queens.rst new file mode 100644 index 000000000..36ac6160c --- /dev/null +++ b/releasenotes/source/queens.rst @@ -0,0 +1,6 @@ +=================================== + Queens Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/queens diff --git a/releasenotes/source/rocky.rst b/releasenotes/source/rocky.rst new file mode 100644 index 000000000..40dd517b7 --- /dev/null +++ b/releasenotes/source/rocky.rst @@ -0,0 +1,6 @@ +=================================== + Rocky Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/rocky diff --git a/releasenotes/source/stein.rst b/releasenotes/source/stein.rst new file mode 100644 index 000000000..efaceb667 --- /dev/null +++ b/releasenotes/source/stein.rst @@ -0,0 +1,6 @@ +=================================== + Stein Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/stein diff --git a/releasenotes/source/train.rst b/releasenotes/source/train.rst new file mode 100644 index 000000000..583900393 --- /dev/null +++ b/releasenotes/source/train.rst @@ -0,0 +1,6 @@ +========================== +Train Series Release Notes +========================== + +.. release-notes:: + :branch: stable/train diff --git a/releasenotes/source/ussuri.rst b/releasenotes/source/ussuri.rst new file mode 100644 index 000000000..e21e50e0c --- /dev/null +++ b/releasenotes/source/ussuri.rst @@ -0,0 +1,6 @@ +=========================== +Ussuri Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/ussuri diff --git a/releasenotes/source/victoria.rst b/releasenotes/source/victoria.rst new file mode 100644 index 000000000..8ce933419 --- /dev/null +++ b/releasenotes/source/victoria.rst @@ -0,0 +1,6 @@ +============================= +Victoria Series Release Notes +============================= + +.. release-notes:: + :branch: unmaintained/victoria diff --git a/releasenotes/source/wallaby.rst b/releasenotes/source/wallaby.rst new file mode 100644 index 000000000..bcf35c5f8 --- /dev/null +++ b/releasenotes/source/wallaby.rst @@ -0,0 +1,6 @@ +============================ +Wallaby Series Release Notes +============================ + +.. release-notes:: + :branch: unmaintained/wallaby diff --git a/releasenotes/source/xena.rst b/releasenotes/source/xena.rst new file mode 100644 index 000000000..d19eda488 --- /dev/null +++ b/releasenotes/source/xena.rst @@ -0,0 +1,6 @@ +========================= +Xena Series Release Notes +========================= + +.. release-notes:: + :branch: unmaintained/xena diff --git a/releasenotes/source/yoga.rst b/releasenotes/source/yoga.rst new file mode 100644 index 000000000..43cafdea8 --- /dev/null +++ b/releasenotes/source/yoga.rst @@ -0,0 +1,6 @@ +========================= +Yoga Series Release Notes +========================= + +.. release-notes:: + :branch: unmaintained/yoga diff --git a/releasenotes/source/zed.rst b/releasenotes/source/zed.rst new file mode 100644 index 000000000..6cc2b1554 --- /dev/null +++ b/releasenotes/source/zed.rst @@ -0,0 +1,6 @@ +======================== +Zed Series Release Notes +======================== + +.. release-notes:: + :branch: unmaintained/zed diff --git a/requirements.txt b/requirements.txt index 1a44faf31..88c0e2b6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,12 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. -pbr!=2.1.0,>=2.0.0 # Apache-2.0 -keystoneauth1>=3.2.0 # Apache-2.0 +# Requirements lower bounds listed here are our best effort to keep them up to +# date but we do not test them so no guarantee of having them all correct. If +# you find any incorrect lower bounds, let us know or propose a fix. + +pbr>=3.0.0 # Apache-2.0 +keystoneauth1>=3.5.0 # Apache-2.0 iso8601>=0.1.11 # MIT oslo.i18n>=3.15.3 # Apache-2.0 -oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 -oslo.utils>=3.28.0 # Apache-2.0 -PrettyTable<0.8,>=0.7.1 # BSD -simplejson>=3.5.1 # MIT -six>=1.9.0 # MIT -Babel!=2.4.0,>=2.3.4 # BSD +oslo.serialization>=2.20.0 # Apache-2.0 +oslo.utils>=3.33.0 # Apache-2.0 +PrettyTable>=0.7.2 # BSD +stevedore>=2.0.1 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 7b3f4a201..d78f29505 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,13 @@ [metadata] name = python-novaclient summary = Client library for OpenStack Compute API -description-file = +description_file = README.rst license = Apache License, Version 2.0 author = OpenStack -author-email = openstack-dev@lists.openstack.org -home-page = https://docs.openstack.org/python-novaclient/latest +author_email = openstack-discuss@lists.openstack.org +home_page = https://docs.openstack.org/python-novaclient/latest +python_requires = >=3.9 classifier = Development Status :: 5 - Production/Stable Environment :: Console @@ -16,10 +17,13 @@ classifier = License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: Implementation :: CPython [files] packages = @@ -28,35 +32,3 @@ packages = [entry_points] console_scripts = nova = novaclient.shell:main - -[build_sphinx] -builders = html,man -all-files = 1 -warning-is-error = 1 -source-dir = doc/source -build-dir = doc/build - -[upload_sphinx] -upload-dir = doc/build/html - -[pbr] -autodoc_index_modules = True -autodoc_exclude_modules = novaclient.tests.* novaclient.v2.contrib.* -api_doc_dir = reference/api - -[compile_catalog] -domain = novaclient -directory = novaclient/locale - -[update_catalog] -domain = novaclient -output_dir = novaclient/locale -input_file = novaclient/locale/novaclient.pot - -[extract_messages] -keywords = _ gettext ngettext l_ lazy_gettext -mapping_file = babel.cfg -output_file = novaclient/locale/novaclient.pot - -[wheel] -universal = 1 diff --git a/setup.py b/setup.py index 566d84432..cd35c3c35 100644 --- a/setup.py +++ b/setup.py @@ -13,17 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools -# In python < 2.7.4, a lazy loading of package `pbr` will break -# setuptools if some other modules registered functions in `atexit`. -# solution from: http://bugs.python.org/issue15881#msg170215 -try: - import multiprocessing # noqa -except ImportError: - pass - setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt index b04c4ffd7..9ade238ad 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,26 +1,9 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. -hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 - -bandit>=1.1.0 # Apache-2.0 -coverage!=4.4,>=4.0 # Apache-2.0 +coverage>=4.4.1 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD -keyring>=5.5.1 # MIT/PSF -mock>=2.0.0 # BSD -python-keystoneclient>=3.8.0 # Apache-2.0 -python-cinderclient>=3.2.0 # Apache-2.0 -python-glanceclient>=2.8.0 # Apache-2.0 -python-neutronclient>=6.3.0 # Apache-2.0 -requests-mock>=1.1.0 # Apache-2.0 -sphinx>=1.6.2 # BSD -os-client-config>=1.28.0 # Apache-2.0 -openstackdocstheme>=1.17.0 # Apache-2.0 +requests-mock>=1.2.0 # Apache-2.0 +openstacksdk>=0.11.2 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 -testrepository>=0.0.18 # Apache-2.0/BSD +stestr>=2.0.0 # Apache-2.0 testscenarios>=0.4 # Apache-2.0/BSD -testtools>=1.4.0 # MIT -tempest>=16.1.0 # Apache-2.0 - -# releasenotes -reno>=2.5.0 # Apache-2.0 +testtools>=2.2.0 # MIT +tempest>=17.1.0 # Apache-2.0 diff --git a/tools/nova.bash_completion b/tools/nova.bash_completion index 3c58d3493..bbaee901c 100644 --- a/tools/nova.bash_completion +++ b/tools/nova.bash_completion @@ -3,25 +3,25 @@ _nova_flags="" # lazy init _nova_opts_exp="" # lazy init _nova() { - local cur prev nbc cflags - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" + local cur prev nbc cflags + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" - if [ "x$_nova_opts" == "x" ] ; then - nbc="`nova bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`" - _nova_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`" - _nova_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`" - _nova_opts_exp="`echo "$_nova_opts" | tr ' ' '|'`" - fi + if [ "x$_nova_opts" == "x" ] ; then + nbc="`nova bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`" + _nova_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`" + _nova_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`" + _nova_opts_exp="`echo "$_nova_opts" | tr ' ' '|'`" + fi - if [[ " ${COMP_WORDS[@]} " =~ " "($_nova_opts_exp)" " && "$prev" != "help" ]] ; then - COMPLETION_CACHE=~/.novaclient/*/*-cache - cflags="$_nova_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') - COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) - else - COMPREPLY=($(compgen -W "${_nova_opts}" -- ${cur})) - fi - return 0 + if [[ " ${COMP_WORDS[@]} " =~ " "($_nova_opts_exp)" " && "$prev" != "help" ]] ; then + COMPLETION_CACHE=~/.novaclient/*/*-cache + cflags="$_nova_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') + COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) + else + COMPREPLY=($(compgen -W "${_nova_opts}" -- ${cur})) + fi + return 0 } complete -F _nova nova diff --git a/tools/pretty_tox.sh b/tools/pretty_tox.sh deleted file mode 100755 index 799ac1848..000000000 --- a/tools/pretty_tox.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -set -o pipefail - -TESTRARGS=$1 - -# --until-failure is not compatible with --subunit see: -# -# https://bugs.launchpad.net/testrepository/+bug/1411804 -# -# this work around exists until that is addressed -if [[ "$TESTARGS" =~ "until-failure" ]]; then - python setup.py testr --slowest --testr-args="$TESTRARGS" -else - python setup.py testr --slowest --testr-args="--subunit $TESTRARGS" | subunit-trace -f -fi diff --git a/tools/tox_install.sh b/tools/tox_install.sh deleted file mode 100755 index 43468e450..000000000 --- a/tools/tox_install.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -# Client constraint file contains this client version pin that is in conflict -# with installing the client from source. We should remove the version pin in -# the constraints file before applying it for from-source installation. - -CONSTRAINTS_FILE=$1 -shift 1 - -set -e - -# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get -# published to logs.openstack.org for easy debugging. -localfile="$VIRTUAL_ENV/log/upper-constraints.txt" - -if [[ $CONSTRAINTS_FILE != http* ]]; then - CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE -fi -# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep -curl $CONSTRAINTS_FILE --insecure --progress-bar --output $localfile - -pip install -c$localfile openstack-requirements - -# This is the main purpose of the script: Allow local installation of -# the current repo. It is listed in constraints file and thus any -# install will be constrained and we need to unconstrain it. -edit-constraints $localfile -- $CLIENT_NAME - -pip install -c$localfile -U $* -exit $? diff --git a/tox.ini b/tox.ini index 48351b605..e52b28949 100644 --- a/tox.ini +++ b/tox.ini @@ -1,64 +1,99 @@ -# noted to use py35 you need virtualenv >= 1.11.4 [tox] -envlist = py35,py27,pypy,pep8,docs -minversion = 2.0 -skipsdist = True +envlist = py3,pep8,docs +minversion = 4.6.0 [testenv] -usedevelop = True -# tox is silly... these need to be separated by a newline.... -whitelist_externals = find - bash -passenv = ZUUL_CACHE_DIR - REQUIREMENTS_PIP_LOCATION -install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} -setenv = VIRTUAL_ENV={envdir} - BRANCH_NAME=master - CLIENT_NAME=python-novaclient - -deps = -r{toxinidir}/test-requirements.txt +description = + Run unit tests. +usedevelop = true +allowlist_externals = + find + rm + make +passenv = + ZUUL_CACHE_DIR + REQUIREMENTS_PIP_LOCATION +deps = + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt commands = find . -type f -name "*.pyc" -delete - bash tools/pretty_tox.sh '{posargs}' - # there is also secret magic in pretty_tox.sh which lets you run in a fail only - # mode. To do this define the TRACE_FAILONLY environmental variable. + stestr run {posargs} [testenv:pep8] -commands = flake8 {posargs} +description = + Run style checks. +deps = + pre-commit +commands = + pre-commit run --all-files --show-diff-on-failure [testenv:bandit] -commands = bandit -r novaclient -n5 -x tests +description = + Run security checks. +deps = + pre-commit +commands = + pre-commit run --all-files --show-diff-on-failure bandit [testenv:venv] +deps = + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = {posargs} [testenv:docs] +description = + Build documentation in HTML format. +deps = + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt 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 + rm -rf doc/build/html doc/build/doctrees + sphinx-build -W -b html -d doc/build/doctrees doc/source doc/build/html + # Test the redirects. This must run after the main docs build + whereto doc/build/html/.htaccess doc/test/redirect-tests.txt +[testenv:pdf-docs] +description = + Build documentation in PDF format. +deps = {[testenv:docs]deps} +commands = + rm -rf doc/build/pdf + sphinx-build -W -b latex doc/source doc/build/pdf + make -C doc/build/pdf -[testenv:functional] -basepython = python2.7 -passenv = OS_NOVACLIENT_TEST_NETWORK -setenv = - {[testenv]setenv} - OS_TEST_PATH = ./novaclient/tests/functional -commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' +[testenv:releasenotes] +description = + Build release notes. +deps = + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt +commands = + sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html -[testenv:functional-py35] -basepython = python3.5 -passenv = OS_NOVACLIENT_TEST_NETWORK -setenv = - {[testenv]setenv} - OS_TEST_PATH = ./novaclient/tests/functional -commands = bash tools/pretty_tox.sh '--concurrency=1 {posargs}' +[testenv:functional{,-py310,-py311,-py312,-py313}] +description = + Run functional tests. +passenv = + OS_* +commands = + stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} + python novaclient/tests/functional/hooks/check_resources.py [testenv:cover] +description = + Run unit tests and print coverage information. +setenv = + PYTHON=coverage run --source novaclient --parallel-mode commands = - python setup.py testr --coverage --testr-args='{posargs}' + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml coverage report [flake8] @@ -69,18 +104,21 @@ commands = # # Following checks are ignored on purpose. # -# Additional checks are also ignored on purpose: F811, F821 -ignore = F811,F821,H404,H405 -show-source = True +# Additional checks are also ignored on purpose: F811, F821, W504 +ignore = E731,F811,F821,H404,H405,W504 +show-source = true exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build,doc/source/conf.py,releasenotes [hacking] import_exceptions = novaclient.i18n [testenv:bindep] +description = + Check for installed binary dependencies. # Do not install any requirements. We want this to be fast and work even if # system dependencies are missing, since it's used to tell you what system # dependencies are missing! This also means that bindep must be installed # separately, outside of the requirements files. deps = bindep +skip_install = true commands = bindep test