diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..b474b73c7 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +branch = True +source = novaclient +omit = novaclient/tests/* + +[report] +ignore_errors = True diff --git a/.gitignore b/.gitignore index 3576b8447..611c10e37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,26 @@ .coverage .venv +.stestr/ +subunit.log .tox *,cover cover *.pyc .idea -*.swp +*.sw? *~ build dist -python_novaclient.egg-info AUTHORS ChangeLog novaclient/versioninfo +*.egg +*egg-info +.eggs + +# Files created by documentation build +/doc/build/ +/doc/source/reference/api/ + +# Files created by releasenotes build +/releasenotes/build 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/.mailmap b/.mailmap index 2b533aa89..ec3c43d93 100644 --- a/.mailmap +++ b/.mailmap @@ -4,6 +4,7 @@ Antony Messerli root Chris Behrens comstud +Joe Gordon Johannes Erdfelt jerdfelt @@ -13,7 +14,12 @@ Johannes Erdfelt jerdfelt termie + + + +hwbi Nikolay Sokolov Nokolay Sokolov Nikolay Sokolov Nokolay Sokolov +zhangguoqing 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/.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 new file mode 100644 index 000000000..d4205aed3 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,19 @@ +The source repository for this project can be found at: + + https://opendev.org/openstack/python-novaclient + +Pull requests submitted through GitHub are not monitored. + +To start contributing to OpenStack, follow the steps in the contribution guide +to set up and use Gerrit: + + https://docs.openstack.org/contributors/code-and-documentation/quick-start.html + +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 b/HACKING deleted file mode 100644 index f131e04a1..000000000 --- a/HACKING +++ /dev/null @@ -1,115 +0,0 @@ -Nova Style Commandments -======================= - -Step 1: Read http://www.python.org/dev/peps/pep-0008/ -Step 2: Read http://www.python.org/dev/peps/pep-0008/ again -Step 3: Read on - -Imports -------- -- thou shalt not import objects, only modules -- thou shalt not import more than one module per line -- thou shalt not make relative imports -- thou shalt organize your imports according to the following template - -:: - # vim: tabstop=4 shiftwidth=4 softtabstop=4 - {{stdlib imports in human alphabetical order}} - \n - {{nova imports in human alphabetical order}} - \n - \n - {{begin your code}} - - -General -------- -- thou shalt put two newlines twixt toplevel code (funcs, classes, etc) -- thou shalt put one newline twixt methods in classes and anywhere else -- thou shalt not write "except:", use "except Exception:" at the very least -- thou shalt include your name with TODOs as in "TODO(termie)" -- thou shalt not name anything the same name as a builtin or reserved word -- thou shalt not violate causality in our time cone, or else - - -Human Alphabetical Order Examples ---------------------------------- -:: - import httplib - import logging - import random - import StringIO - import time - import unittest - - from nova import flags - from nova import test - from nova.auth import users - from nova.endpoint import api - from nova.endpoint import cloud - -Docstrings ----------- - """A one line docstring looks like this and ends in a period.""" - - - """A multiline docstring has a one-line summary, less than 80 characters. - - Then a new paragraph after a newline that explains in more detail any - general information about the function, class or method. Example usages - are also great to have here if it is a complex class for function. After - you have finished your descriptions add an extra newline and close the - quotations. - - When writing the docstring for a class, an extra line should be placed - after the closing quotations. For more in-depth explanations for these - decisions see http://www.python.org/dev/peps/pep-0257/ - - If you are going to describe parameters and return values, use Sphinx, the - appropriate syntax is as follows. - - :param foo: the foo parameter - :param bar: the bar parameter - :returns: description of the return value - - """ - -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. - -- All external text that is not explicitly encoded (database storage, - commandline arguments, etc.) should be presumed to be encoded as utf-8. - - WRONG: - - mystring = infile.readline() - myreturnstring = do_some_magic_with(mystring) - outfile.write(myreturnstring) - - RIGHT: - - mystring = infile.readline() - mytext = s.decode('utf-8') - returntext = do_some_magic_with(mytext) - returnstring = returntext.encode('utf-8') - outfile.write(returnstring) diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 000000000..b5653449b --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,41 @@ +Nova Client Style Commandments +============================== + +- Step 1: Read the OpenStack Style Commandments + https://docs.openstack.org/hacking/latest +- Step 2: Read on + +Nova Client Specific Commandments +--------------------------------- +None so far + +Text encoding +------------- + +- Transitions between internal unicode and external strings should always + be immediately and explicitly encoded or decoded. + +- All external text that is not explicitly encoded (database storage, + commandline arguments, etc.) should be presumed to be encoded as utf-8. + + Wrong:: + + mystring = infile.readline() + myreturnstring = do_some_magic_with(mystring) + outfile.write(myreturnstring) + + Right:: + + mystring = infile.readline() + mytext = s.decode('utf-8') + returntext = do_some_magic_with(mytext) + returnstring = returntext.encode('utf-8') + outfile.write(returnstring) + +Running Tests +------------- + +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 +stestr itself. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index ecc3b8b21..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,10 +0,0 @@ -include AUTHORS -include HACKING -include LICENSE -include README.rst -include ChangeLog -include run_tests.sh tox.ini -include novaclient/versioninfo -recursive-include doc * -recursive-include tests * -recursive-include tools * diff --git a/README.rst b/README.rst index 7886df2e6..cdaf86536 100644 --- a/README.rst +++ b/README.rst @@ -1,259 +1,41 @@ -Python bindings to the OpenStack Nova API -================================================== - -This is a client for the OpenStack Nova API. There's a Python API (the -``novaclient`` module), and a command-line script (``nova``). Each -implements 100% of the OpenStack Nova API. - -[PENDING] `Full documentation is available`__. - -__ http://packages.python.org/python-novaclient/ - -You'll also probably want to read `OpenStack Compute Developer Guide API`__ -- -the first bit, at least -- to get an idea of the concepts. Rackspace is doing -the cloud hosting thing a bit differently from Amazon, and if you get the -concepts this library should make more sense. - -__ http://docs.openstack.org/api/ - -The project is hosted on `Launchpad`_, where bugs can be filed. The code is -hosted on `Github`_. Patches must be submitted using `Gerrit`_, *not* Github -pull requests. - -.. _Github: https://github.com/openstack/python-novaclient -.. _Launchpad: https://launchpad.net/python-novaclient -.. _Gerrit: http://wiki.openstack.org/GerritWorkflow - -This code a fork of `Jacobian's python-cloudservers`__ If you need API support -for the Rackspace API solely or the BSD license, you should use that repository. -python-client is licensed under the Apache License like the rest of OpenStack. - -__ http://github.com/jacobian/python-cloudservers - -.. contents:: Contents: - :local: - -Command-line API ----------------- - -Installing this package gets you a shell command, ``nova``, that you -can use to interact with any Rackspace compatible API (including OpenStack). - -You'll need to provide your OpenStack username and password. You can do this -with the ``--os-username``, ``--os-password`` and ``--os-tenant-name`` -params, but it's easier to just set them as environment variables:: - - export OS_USERNAME=openstack - export OS_PASSWORD=yadayada - export OS_TENANT_NAME=myproject - -You will also need to define the authentication url with ``--os-auth-url`` -and the version of the API with ``--version``. Or set them as an environment -variables as well:: - - export OS_AUTH_URL=http://example.com:8774/v1.1/ - export OS_COMPUTE_API_VERSION=1.1 - -If you are using Keystone, you need to set the NOVA_URL to the keystone -endpoint:: - - export OS_AUTH_URL=http://example.com:5000/v2.0/ - -Since Keystone can return multiple regions in the Service Catalog, you -can specify the one you want with ``--os-region-name`` (or -``export OS_REGION_NAME``). It defaults to the first in the list returned. - -You'll find complete documentation on the shell by running -``nova help``:: - - usage: nova [--debug] [--no-cache] [--timings] - [--os-username ] [--os-password ] - [--os-tenant-name ] [--os-auth-url ] - [--os-region-name ] [--os-auth-system ] - [--service-type ] [--service-name ] - [--volume-service-name ] - [--endpoint-type ] - [--os-compute-api-version ] [--insecure] - [--bypass-url ] - ... - - Command-line interface to the OpenStack Nova API. - - Positional arguments: - - absolute-limits Print a list of absolute limits for a user - actions Retrieve server actions. - add-fixed-ip Add new IP address to network. - add-floating-ip Add a floating IP address to a server. - aggregate-add-host Add the host to the specified aggregate. - aggregate-create Create a new aggregate with the specified details. - aggregate-delete Delete the aggregate by its id. - aggregate-details Show details of the specified aggregate. - aggregate-list Print a list of all aggregates. - aggregate-remove-host - Remove the specified host from the specified aggregate. - aggregate-set-metadata - Update the metadata associated with the aggregate. - aggregate-update Update the aggregate's name and optionally - availability zone. - boot Boot a new server. - console-log Get console log output of a server. - credentials Show user credentials returned from auth - delete Immediately shut down and delete a server. - describe-resource Show details about a resource - diagnostics Retrieve server diagnostics. - dns-create Create a DNS entry for domain, name and ip. - dns-create-private-domain - Create the specified DNS domain. - dns-create-public-domain - Create the specified DNS domain. - dns-delete Delete the specified DNS entry. - dns-delete-domain Delete the specified DNS domain. - dns-domains Print a list of available dns domains. - dns-list List current DNS entries for domain and ip or domain - and name. - endpoints Discover endpoints that get returned from the - authenticate services - flavor-create Create a new flavor - flavor-delete Delete a specific flavor - flavor-list Print a list of available 'flavors' (sizes of - servers). - floating-ip-create Allocate a floating IP for the current tenant. - floating-ip-delete De-allocate a floating IP. - floating-ip-list List floating ips for this tenant. - floating-ip-pool-list - List all floating ip pools. - get-vnc-console Get a vnc console to a server. - host-action Perform a power action on a host. - host-update Update host settings. - image-create Create a new image by taking a snapshot of a running - server. - image-delete Delete an image. - image-list Print a list of available images to boot from. - image-meta Set or Delete metadata on an image. - image-show Show details about the given image. - keypair-add Create a new key pair for use with instances - keypair-delete Delete keypair by its id - keypair-list Print a list of keypairs for a user - list List active servers. - live-migration Migrates a running instance to a new machine. - lock Lock a server. - meta Set or Delete metadata on a server. - migrate Migrate a server. - pause Pause a server. - rate-limits Print a list of rate limits for a user - reboot Reboot a server. - rebuild Shutdown, re-image, and re-boot a server. - remove-fixed-ip Remove an IP address from a server. - remove-floating-ip Remove a floating IP address from a server. - rename Rename a server. - rescue Rescue a server. - resize Resize a server. - resize-confirm Confirm a previous resize. - resize-revert Revert a previous resize (and return to the previous - VM). - resume Resume a server. - root-password Change the root password for a server. - secgroup-add-group-rule - Add a source group rule to a security group. - secgroup-add-rule Add a rule to a security group. - secgroup-create Create a security group. - secgroup-delete Delete a security group. - secgroup-delete-group-rule - Delete a source group rule from a security group. - secgroup-delete-rule - Delete a rule from a security group. - secgroup-list List security groups for the curent tenant. - secgroup-list-rules - List rules for a security group. - show Show details about the given server. - ssh SSH into a server. - start Start a server. - stop Stop a server. - suspend Suspend a server. - unlock Unlock a server. - unpause Unpause a server. - unrescue Unrescue a server. - usage-list List usage data for all tenants - volume-attach Attach a volume to a server. - volume-create Add a new volume. - volume-delete Remove a volume. - volume-detach Detach a volume from a server. - volume-list List all the volumes. - volume-show Show details about a volume. - volume-snapshot-create - Add a new snapshot. - volume-snapshot-delete - Remove a snapshot. - volume-snapshot-list - List all the snapshots. - volume-snapshot-show - Show details about a snapshot. - volume-type-create Create a new volume type. - volume-type-delete Delete a specific flavor - volume-type-list Print a list of available 'volume types'. - x509-create-cert Create x509 cert for a user in tenant - x509-get-root-cert Fetches the x509 root cert. - bash-completion Prints all of the commands and options to stdout so - that the - help Display help about this program or one of its - subcommands. - - Optional arguments: - --debug Print debugging output - --no-cache Don't use the auth token cache. - --timings Print call timing info - --os-username - Defaults to env[OS_USERNAME]. - --os-password - Defaults to env[OS_PASSWORD]. - --os-tenant-name - Defaults to env[OS_TENANT_NAME]. - --os-auth-url - Defaults to env[OS_AUTH_URL]. - --os-region-name - Defaults to env[OS_REGION_NAME]. - --os-auth-system - Defaults to env[OS_AUTH_SYSTEM]. - --service-type - Defaults to compute for most actions - --service-name - Defaults to env[NOVA_SERVICE_NAME] - --volume-service-name - Defaults to env[NOVA_VOLUME_SERVICE_NAME] - --endpoint-type - Defaults to env[NOVA_ENDPOINT_TYPE] or publicURL. - --os-compute-api-version - Accepts 1.1, defaults to env[OS_COMPUTE_API_VERSION]. --username USERNAME Deprecated - --insecure Explicitly allow novaclient to perform "insecure" SSL - (https) requests. The server's certificate will not be - verified against any certificate authorities. This - option should be used with caution. - --bypass-url - Use this API endpoint instead of the Service Catalog - - See "nova help COMMAND" for help on a specific command. - -Python API ----------- - -[PENDING] There's also a `complete Python API`__. - -__ http://packages.python.org/python-novaclient/ - -Quick-start using keystone:: - - # use v2.0 auth with http://example.com:5000/v2.0/") - >>> from novaclient.v1_1 import client - >>> nt = client.Client(USER, PASS, TENANT, AUTH_URL, service_type="compute") - >>> nt.flavors.list() - [...] - >>> nt.servers.list() - [...] - >>> nt.keypairs.list() - [...] - -What's new? ------------ - -[PENDING] See `the release notes `_. +======================== +Team and repository tags +======================== + +.. image:: https://governance.openstack.org/tc/badges/python-novaclient.svg + :target: https://governance.openstack.org/tc/reference/tags/index.html + +.. Change things from this point on + +============================================ +Python bindings to the OpenStack Compute API +============================================ + +.. image:: https://img.shields.io/pypi/v/python-novaclient.svg + :target: https://pypi.org/project/python-novaclient/ + :alt: Latest Version + +This is a client for the OpenStack Compute API. It provides a Python API (the +``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 +* `Online Documentation`_ +* `Launchpad project`_ - release management +* `Blueprints`_ - feature specifications +* `Bugs`_ - issue tracking +* `Source`_ +* `Specs`_ +* `How to Contribute`_ +* `Release Notes`_ + +.. _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://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/bindep.txt b/bindep.txt new file mode 100644 index 000000000..bf36b2d84 --- /dev/null +++ b/bindep.txt @@ -0,0 +1,22 @@ +# This is a cross-platform list tracking distribution packages needed by tests; +# see https://docs.opendev.org/opendev/bindep/latest/ for additional information. + +build-essential [platform:dpkg] +dbus-devel [platform:rpm] +dbus-glib-devel [platform:rpm] +gettext +language-pack-en [platform:ubuntu] +libdbus-1-dev [platform:dpkg] +libdbus-glib-1-dev [platform:dpkg] +libffi-dev [platform:dpkg] +libffi-devel [platform:rpm] +libssl-dev [platform:ubuntu] +libuuid-devel [platform:rpm] +locales [platform:debian] +openssl +python3-all-dev [platform:ubuntu !platform:ubuntu-precise] +python3-dev [platform:dpkg] +python3-devel [platform:fedora] +uuid-dev [platform:dpkg] +libpcre2-dev [platform:dpkg doc] +pcre2-devel [platform:rpm doc] diff --git a/doc/.gitignore b/doc/.gitignore deleted file mode 100644 index 567609b12..000000000 --- a/doc/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index 73aeb6edf..000000000 --- a/doc/Makefile +++ /dev/null @@ -1,90 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXSOURCE = source -PAPER = -BUILDDIR = build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SPHINXSOURCE) - -.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-novaclient.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-novaclient.qhc" - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." 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/api.rst b/doc/source/api.rst deleted file mode 100644 index 6e2b1002a..000000000 --- a/doc/source/api.rst +++ /dev/null @@ -1,67 +0,0 @@ -The :mod:`novaclient` Python API -================================== - -.. module:: novaclient - :synopsis: A client for the OpenStack Nova API. - -.. currentmodule:: novaclient - -Usage ------ - -First create an instance of :class:`OpenStack` with your credentials:: - - >>> from novaclient import OpenStack - >>> nova = OpenStack(USERNAME, PASSWORD, AUTH_URL) - -Then call methods on the :class:`OpenStack` object: - -.. class:: OpenStack - - .. attribute:: backup_schedules - - A :class:`BackupScheduleManager` -- manage automatic backup images. - - .. attribute:: flavors - - A :class:`FlavorManager` -- query available "flavors" (hardware - configurations). - - .. attribute:: images - - An :class:`ImageManager` -- query and create server disk images. - - .. attribute:: ipgroups - - A :class:`IPGroupManager` -- manage shared public IP addresses. - - .. attribute:: servers - - A :class:`ServerManager` -- start, stop, and manage virtual machines. - - .. automethod:: authenticate - -For example:: - - >>> nova.servers.list() - [] - - >>> nova.flavors.list() - [, - , - , - , - , - , - ] - - >>> fl = nova.flavors.find(ram=512) - >>> nova.servers.create("my-server", flavor=fl) - - -For more information, see the reference: - -.. toctree:: - :maxdepth: 2 - - ref/index diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst new file mode 100644 index 000000000..ce2917315 --- /dev/null +++ b/doc/source/cli/index.rst @@ -0,0 +1,8 @@ +=============== + CLI Reference +=============== + +.. toctree:: + :maxdepth: 2 + + nova diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst new file mode 100644 index 000000000..03145381a --- /dev/null +++ b/doc/source/cli/nova.rst @@ -0,0 +1,4010 @@ +====== + nova +====== + +The nova client is the command-line interface (CLI) for +the Compute service (nova) API and its extensions. + +For help on a specific :command:`nova` command, enter: + +.. code-block:: console + + $ nova help COMMAND + +.. 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 <>`. + +.. _nova_command_usage: + +nova usage +~~~~~~~~~~ + +.. code-block:: console + + 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:** + +``add-fixed-ip`` + **DEPRECATED** Add new IP address on a network to + server. + +``add-secgroup`` + Add a Security Group to a server. + +``agent-create`` + Create new agent build. + +``agent-delete`` + Delete existing agent build. + +``agent-list`` + List all builds. + +``agent-modify`` + Modify existing agent build. + +``aggregate-add-host`` + Add the host to the specified aggregate. + +``aggregate-cache-images`` + Request images be pre-cached on hosts within an aggregate. + (Supported by API versions '2.81' - '2.latest') + +``aggregate-create`` + Create a new aggregate with the specified + details. + +``aggregate-delete`` + Delete the aggregate. + +``aggregate-list`` + Print a list of all aggregates. + +``aggregate-remove-host`` + Remove the specified host from the specified + aggregate. + +``aggregate-set-metadata`` + Update the metadata associated with the + aggregate. + +``aggregate-show`` + Show details of the specified aggregate. + +``aggregate-update`` + Update the aggregate's name and optionally + availability zone. + +``availability-zone-list`` + List all the availability zones. + +``backup`` + Backup a server by creating a 'backup' type + snapshot. + +``boot`` + Boot a new server. + +``clear-password`` + Clear the admin password for a server from the + metadata server. This action does not actually + change the instance server password. + +``cloudpipe-configure`` + **DEPRECATED** Update the VPN IP/port of a + cloudpipe instance. + +``cloudpipe-create`` + **DEPRECATED** Create a cloudpipe instance for the + given project. + +``cloudpipe-list`` + **DEPRECATED** Print a list of all cloudpipe + instances. + +``console-log`` + Get console log output of a server. + +``delete`` + Immediately shut down and delete specified + server(s). + +``diagnostics`` + Retrieve server diagnostics. + +``evacuate`` + Evacuate server from failed host. + +``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 1461bff54..0552cccab 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,203 +1,87 @@ -# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at # -# python-novaclient documentation build configuration file, created by -# sphinx-quickstart on Sun Dec 6 14:19:25 2009. +# http://www.apache.org/licenses/LICENSE-2.0 # -# This file is execfile()d with the current directory set to its containing -# dir. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. # -# Note 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. - -import os -import sys - -# 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.append(os.path.abspath('.')) +# python-novaclient documentation build configuration file # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be -# extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'openstackdocstheme', + 'sphinx.ext.autodoc', + 'sphinxcontrib.apidoc', +] -# The suffix of source filenames. -source_suffix = '.rst' +# sphinxcontrib.apidoc options +apidoc_module_dir = '../../novaclient' +apidoc_output_dir = 'reference/api' +apidoc_excluded_paths = [ + 'tests/*'] +apidoc_separate_modules = True -# The encoding of source files. -#source_encoding = 'utf-8' +# The content that will be inserted into the main body of an autoclass +# directive. +autoclass_content = 'both' # The master toctree document. master_doc = 'index' -# General information about the project. -project = u'python-novaclient' -copyright = u'Rackspace, based on work by Jacob Kaplan-Moss' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '2.6' -# The full version, including alpha/beta/rc tags. -release = '2.6.10' - -# 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 documents that shouldn't be included in the build. -#unused_docs = [] - -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = [] - -# 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 = [] +copyright = 'OpenStack Contributors' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +html_theme = 'openstackdocs' -# 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'] - -# 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_use_modindex = 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, 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 = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'python-novaclientdoc' +# Add any paths that contain "extra" files, such as .htaccess or +# robots.txt. +html_extra_path = ['_extra'] # -- Options for LaTeX output ------------------------------------------------- -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]) -# . latex_documents = [ - ('index', 'python-novaclient.tex', u'python-novaclient Documentation', - u'Rackspace - based on work by Jacob Kaplan-Moss', 'manual'), + ('index', 'doc-python-novaclient.tex', 'python-novaclient Documentation', + 'OpenStack Foundation', '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 - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_use_modindex = True +latex_elements = { + 'extraclassoptions': 'openany,oneside', + 'preamble': r'\setcounter{tocdepth}{4}', + 'makeindex': '', + 'printindex': '', +} + +# -- Options for openstackdocstheme ------------------------------------------- + +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 ------------------------------------------ -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('cli/nova', 'nova', 'OpenStack Nova command line client', + ['OpenStack Contributors'], 1), +] 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/contributor/deprecation-policy.rst b/doc/source/contributor/deprecation-policy.rst new file mode 100644 index 000000000..c7e84fbbc --- /dev/null +++ b/doc/source/contributor/deprecation-policy.rst @@ -0,0 +1,32 @@ +Deprecating commands +==================== + +There are times when commands need to be deprecated due to rename or removal. +The process for command deprecation is: + +1. Push up a change for review which deprecates the command(s). + + - The change should print a deprecation warning to ``stderr`` each time a + deprecated command is used. + - That warning message should include a rough timeline for when the command + will be removed and what should be used instead, if anything. + - The description in the help text for the deprecated command should mark + that it is deprecated. + - The change should include a release note with the ``deprecations`` section + filled out. + - The deprecation cycle is typically the first client release *after* the + next *full* nova server release so that there is at least six months of + deprecation. + +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.opendev.org/#/admin/groups/147,members + +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 + 2.25.0. Once the change was merged, `python-novaclient` 2.26.0 was released. + Since there was less than six months before 12.0.0 would be released, the + deprecation cycle ran through the 13.0.0 nova server release. diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst new file mode 100644 index 000000000..b52cfce63 --- /dev/null +++ b/doc/source/contributor/index.rst @@ -0,0 +1,20 @@ +=================== + Contributor Guide +=================== + +Basic Information +================= + +.. 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 new file mode 100644 index 000000000..d6a2cfd71 --- /dev/null +++ b/doc/source/contributor/testing.rst @@ -0,0 +1,28 @@ +========= + Testing +========= + +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 py310`` + Traditional unit testing (Python 3.10). + +``tox -e functional`` + Live functional testing against an existing OpenStack instance. (Python 3.10) + +``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-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. + +__ https://governance.openstack.org/tc/reference/project-testing-interface.html diff --git a/doc/source/index.rst b/doc/source/index.rst index a9b617abf..60791b8e7 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,48 +1,27 @@ -Python bindings to the OpenStack Nova API -================================================== +=========================================== + 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:: - You may want to read the `OpenStack Compute Developer Guide`__ -- the overview, at - least -- to get an idea of the concepts. By understanding the concepts + You may want to read the `OpenStack Compute API Guide`__ + to get an idea of the concepts. By understanding the concepts this library should make more sense. - __ http://docs.openstack.org/api/openstack-compute/2/content/ - -Contents: + __ https://docs.openstack.org/api-guide/compute/index.html .. toctree:: :maxdepth: 2 - shell - api - ref/index - releases - -Contributing -============ - -Code is hosted `on GitHub`_. Submit bugs to the Nova project on -`Launchpad`_. Submit code to the openstack/python-novaclient project using -`Gerrit`_. - -.. _on GitHub: https://github.com/openstack/python-novaclient -.. _Launchpad: https://launchpad.net/nova -.. _Gerrit: http://wiki.openstack.org/GerritWorkflow - -Run tests with ``python setup.py test``. - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` + user/index + cli/index + reference/index + contributor/index diff --git a/doc/source/ref/backup_schedules.rst b/doc/source/ref/backup_schedules.rst deleted file mode 100644 index 28163480b..000000000 --- a/doc/source/ref/backup_schedules.rst +++ /dev/null @@ -1,60 +0,0 @@ -Backup schedules -================ - -.. currentmodule:: novaclient - -Rackspace allows scheduling of weekly and/or daily backups for virtual -servers. You can access these backup schedules either off the API object as -:attr:`OpenStack.backup_schedules`, or directly off a particular -:class:`Server` instance as :attr:`Server.backup_schedule`. - -Classes -------- - -.. autoclass:: BackupScheduleManager - :members: create, delete, update, get - -.. autoclass:: BackupSchedule - :members: update, delete - - .. attribute:: enabled - - Is this backup enabled? (boolean) - - .. attribute:: weekly - - The day of week upon which to perform a weekly backup. - - .. attribute:: daily - - The daily time period during which to perform a daily backup. - -Constants ---------- - -Constants for selecting weekly backup days: - - .. data:: BACKUP_WEEKLY_DISABLED - .. data:: BACKUP_WEEKLY_SUNDAY - .. data:: BACKUP_WEEKLY_MONDAY - .. data:: BACKUP_WEEKLY_TUESDAY - .. data:: BACKUP_WEEKLY_WEDNESDA - .. data:: BACKUP_WEEKLY_THURSDAY - .. data:: BACKUP_WEEKLY_FRIDAY - .. data:: BACKUP_WEEKLY_SATURDAY - -Constants for selecting hourly backup windows: - - .. data:: BACKUP_DAILY_DISABLED - .. data:: BACKUP_DAILY_H_0000_0200 - .. data:: BACKUP_DAILY_H_0200_0400 - .. data:: BACKUP_DAILY_H_0400_0600 - .. data:: BACKUP_DAILY_H_0600_0800 - .. data:: BACKUP_DAILY_H_0800_1000 - .. data:: BACKUP_DAILY_H_1000_1200 - .. data:: BACKUP_DAILY_H_1200_1400 - .. data:: BACKUP_DAILY_H_1400_1600 - .. data:: BACKUP_DAILY_H_1600_1800 - .. data:: BACKUP_DAILY_H_1800_2000 - .. data:: BACKUP_DAILY_H_2000_2200 - .. data:: BACKUP_DAILY_H_2200_0000 diff --git a/doc/source/ref/exceptions.rst b/doc/source/ref/exceptions.rst deleted file mode 100644 index 0744edc22..000000000 --- a/doc/source/ref/exceptions.rst +++ /dev/null @@ -1,14 +0,0 @@ -Exceptions -========== - -.. currentmodule:: novaclient - -Exceptions ----------- - -Exceptions that the API might throw: - -.. automodule:: novaclient - :members: OpenStackException, BadRequest, Unauthorized, Forbidden, - NotFound, OverLimit - diff --git a/doc/source/ref/flavors.rst b/doc/source/ref/flavors.rst deleted file mode 100644 index 6b281a0e4..000000000 --- a/doc/source/ref/flavors.rst +++ /dev/null @@ -1,35 +0,0 @@ -Flavors -======= - -From Rackspace's API documentation: - - A flavor is an available hardware configuration for a server. Each flavor - has a unique combination of disk space, memory capacity and priority for - CPU time. - -Classes -------- - -.. currentmodule:: novaclient - -.. autoclass:: FlavorManager - :members: get, list, find, findall - -.. autoclass:: Flavor - :members: - - .. attribute:: id - - This flavor's ID. - - .. attribute:: name - - A human-readable name for this flavor. - - .. attribute:: ram - - The amount of RAM this flavor has, in MB. - - .. attribute:: disk - - The amount of disk space this flavor has, in MB diff --git a/doc/source/ref/images.rst b/doc/source/ref/images.rst deleted file mode 100644 index 7d1ceab70..000000000 --- a/doc/source/ref/images.rst +++ /dev/null @@ -1,54 +0,0 @@ -Images -====== - -.. currentmodule:: novaclient - -An "image" is a snapshot from which you can create new server instances. - -From Rackspace's own API documentation: - - An image is a collection of files used to create or rebuild a server. - Rackspace provides a number of pre-built OS images by default. You may - also create custom images from cloud servers you have launched. These - custom images are useful for backup purposes or for producing "gold" - server images if you plan to deploy a particular server configuration - frequently. - -Classes -------- - -.. autoclass:: ImageManager - :members: get, list, find, findall, create, delete - -.. autoclass:: Image - :members: delete - - .. attribute:: id - - This image's ID. - - .. attribute:: name - - This image's name. - - .. attribute:: created - - The date/time this image was created. - - .. attribute:: updated - - The date/time this instance was updated. - - .. attribute:: status - - The status of this image (usually ``"SAVING"`` or ``ACTIVE``). - - .. attribute:: progress - - During saving of an image this'll be set to something between - 0 and 100, representing a rough percentage done. - - .. attribute:: serverId - - If this image was created from a :class:`Server` then this attribute - will be set to the ID of the server whence this image came. diff --git a/doc/source/ref/index.rst b/doc/source/ref/index.rst deleted file mode 100644 index c1fe136bb..000000000 --- a/doc/source/ref/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -API Reference -============= - -.. toctree:: - :maxdepth: 1 - - backup_schedules - exceptions - flavors - images - ipgroups - servers \ No newline at end of file diff --git a/doc/source/ref/ipgroups.rst b/doc/source/ref/ipgroups.rst deleted file mode 100644 index a08d0668f..000000000 --- a/doc/source/ref/ipgroups.rst +++ /dev/null @@ -1,46 +0,0 @@ -Shared IP addresses -=================== - -From the Rackspace API guide: - - Public IP addresses can be shared across multiple servers for use in - various high availability scenarios. When an IP address is shared to - another server, the cloud network restrictions are modified to allow each - server to listen to and respond on that IP address (you may optionally - specify that the target server network configuration be modified). Shared - IP addresses can be used with many standard heartbeat facilities (e.g. - ``keepalived``) that monitor for failure and manage IP failover. - - A shared IP group is a collection of servers that can share IPs with other - members of the group. Any server in a group can share one or more public - IPs with any other server in the group. With the exception of the first - server in a shared IP group, servers must be launched into shared IP - groups. A server may only be a member of one shared IP group. - -.. seealso:: - - Use :meth:`Server.share_ip` and `Server.unshare_ip` to share and unshare - IPs in a group. - -Classes -------- - -.. currentmodule:: novaclient - -.. autoclass:: IPGroupManager - :members: get, list, find, findall, create, delete - -.. autoclass:: IPGroup - :members: delete - - .. attribute:: id - - Shared group ID. - - .. attribute:: name - - Name of the group. - - .. attribute:: servers - - A list of server IDs in this group. diff --git a/doc/source/ref/servers.rst b/doc/source/ref/servers.rst deleted file mode 100644 index 529b807d8..000000000 --- a/doc/source/ref/servers.rst +++ /dev/null @@ -1,73 +0,0 @@ -Servers -======= - -A virtual machine instance. - -Classes -------- - -.. currentmodule:: novaclient - -.. autoclass:: ServerManager - :members: get, list, find, findall, create, update, delete, share_ip, - unshare_ip, reboot, rebuild, resize, confirm_resize, - revert_resize - -.. autoclass:: Server - :members: update, delete, share_ip, unshare_ip, reboot, rebuild, resize, - confirm_resize, revert_resize - - .. attribute:: id - - This server's ID. - - .. attribute:: name - - The name you gave the server when you booted it. - - .. attribute:: imageId - - The :class:`Image` this server was booted with. - - .. attribute:: flavorId - - This server's current :class:`Flavor`. - - .. attribute:: hostId - - Rackspace doesn't document this value. It appears to be SHA1 hash. - - .. attribute:: status - - The server's status (``BOOTING``, ``ACTIVE``, etc). - - .. attribute:: progress - - When booting, resizing, updating, etc., this will be set to a - value between 0 and 100 giving a rough estimate of the progress - of the current operation. - - .. attribute:: addresses - - The public and private IP addresses of this server. This'll be a dict - of the form:: - - { - "public" : ["67.23.10.138"], - "private" : ["10.176.42.19"] - } - - You *can* get more than one public/private IP provisioned, but not - directly from the API; you'll need to open a support ticket. - - .. attribute:: metadata - - The metadata dict you gave when creating the server. - -Constants ---------- - -Reboot types: - -.. data:: REBOOT_SOFT -.. data:: REBOOT_HARD diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst new file mode 100644 index 000000000..272b64ada --- /dev/null +++ b/doc/source/reference/index.rst @@ -0,0 +1,8 @@ +========= +Reference +========= + +.. toctree:: + :maxdepth: 6 + + api/modules diff --git a/doc/source/releases.rst b/doc/source/releases.rst deleted file mode 100644 index 6eb034033..000000000 --- a/doc/source/releases.rst +++ /dev/null @@ -1,99 +0,0 @@ -============= -Release notes -============= - -2.5.8 (July 11, 2011) -===================== -* returns all public/private ips, not just first one -* better 'nova list' search options - -2.5.7 - 2.5.6 = minor tweaks - -2.5.5 (June 21, 2011) -===================== -* zone-boot min/max instance count added thanks to comstud -* create for user added thanks to cerberus -* fixed tests - -2.5.3 (June 15, 2011) -===================== -* ProjectID can be None for backwards compatability. -* README/docs updated for projectId thanks to usrleon - -2.5.1 (June 10, 2011) -===================== -* ProjectID now part of authentication - -2.5.0 (June 3, 2011) -================= - -* better logging thanks to GridDynamics - -2.4.4 (June 1, 2011) -================= - -* added support for GET /servers with reservation_id (and /servers/detail) - -2.4.3 (May 27, 2011) -================= - -* added support for POST /zones/select (client only, not cmdline) - -2.4 (March 7, 2011) -================= - -* added Jacob Kaplan-Moss copyright notices to older/untouched files. - - -2.3 (March 2, 2011) -================= - -* package renamed to python-novaclient. Module to novaclient - - -2.2 (March 1, 2011) -================= - -* removed some license/copywrite notices from source that wasn't - significantly changed. - - -2.1 (Feb 28, 2011) -================= - -* shell renamed to nova from novatools - -* license changed from BSD to Apache - -2.0 (Feb 7, 2011) -================= - -* Forked from https://github.com/jacobian/python-cloudservers - -* Rebranded to python-novatools - -* Auth URL support - -* New OpenStack specific commands added (pause, suspend, etc) - -1.2 (August 15, 2010) -===================== - -* Support for Python 2.4 - 2.7. - -* Improved output of :program:`cloudservers ipgroup-list`. - -* Made ``cloudservers boot --ipgroup `` work (as well as ``--ipgroup - ``). - -1.1 (May 6, 2010) -================= - -* Added a ``--files`` option to :program:`cloudservers boot` supporting - the upload of (up to five) files at boot time. - -* Added a ``--key`` option to :program:`cloudservers boot` to key the server - with an SSH public key at boot time. This is just a shortcut for ``--files``, - but it's a useful shortcut. - -* Changed the default server image to Ubuntu 10.04 LTS. diff --git a/doc/source/shell.rst b/doc/source/shell.rst deleted file mode 100644 index f02c1597e..000000000 --- a/doc/source/shell.rst +++ /dev/null @@ -1,52 +0,0 @@ -The :program:`nova` shell utility -========================================= - -.. program:: nova -.. highlight:: bash - -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 username and -API key. You can do this with the :option:`--os-username`, :option:`--os-password` -and :option:`--os-tenant-id` options, but it's easier to just set them as -environment variables by setting two environment variables: - -.. envvar:: OS_USERNAME - - Your OpenStack Nova username. - -.. envvar:: OS_PASSWORD - - Your password. - -.. envvar:: OS_TENANT_NAME - - Project for work. - -.. envvar:: OS_AUTH_URL - - The OpenStack API server URL. - -.. envvar:: OS_COMPUTE_API_VERSION - - The OpenStack API version. - -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://... - export OS_COMPUTE_API_VERSION=1.1 - -From there, all shell commands take the form:: - - nova [arguments...] - -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. diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst new file mode 100644 index 000000000..32510e676 --- /dev/null +++ b/doc/source/user/index.rst @@ -0,0 +1,9 @@ +============ + User Guide +============ + +.. toctree:: + :maxdepth: 2 + + shell + python-api diff --git a/doc/source/user/python-api.rst b/doc/source/user/python-api.rst new file mode 100644 index 000000000..80adda3a1 --- /dev/null +++ b/doc/source/user/python-api.rst @@ -0,0 +1,104 @@ +================================== + The :mod:`novaclient` Python API +================================== + +.. module:: novaclient + :synopsis: A client for the OpenStack Nova API. + :no-index: + +.. currentmodule:: novaclient + +Usage +----- + +First create a client instance with your credentials:: + + >>> from novaclient import client + >>> nova = client.Client(VERSION, USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) + +Here ``VERSION`` can be a string or ``novaclient.api_versions.APIVersion`` obj. +If you prefer string value, you can use ``1.1`` (deprecated now), ``2`` or +``2.X`` (where X is a microversion). + +Alternatively, you can create a client instance using the keystoneauth +session API:: + + >>> from keystoneauth1 import loading + >>> from keystoneauth1 import session + >>> from novaclient import client + >>> loader = loading.get_plugin_loader('password') + >>> auth = loader.load_from_options(auth_url=AUTH_URL, + ... username=USERNAME, + ... password=PASSWORD, + ... project_id=PROJECT_ID) + >>> sess = session.Session(auth=auth) + >>> nova = client.Client(VERSION, session=sess) + +If you have PROJECT_NAME instead of a PROJECT_ID, use the project_name +parameter. Similarly, if your cloud uses keystone v3 and you have a DOMAIN_NAME +or DOMAIN_ID, provide it as `user_domain_(name|id)` and if you are using a +PROJECT_NAME also provide the domain information as `project_domain_(name|id)`. + +novaclient adds 'python-novaclient' and its version to the user-agent string +that keystoneauth produces. If you are creating an application using novaclient +and want to register a name and version in the user-agent string, pass those +to the Session:: + + >>> sess = session.Session( + ... auth=auth, app_name'nodepool', app_version'1.2.3') + +If you are making a library that consumes novaclient but is not an end-user +application, you can append a (name, version) tuple to the session's +`additional_user_agent` property:: + + >>> sess = session.Session(auth=auth) + >>> sess.additional_user_agent.append(('shade', '1.2.3')) + +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:: + + >>> from novaclient import client + >>> with client.Client(VERSION, USERNAME, PASSWORD, + ... PROJECT_ID, AUTH_URL) as nova: + ... nova.servers.list() + ... nova.flavors.list() + ... + +It is also possible to have a permanent (process-long) connection pool, +by passing a connection_pool=True:: + + >>> from novaclient import client + >>> nova = client.Client(VERSION, USERNAME, PASSWORD, PROJECT_ID, + ... AUTH_URL, connection_pool=True) + +Then call methods on its managers:: + + >>> nova.servers.list() + [] + + >>> nova.flavors.list() + [, + , + , + , + , + , + ] + + >>> fl = nova.flavors.find(ram=512) + >>> nova.servers.create("my-server", flavor=fl) + + +.. warning:: Direct initialization of ``novaclient.v2.client.Client`` object + can cause you to "shoot yourself in the foot". See launchpad bug-report + `1493576`_ for more details. + +.. _1493576: https://launchpad.net/bugs/1493576 + +Reference +--------- + +See :doc:`the module reference `. diff --git a/doc/source/user/shell.rst b/doc/source/user/shell.rst new file mode 100644 index 000000000..de96637a7 --- /dev/null +++ b/doc/source/user/shell.rst @@ -0,0 +1,96 @@ +=================================== + The :program:`nova` Shell Utility +=================================== + +.. program:: nova +.. highlight:: bash + +The :program:`nova` shell utility interacts with OpenStack Nova API from the +command line. It supports the entirety of the OpenStack Nova API. + +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: + +.. 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 Keystone user name. + +.. envvar:: OS_PASSWORD + + Your password. + +.. 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 + + The user's domain name. + +.. envvar:: OS_USER_DOMAIN_ID + + The user's domain ID. + +.. envvar:: OS_AUTH_URL + + The OpenStack Keystone endpoint URL. + +.. envvar:: OS_COMPUTE_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_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:: + + nova [arguments...] + +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. + +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 fa6b8e590..bf021c6ac 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -11,23 +11,18 @@ # 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 os +import pbr.version -def _get_novaclient_version(): - """Read version from versioninfo file.""" - mod_abspath = inspect.getabsfile(inspect.currentframe()) - novaclient_path = os.path.dirname(mod_abspath) - version_path = os.path.join(novaclient_path, 'versioninfo') +from novaclient import api_versions - if os.path.exists(version_path): - version = open(version_path).read().strip() - else: - version = "Unknown, couldn't find versioninfo file at %s"\ - % version_path - return version +__version__ = pbr.version.VersionInfo('python-novaclient').version_string() - -__version__ = _get_novaclient_version() +API_MIN_VERSION = api_versions.APIVersion("2.1") +# The max version should be the latest version that is supported in the client, +# not necessarily the latest that the server can provide. This is only bumped +# 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.96") diff --git a/novaclient/api_versions.py b/novaclient/api_versions.py new file mode 100644 index 000000000..59b8c1cbc --- /dev/null +++ b/novaclient/api_versions.py @@ -0,0 +1,459 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import functools +import logging +import os +import re +import traceback +import warnings + +from oslo_utils import strutils + +import novaclient +from novaclient import exceptions +from novaclient.i18n import _ + +LOG = logging.getLogger(__name__) +if not LOG.handlers: + LOG.addHandler(logging.StreamHandler()) + + +LEGACY_HEADER_NAME = "X-OpenStack-Nova-API-Version" +HEADER_NAME = "OpenStack-API-Version" +SERVICE_TYPE = "compute" + +_SUBSTITUTIONS = {} + +_type_error_msg = _("'%(other)s' should be an instance of '%(cls)s'") + + +class APIVersion(object): + """This class represents an API Version Request. + + This class provides convenience methods for manipulation + and comparison of version numbers that we need to do to + implement microversions. + """ + + def __init__(self, version_str=None): + """Create an API version object. + + :param version_str: String representation of APIVersionRequest. + Correct format is 'X.Y', where 'X' and 'Y' + are int values. None value should be used + to create Null APIVersionRequest, which is + equal to 0.0 + """ + self.ver_major = 0 + self.ver_minor = 0 + + if version_str is not None: + match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0|latest)$", version_str) + if match: + self.ver_major = int(match.group(1)) + if match.group(2) == "latest": + # NOTE(andreykurilin): Infinity allows to easily determine + # latest version and doesn't require any additional checks + # in comparison methods. + self.ver_minor = float("inf") + else: + self.ver_minor = int(match.group(2)) + else: + msg = _("Invalid format of client version '%s'. " + "Expected format 'X.Y', where X is a major part and Y " + "is a minor part of version.") % version_str + raise exceptions.UnsupportedVersion(msg) + + def __str__(self): + """Debug/Logging representation of object.""" + if self.is_latest(): + return "Latest API Version Major: %s" % self.ver_major + return ("API Version Major: %s, Minor: %s" + % (self.ver_major, self.ver_minor)) + + def __repr__(self): + if self.is_null(): + return "" + else: + return "" % self.get_string() + + def is_null(self): + return self.ver_major == 0 and self.ver_minor == 0 + + def is_latest(self): + return self.ver_minor == float("inf") + + def __lt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) < + (other.ver_major, other.ver_minor)) + + def __eq__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) == + (other.ver_major, other.ver_minor)) + + def __gt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) > + (other.ver_major, other.ver_minor)) + + def __le__(self, other): + return self < other or self == other + + def __ne__(self, other): + return not self.__eq__(other) + + def __ge__(self, other): + return self > other or self == other + + def matches(self, min_version, max_version): + """Matches the version object. + + Returns whether the version object represents a version + greater than or equal to the minimum version and less than + or equal to the maximum version. + + :param min_version: Minimum acceptable version. + :param max_version: Maximum acceptable version. + :returns: boolean + + If min_version is null then there is no minimum limit. + If max_version is null then there is no maximum limit. + If self is null then raise ValueError + """ + + if self.is_null(): + raise ValueError(_("Null APIVersion doesn't support 'matches'.")) + if max_version.is_null() and min_version.is_null(): + return True + elif max_version.is_null(): + return min_version <= self + elif min_version.is_null(): + return self <= max_version + else: + return min_version <= self <= max_version + + def get_string(self): + """Version string representation. + + Converts object to string representation which if used to create + an APIVersion object results in the same version. + """ + if self.is_null(): + raise ValueError( + _("Null APIVersion cannot be converted to string.")) + elif self.is_latest(): + return "%s.%s" % (self.ver_major, "latest") + return "%s.%s" % (self.ver_major, self.ver_minor) + + +class VersionedMethod(object): + + def __init__(self, name, start_version, end_version, func): + """Versioning information for a single method + + :param name: Name of the method + :param start_version: Minimum acceptable version + :param end_version: Maximum acceptable_version + :param func: Method to call + + Minimum and maximums are inclusive + """ + self.name = name + self.start_version = start_version + self.end_version = end_version + self.func = func + + def __str__(self): + return ("Version Method %s: min: %s, max: %s" + % (self.name, self.start_version, self.end_version)) + + def __repr__(self): + return "" % self.name + + +def get_available_major_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 + """ + 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) + + +def get_api_version(version_string): + """Returns checked APIVersion object""" + version_string = str(version_string) + if strutils.is_int_like(version_string): + version_string = "%s.0" % version_string + + api_version = APIVersion(version_string) + check_major_version(api_version) + return api_version + + +def _get_server_version_range(client): + version = client.versions.get_current() + + if not hasattr(version, 'version') or not version.version: + return APIVersion(), APIVersion() + + return APIVersion(version.min_version), APIVersion(version.version) + + +def discover_version(client, requested_version): + """Discover most recent version supported by API and client. + + Checks ``requested_version`` and returns the most recent version + supported by both the API and the client. + + :param client: client object + :param requested_version: requested version represented by APIVersion obj + :returns: APIVersion + """ + server_start_version, server_end_version = _get_server_version_range( + client) + + if (not requested_version.is_latest() and + requested_version != APIVersion('2.0')): + if server_start_version.is_null() and server_end_version.is_null(): + raise exceptions.UnsupportedVersion( + _("Server doesn't support microversions")) + if not requested_version.matches(server_start_version, + server_end_version): + raise exceptions.UnsupportedVersion( + _("The specified version isn't supported by server. The valid " + "version range is '%(min)s' to '%(max)s'") % { + "min": server_start_version.get_string(), + "max": server_end_version.get_string()}) + return requested_version + + if requested_version == APIVersion('2.0'): + if (server_start_version == APIVersion('2.1') or + (server_start_version.is_null() and + server_end_version.is_null())): + return APIVersion('2.0') + else: + raise exceptions.UnsupportedVersion( + _("The server isn't backward compatible with Nova V2 REST " + "API")) + + if server_start_version.is_null() and server_end_version.is_null(): + return APIVersion('2.0') + elif novaclient.API_MIN_VERSION > server_end_version: + raise exceptions.UnsupportedVersion( + _("Server version is too old. The client valid version range is " + "'%(client_min)s' to '%(client_max)s'. The server valid version " + "range is '%(server_min)s' to '%(server_max)s'.") % { + 'client_min': novaclient.API_MIN_VERSION.get_string(), + 'client_max': novaclient.API_MAX_VERSION.get_string(), + 'server_min': server_start_version.get_string(), + 'server_max': server_end_version.get_string()}) + elif novaclient.API_MAX_VERSION < server_start_version: + raise exceptions.UnsupportedVersion( + _("Server version is too new. The client valid version range is " + "'%(client_min)s' to '%(client_max)s'. The server valid version " + "range is '%(server_min)s' to '%(server_max)s'.") % { + 'client_min': novaclient.API_MIN_VERSION.get_string(), + 'client_max': novaclient.API_MAX_VERSION.get_string(), + 'server_min': server_start_version.get_string(), + 'server_max': server_end_version.get_string()}) + elif novaclient.API_MAX_VERSION <= server_end_version: + return novaclient.API_MAX_VERSION + elif server_end_version < novaclient.API_MAX_VERSION: + return server_end_version + + +def update_headers(headers, api_version): + """Set microversion headers if api_version is not null""" + + if not api_version.is_null(): + version_string = api_version.get_string() + if api_version.ver_minor != 0: + headers[LEGACY_HEADER_NAME] = version_string + if api_version.ver_minor >= 27: + headers[HEADER_NAME] = '%s %s' % (SERVICE_TYPE, version_string) + + +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): + _warn_missing_microversion_header(LEGACY_HEADER_NAME) + elif (api_version.ver_minor >= 27 and + HEADER_NAME not in response.headers): + _warn_missing_microversion_header(HEADER_NAME) + + +def _add_substitution(versioned_method): + _SUBSTITUTIONS.setdefault(versioned_method.name, []) + _SUBSTITUTIONS[versioned_method.name].append(versioned_method) + + +def _get_function_name(func): + # NOTE(andreykurilin): Based on the facts: + # - Python 2 does not have __qualname__ property as Python 3 has; + # - we cannot use im_class here, since we need to obtain name of + # function in `wraps` decorator during class initialization + # ("im_class" property does not exist at that moment) + # we need to write own logic to obtain the full function name which + # include module name, owner name(optional) and just function name. + filename, _lineno, _name, line = traceback.extract_stack()[-4] + module, _file_extension = os.path.splitext(filename) + module = module.replace("/", ".") + if module.endswith(func.__module__): + return "%s.[%s].%s" % (func.__module__, line, func.__name__) + else: + return "%s.%s" % (func.__module__, func.__name__) + + +def get_substitutions(func_name, api_version=None): + if hasattr(func_name, "__id__"): + func_name = func_name.__id__ + + substitutions = _SUBSTITUTIONS.get(func_name, []) + if api_version and not api_version.is_null(): + return [m for m in substitutions + if api_version.matches(m.start_version, m.end_version)] + return sorted(substitutions, key=lambda m: m.start_version) + + +# FIXME(mriedem): This breaks any ManagerWithFind.list method that has a +# 'detailed' kwarg since the ManagerWithFind.findall won't find the correct +# argspec from the wrapped list method. +def wraps(start_version, end_version=None): + start_version = APIVersion(start_version) + if end_version: + end_version = APIVersion(end_version) + else: + end_version = APIVersion("%s.latest" % start_version.ver_major) + + def decor(func): + func.versioned = True + name = _get_function_name(func) + + versioned_method = VersionedMethod(name, start_version, + end_version, func) + _add_substitution(versioned_method) + + @functools.wraps(func) + def substitution(obj, *args, **kwargs): + methods = get_substitutions(name, obj.api_version) + + if not methods: + raise exceptions.VersionNotFoundForAPIMethod( + obj.api_version.get_string(), name) + return methods[-1].func(obj, *args, **kwargs) + + # Let's share "arguments" with original method and substitution to + # allow put utils.arg and wraps decorators in any order + if not hasattr(func, 'arguments'): + func.arguments = [] + substitution.arguments = func.arguments + + # NOTE(andreykurilin): The way to obtain function's name in Python 2 + # bases on traceback(see _get_function_name for details). Since the + # right versioned method is used in several places, one object + # can have different names. Let's generate name of function one time + # and use __id__ property in all other places. + substitution.__id__ = name + + return substitution + + return decor + + +def _warn_missing_microversion_header(header_name): + """Log a warning about missing microversion response header.""" + LOG.warning(_( + "Your request was processed by a Nova API which does not support " + "microversions (%s header is missing from response). " + "Warning: Response may be incorrect."), header_name) + + +def deprecated_after(version): + decorator = wraps('2.0', version) + + def wrapper(fn): + @functools.wraps(fn) + def wrapped(*a, **k): + decorated = decorator(fn) + if hasattr(fn, '__module__'): + mod = fn.__module__ + else: + mod = a[0].__module__ + warnings.warn('The %s module is deprecated ' + 'and will be removed.' % mod, DeprecationWarning) + return decorated(*a, **k) + return wrapped + return wrapper diff --git a/novaclient/base.py b/novaclient/base.py index c5aa4a769..48c06f372 100644 --- a/novaclient/base.py +++ b/novaclient/base.py @@ -1,6 +1,6 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -19,47 +19,238 @@ Base utilities to build API operation managers and objects on top of. """ +import abc import contextlib +import copy import hashlib import os -from novaclient import exceptions -from novaclient import utils +import threading +from oslo_utils import reflection +from oslo_utils import strutils +import requests -# Python 2.4 compat -try: - all -except NameError: - def all(iterable): - return True not in (not x for x in iterable) +from novaclient import exceptions +from novaclient import utils def getid(obj): - """ + """Get object's ID or object. + 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 +class HookableMixin(object): + """Mixin so classes can register and run hooks.""" + _hooks_map = {} + + @classmethod + def add_hook(cls, hook_type, hook_func): + """Add a new hook of specified type. + + :param cls: class that registers hooks + :param hook_type: hook type, e.g., '__pre_parse_args__' + :param hook_func: hook function + """ + if hook_type not in cls._hooks_map: + cls._hooks_map[hook_type] = [] + + cls._hooks_map[hook_type].append(hook_func) + + @classmethod + def run_hooks(cls, hook_type, *args, **kwargs): + """Run all hooks of specified type. + + :param cls: class that registers hooks + :param hook_type: hook type, e.g., '__pre_parse_args__' + :param args: args to be passed to every hook function + :param kwargs: kwargs to be passed to every hook function + """ + hook_funcs = cls._hooks_map.get(hook_type) or [] + for hook_func in hook_funcs: + hook_func(*args, **kwargs) + + +class RequestIdMixin(object): + """Wrapper class to expose x-openstack-request-id to the caller. + """ + def request_ids_setup(self): + self.x_openstack_request_ids = [] + + @property + def request_ids(self): + return self.x_openstack_request_ids + + def append_request_ids(self, resp): + """Add request_ids as an attribute to the object + + :param resp: Response object or list of Response objects + """ + if isinstance(resp, list): + # Add list of request_ids if response is of type list. + for resp_obj in resp: + self._append_request_id(resp_obj) + elif resp is not None: + # Add request_ids if response contains single object. + self._append_request_id(resp) + + def _append_request_id(self, resp): + 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 + resp.headers.get('x-compute-request-id')) + else: + # If resp is of type string or None. + request_id = resp + if request_id not in self.x_openstack_request_ids: + self.x_openstack_request_ids.append(request_id) -class Manager(utils.HookableMixin): +class Resource(RequestIdMixin): + """Base class for OpenStack resources (tenant, user, etc.). + + This is pretty much just a bag for attributes. """ + + HUMAN_ID = False + NAME_ATTR = 'name' + + def __init__(self, manager, info, loaded=False, resp=None): + """Populate and bind to a manager. + + :param manager: BaseManager object + :param info: dictionary representing resource attributes + :param loaded: prevent lazy-loading if set to True + :param resp: Response or list of Response objects + """ + self.manager = manager + self._info = info + self._add_details(info) + self._loaded = loaded + self.request_ids_setup() + self.append_request_ids(resp) + + def __repr__(self): + reprkeys = sorted(k + for k in self.__dict__.keys() + if k[0] != '_' and + k not in ['manager', 'x_openstack_request_ids']) + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) + + @property + def api_version(self): + return self.manager.api_version + + @property + def human_id(self): + """Human-readable ID which can be used for bash completion. + """ + if self.HUMAN_ID: + name = getattr(self, self.NAME_ATTR, None) + if name is not None: + return strutils.to_slug(name) + return None + + def _add_details(self, info): + for (k, v) in info.items(): + try: + setattr(self, k, v) + self._info[k] = v + except AttributeError: + # In this case we already defined the attribute on the class + pass + + def __getattr__(self, k): + if k not in self.__dict__: + # NOTE(bcwaldon): disallow lazy-loading if already loaded once + if not self.is_loaded(): + self.get() + return self.__getattr__(k) + + raise AttributeError(k) + else: + return self.__dict__[k] + + def get(self): + """Support for lazy loading details. + + Some clients, such as novaclient have the option to lazy load the + details, details which can be loaded with this function. + """ + # set_loaded() first ... so if we have to bail, we know we tried. + self.set_loaded(True) + if not hasattr(self.manager, 'get'): + return + + new = self.manager.get(self.id) + if new: + self._add_details(new._info) + # The 'request_ids' attribute has been added, + # so store the request id to it instead of _info + self.append_request_ids(new.request_ids) + + def __eq__(self, other): + if not isinstance(other, Resource): + return NotImplemented + # two resources of different types are not equal + if not isinstance(other, self.__class__): + return False + if hasattr(self, 'id') and hasattr(other, 'id'): + return self.id == other.id + return self._info == other._info + + def __ne__(self, other): + # Using not of '==' implementation because the not of + # __eq__, when it returns NotImplemented, is returning False. + return not self == other + + def is_loaded(self): + return self._loaded + + def set_loaded(self, val): + self._loaded = val + + def set_info(self, key, value): + self._info[key] = value + + def to_dict(self): + return copy.deepcopy(self._info) + + +class Manager(HookableMixin): + """Manager for API service. + Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. """ resource_class = None + cache_lock = threading.RLock() def __init__(self, api): self.api = api - def _list(self, url, response_key, obj_class=None, body=None): + @property + def client(self): + return self.api.client + + @property + def api_version(self): + return self.api.api_version + + 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) + resp, body = self.api.client.post(url, body=body) else: - _resp, body = self.api.client.get(url) + resp, body = self.api.client.get(url) if obj_class is None: obj_class = self.resource_class @@ -75,12 +266,26 @@ def _list(self, url, response_key, obj_class=None, body=None): with self.completion_cache('human_id', obj_class, mode="w"): with self.completion_cache('uuid', obj_class, mode="w"): - return [obj_class(self, res, loaded=True) - for res in data if res] + items = [obj_class(self, res, loaded=True) + for res in data if res] + return ListWithMeta(items, resp) + + @contextlib.contextmanager + def alternate_service_type(self, default, allowed_types=()): + original_service_type = self.api.client.service_type + if original_service_type in allowed_types: + yield + else: + self.api.client.service_type = default + try: + yield + finally: + self.api.client.service_type = original_service_type @contextlib.contextmanager def completion_cache(self, cache_type, obj_class, mode): - """ + """The completion cache for bash autocompletion. + The completion cache store items that can be used for bash autocompletion, like UUIDs or human-friendly IDs. @@ -91,88 +296,120 @@ def completion_cache(self, cache_type, obj_class, mode): Delete is not handled because listings are assumed to be performed often enough to keep the cache reasonably up-to-date. """ - base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', - default="~/.novaclient") - - # NOTE(sirp): Keep separate UUID caches for each username + endpoint - # pair - username = utils.env('OS_USERNAME', 'NOVA_USERNAME') - url = utils.env('OS_URL', 'NOVA_URL') - uniqifier = hashlib.md5(username + url).hexdigest() - - cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) - - try: - os.makedirs(cache_dir, 0755) - except OSError: - # NOTE(kiall): This is typicaly either permission denied while - # attempting to create the directory, or the directory - # already exists. Either way, don't fail. - pass - - resource = obj_class.__name__.lower() - filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-')) - path = os.path.join(cache_dir, filename) - - cache_attr = "_%s_cache" % cache_type - - try: - setattr(self, cache_attr, open(path, mode)) - except IOError: - # NOTE(kiall): This is typicaly a permission denied while - # attempting to write the cache file. - pass - - try: - yield - finally: - cache = getattr(self, cache_attr, None) - if cache: - cache.close() - delattr(self, cache_attr) + # NOTE(wryan): This lock protects read and write access to the + # completion caches + with self.cache_lock: + base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR', + default="~/.novaclient") + + # NOTE(sirp): Keep separate UUID caches for each username + + # endpoint pair + username = utils.env('OS_USERNAME', 'NOVA_USERNAME') + url = utils.env('OS_URL', 'NOVA_URL') + uniqifier = hashlib.sha256(username.encode('utf-8') + + url.encode('utf-8')).hexdigest() + + cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) + + try: + os.makedirs(cache_dir, 0o755) + except OSError: + # NOTE(kiall): This is typically either permission denied while + # attempting to create the directory, or the + # directory already exists. Either way, don't + # fail. + pass + + resource = obj_class.__name__.lower() + filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-')) + path = os.path.join(cache_dir, filename) + + cache_attr = "_%s_cache" % cache_type + + try: + setattr(self, cache_attr, open(path, mode)) + except IOError: + # NOTE(kiall): This is typically a permission denied while + # attempting to write the cache file. + pass + + try: + yield + finally: + cache = getattr(self, cache_attr, None) + if cache: + cache.close() + delattr(self, cache_attr) def write_to_completion_cache(self, cache_type, val): cache = getattr(self, "_%s_cache" % cache_type, None) if cache: cache.write("%s\n" % val) - def _get(self, url, response_key=None): - _resp, body = self.api.client.get(url) - if response_key: - return self.resource_class(self, body[response_key], loaded=True) + 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] else: - return self.resource_class(self, body, loaded=True) + content = body + 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) + resp, body = self.api.client.post(url, body=body) if return_raw: - return body[response_key] + return self.convert_into_with_meta(body[response_key], resp) + + if obj_class is None: + obj_class = self.resource_class - 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]) + 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) + resp, body = self.api.client.delete(url) + return self.convert_into_with_meta(body, resp) - def _update(self, url, body, **kwargs): + def _update(self, url, body, response_key=None, **kwargs): self.run_hooks('modify_body_for_update', body, **kwargs) - _resp, body = self.api.client.put(url, body=body) - return body + resp, body = self.api.client.put(url, body=body) + if body: + if response_key: + return self.resource_class(self, body[response_key], resp=resp) + else: + return self.resource_class(self, body, resp=resp) + else: + return StrWithMeta(body, resp) + + def convert_into_with_meta(self, item, resp): + if isinstance(item, str): + return StrWithMeta(item, resp) + elif isinstance(item, bytes): + return BytesWithMeta(item, resp) + elif isinstance(item, list): + return ListWithMeta(item, resp) + elif isinstance(item, tuple): + return TupleWithMeta(item, resp) + elif item is None: + return TupleWithMeta((), resp) + else: + return DictWithMeta(item, resp) -class ManagerWithFind(Manager): - """ - Like a `Manager`, but with additional `find()`/`findall()` methods. - """ - def find(self, **kwargs): - """ - Find a single item with attributes matching ``**kwargs``. +class ManagerWithFind(Manager, metaclass=abc.ABCMeta): + """Like a `Manager`, but with additional `find()`/`findall()` methods.""" - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ + @abc.abstractmethod + def list(self): + pass + + def find(self, **kwargs): + """Find a single item with attributes matching ``**kwargs``.""" matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: @@ -181,177 +418,147 @@ def find(self, **kwargs): elif num_matches > 1: raise exceptions.NoUniqueMatch else: + matches[0].append_request_ids(matches.request_ids) return matches[0] def findall(self, **kwargs): - """ - Find all items with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - found = [] + """Find all items with attributes matching ``**kwargs``.""" + found = ListWithMeta([], None) searches = kwargs.items() - for obj in self.list(): + detailed = True + list_kwargs = {} + + list_argspec = reflection.get_callable_args(self.list) + if 'detailed' in list_argspec: + detailed = ("human_id" not in kwargs and + "name" not in kwargs and + "display_name" not in kwargs) + list_kwargs['detailed'] = detailed + + if 'is_public' in list_argspec and 'is_public' in kwargs: + is_public = kwargs['is_public'] + list_kwargs['is_public'] = is_public + if is_public is None: + tmp_kwargs = kwargs.copy() + del tmp_kwargs['is_public'] + searches = tmp_kwargs.items() + + if 'search_opts' in list_argspec: + # pass search_opts in to do server side based filtering. + # TODO(jogo) not all search_opts support regex, find way to + # identify when to use regex and when to use string matching. + # volumes does not support regex while servers does. So when + # doing findall on servers some client side filtering is still + # needed. + if "human_id" in kwargs: + list_kwargs['search_opts'] = {"name": kwargs["human_id"]} + elif "name" in kwargs: + list_kwargs['search_opts'] = {"name": kwargs["name"]} + elif "display_name" in kwargs: + list_kwargs['search_opts'] = {"name": kwargs["display_name"]} + if "all_tenants" in kwargs: + all_tenants = kwargs['all_tenants'] + list_kwargs['search_opts']['all_tenants'] = all_tenants + searches = [(k, v) for k, v in searches if k != 'all_tenants'] + if "deleted" in kwargs: + deleted = kwargs['deleted'] + list_kwargs['search_opts']['deleted'] = deleted + searches = [(k, v) for k, v in searches if k != 'deleted'] + + listing = self.list(**list_kwargs) + found.append_request_ids(listing.request_ids) + + for obj in listing: try: if all(getattr(obj, attr) == value - for (attr, value) in searches): - found.append(obj) + for (attr, value) in searches): + if detailed: + found.append(obj) + else: + detail = self.get(obj.id) + found.append(detail) + found.append_request_ids(detail.request_ids) except AttributeError: continue return found - def list(self): - raise NotImplementedError - class BootingManagerWithFind(ManagerWithFind): """Like a `ManagerWithFind`, but has the ability to boot servers.""" - def _boot(self, resource_url, response_key, name, image, flavor, - ipgroup=None, meta=None, files=None, - reservation_id=None, return_raw=False, min_count=None, - max_count=None, **kwargs): - """ - Create (boot) a new server. - - :param name: Something to name the server. - :param image: The :class:`Image` to boot with. - :param flavor: The :class:`Flavor` to boot onto. - :param ipgroup: An initial :class:`IPGroup` for this server. - :param meta: A dict of arbitrary key/value metadata to store for this - server. A maximum of five entries is allowed, and both - keys and values must be 255 characters or less. - :param files: A dict of files to overrwrite 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: a UUID for the set of servers being requested. - :param return_raw: If True, don't try to coearse the result into - a Resource object. - """ - body = {"server": { - "name": name, - "imageId": getid(image), - "flavorId": getid(flavor), - }} - if ipgroup: - body["server"]["sharedIpGroupId"] = getid(ipgroup) - if meta: - body["server"]["metadata"] = meta - if reservation_id: - body["server"]["reservation_id"] = reservation_id - - if not min_count: - min_count = 1 - if not max_count: - max_count = min_count - body["server"]["min_count"] = min_count - body["server"]["max_count"] = max_count - - # Files are a slight bit tricky. They're passed in a "personality" - # list to the POST. Each item is a dict giving a file name and the - # base64-encoded contents of the file. We want to allow passing - # either an open file *or* some contents as files here. - if files: - personality = body['server']['personality'] = [] - for filepath, file_or_string in files.items(): - if hasattr(file_or_string, 'read'): - data = file_or_string.read() - else: - data = file_or_string - personality.append({ - 'path': filepath, - 'contents': data.encode('base64'), - }) - return self._create(resource_url, body, response_key, - return_raw=return_raw, **kwargs) + def _parse_block_device_mapping(self, block_device_mapping): + """Parses legacy block device mapping.""" + # FIXME(andreykurilin): make it work with block device mapping v2 + bdm = [] -class Resource(object): - """ - A resource represents a particular instance of an object (server, flavor, - etc). This is pretty much just a bag for attributes. + for device_name, mapping in block_device_mapping.items(): + # + # The mapping is in the format: + # :[]:[]:[] + # + bdm_dict = {'device_name': device_name} - :param manager: Manager object - :param info: dictionary representing resource attributes - :param loaded: prevent lazy-loading if set to True - """ - HUMAN_ID = False - NAME_ATTR = 'name' + mapping_parts = mapping.split(':') + source_id = mapping_parts[0] - def __init__(self, manager, info, loaded=False): - self.manager = manager - self._info = info - self._add_details(info) - self._loaded = loaded + if len(mapping_parts) == 1: + bdm_dict['volume_id'] = source_id + elif len(mapping_parts) > 1: + source_type = mapping_parts[1] + if source_type.startswith('snap'): + bdm_dict['snapshot_id'] = source_id + else: + bdm_dict['volume_id'] = source_id - # NOTE(sirp): ensure `id` is already present because if it isn't we'll - # enter an infinite loop of __getattr__ -> get -> __init__ -> - # __getattr__ -> ... - if 'id' in self.__dict__ and len(str(self.id)) == 36: - self.manager.write_to_completion_cache('uuid', self.id) + if len(mapping_parts) > 2 and mapping_parts[2]: + bdm_dict['volume_size'] = str(int(mapping_parts[2])) - human_id = self.human_id - if human_id: - self.manager.write_to_completion_cache('human_id', human_id) + if len(mapping_parts) > 3: + bdm_dict['delete_on_termination'] = mapping_parts[3] - @property - def human_id(self): - """Subclasses may override this provide a pretty ID which can be used - for bash completion. - """ - if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: - return utils.slugify(getattr(self, self.NAME_ATTR)) - return None + bdm.append(bdm_dict) + return bdm - def _add_details(self, info): - for (k, v) in info.iteritems(): - try: - setattr(self, k, v) - except AttributeError: - # In this case we already defined the attribute on the class - pass - def __getattr__(self, k): - if k not in self.__dict__: - #NOTE(bcwaldon): disallow lazy-loading if already loaded once - if not self.is_loaded(): - self.get() - return self.__getattr__(k) +class ListWithMeta(list, RequestIdMixin): + def __init__(self, values, resp): + super(ListWithMeta, self).__init__(values) + self.request_ids_setup() + self.append_request_ids(resp) - raise AttributeError(k) - else: - return self.__dict__[k] - def __repr__(self): - reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and - k != 'manager') - info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) - return "<%s %s>" % (self.__class__.__name__, info) +class DictWithMeta(dict, RequestIdMixin): + def __init__(self, values, resp): + super(DictWithMeta, self).__init__(values) + self.request_ids_setup() + self.append_request_ids(resp) - def get(self): - # set_loaded() first ... so if we have to bail, we know we tried. - self.set_loaded(True) - if not hasattr(self.manager, 'get'): - return - new = self.manager.get(self.id) - if new: - self._add_details(new._info) +class TupleWithMeta(tuple, RequestIdMixin): + def __new__(cls, values, resp): + return super(TupleWithMeta, cls).__new__(cls, values) - def __eq__(self, other): - if not isinstance(other, self.__class__): - return False - if hasattr(self, 'id') and hasattr(other, 'id'): - return self.id == other.id - return self._info == other._info + def __init__(self, values, resp): + self.request_ids_setup() + self.append_request_ids(resp) - def is_loaded(self): - return self._loaded - def set_loaded(self, val): - self._loaded = val +class StrWithMeta(str, RequestIdMixin): + def __new__(cls, value, resp): + return super(StrWithMeta, cls).__new__(cls, value) + + def __init__(self, values, resp): + self.request_ids_setup() + self.append_request_ids(resp) + + +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) diff --git a/novaclient/client.py b/novaclient/client.py index f66dcfe5c..369d8d08e 100644 --- a/novaclient/client.py +++ b/novaclient/client.py @@ -1,146 +1,89 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # Copyright 2011 Piston Cloud Computing, Inc. # All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + """ OpenStack Client interface. Handles the REST calls and responses. """ -import logging -import os -import time -import urlparse +import itertools +import pkgutil +import warnings -import httplib2 -import pkg_resources +from keystoneauth1 import adapter +from keystoneauth1 import identity +from keystoneauth1 import session as ksession +from oslo_utils import importutils +import stevedore -try: - import json -except ImportError: - import simplejson as json +import novaclient +from novaclient import api_versions +from novaclient import exceptions +from novaclient import extension as ext +from novaclient.i18n import _ +from novaclient import utils -has_keyring = False -try: - import keyring - has_keyring = True -except ImportError: - pass +osprofiler_profiler = importutils.try_import("osprofiler.profiler") +osprofiler_web = importutils.try_import("osprofiler.web") -# Python 2.5 compat fix -if not hasattr(urlparse, 'parse_qsl'): - import cgi - urlparse.parse_qsl = cgi.parse_qsl -from novaclient import exceptions -from novaclient import service_catalog -from novaclient import utils +class SessionClient(adapter.LegacyJsonAdapter): + client_name = 'python-novaclient' + client_version = novaclient.__version__ -def get_auth_system_url(auth_system): - """Load plugin-based auth_url""" - ep_name = 'openstack.client.auth_url' - for ep in pkg_resources.iter_entry_points(ep_name): - if ep.name == auth_system: - return ep.load()() - raise exceptions.AuthSystemNotFound(auth_system) + def __init__(self, *args, **kwargs): + self.times = [] + 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) -def _get_proxy_info(): - """Work around httplib2 proxying bug. + api_versions.check_version(self.api_version) - Full details of the bug here: + super(SessionClient, self).__init__(*args, **kwargs) - http://code.google.com/p/httplib2/issues/detail?id=228 + def request(self, url, method, **kwargs): + kwargs.setdefault('headers', kwargs.get('headers', {})) + api_versions.update_headers(kwargs["headers"], self.api_version) + + # NOTE(dbelova): osprofiler_web.get_trace_id_headers does not add any + # headers in case if osprofiler is not initialized. + if osprofiler_web: + kwargs['headers'].update(osprofiler_web.get_trace_id_headers()) + + # NOTE(jamielennox): The standard call raises errors from + # keystoneauth1, where we need to raise the novaclient errors. + raise_exc = kwargs.pop('raise_exc', True) + with utils.record_time(self.times, self.timings, method, url): + resp, body = super(SessionClient, self).request(url, + method, + raise_exc=False, + **kwargs) + + # TODO(andreykurilin): uncomment this line, when we will be able to + # check only nova-related calls + # api_versions.check_headers(resp, self.api_version) + if raise_exc and resp.status_code >= 400: + raise exceptions.from_response(resp, body, url, method) - Basically, in the case of plain old http with httplib2>=0.7.5 we - want to ensure that PROXY_TYPE_HTTP_NO_TUNNEL is used. - """ - def get_proxy_info(method): - pi = httplib2.ProxyInfo.from_environment(method) - if pi is None or method != 'http': - return pi - - # We can't rely on httplib2.socks being available - # PROXY_TYPE_HTTP_NO_TUNNEL was introduced in 0.7.5 - if not (hasattr(httplib2, 'socks') and - hasattr(httplib2.socks, 'PROXY_TYPE_HTTP_NO_TUNNEL')): - return pi - - pi.proxy_type = httplib2.socks.PROXY_TYPE_HTTP_NO_TUNNEL - return pi - - # 0.7.3 introduced configuring proxy from the environment - if not hasattr(httplib2.ProxyInfo, 'from_environment'): - return None - - return get_proxy_info - - -class HTTPClient(httplib2.Http): - - USER_AGENT = 'python-novaclient' - - def __init__(self, user, password, projectid, auth_url=None, - insecure=False, timeout=None, proxy_tenant_id=None, - proxy_token=None, region_name=None, - endpoint_type='publicURL', service_type=None, - service_name=None, volume_service_name=None, - timings=False, bypass_url=None, no_cache=False, - http_log_debug=False, auth_system='keystone'): - super(HTTPClient, self).__init__(timeout=timeout, - proxy_info=_get_proxy_info()) - self.user = user - self.password = password - self.projectid = projectid - if not auth_url and auth_system and auth_system != 'keystone': - auth_url = get_auth_system_url(auth_system) - if not auth_url: - raise exceptions.EndpointNotFound() - self.auth_url = auth_url.rstrip('/') - self.version = 'v1.1' - self.region_name = region_name - self.endpoint_type = endpoint_type - self.service_type = service_type - self.service_name = service_name - self.volume_service_name = volume_service_name - self.timings = timings - self.bypass_url = bypass_url - self.no_cache = no_cache - self.http_log_debug = http_log_debug - - self.times = [] # [("item", starttime, endtime), ...] - - self.management_url = None - self.auth_token = None - self.proxy_token = proxy_token - self.proxy_tenant_id = proxy_tenant_id - self.used_keyring = False - - # httplib2 overrides - self.force_exception_to_status_code = True - self.disable_ssl_certificate_validation = insecure - - self.auth_system = auth_system - - self._logger = logging.getLogger(__name__) - if self.http_log_debug: - ch = logging.StreamHandler() - self._logger.setLevel(logging.DEBUG) - self._logger.addHandler(ch) - - def use_token_cache(self, use_it): - # One day I'll stop using negative naming. - self.no_cache = not use_it - - def unauthenticate(self): - """Forget all of our authentication information.""" - self.management_url = None - self.auth_token = None - self.used_keyring = False - - def set_management_url(self, url): - self.management_url = url + return resp, body def get_timings(self): return self.times @@ -148,328 +91,230 @@ def get_timings(self): def reset_timings(self): self.times = [] - def http_log_req(self, args, kwargs): - if not self.http_log_debug: - return - string_parts = ['curl -i'] - for element in args: - if element in ('GET', 'POST', 'DELETE', 'PUT'): - string_parts.append(' -X %s' % element) - else: - string_parts.append(' %s' % element) +def _construct_http_client(api_version=None, + auth=None, + auth_token=None, + auth_url=None, + cacert=None, + cert=None, + endpoint_override=None, + endpoint_type='publicURL', + http_log_debug=False, + insecure=False, + logger=None, + os_cache=False, + password=None, + project_domain_id=None, + project_domain_name=None, + project_id=None, + project_name=None, + region_name=None, + service_name=None, + service_type='compute', + session=None, + timeout=None, + timings=False, + user_agent='python-novaclient', + user_domain_id=None, + user_domain_name=None, + user_id=None, + username=None, + **kwargs): + if not session: + if not auth and auth_token: + auth = identity.Token(auth_url=auth_url, + token=auth_token, + project_id=project_id, + project_name=project_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name) + elif not auth: + auth = identity.Password(username=username, + user_id=user_id, + password=password, + project_id=project_id, + project_name=project_name, + auth_url=auth_url, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name) + session = ksession.Session(auth=auth, + verify=(cacert or not insecure), + timeout=timeout, + cert=cert, + user_agent=user_agent) + + return SessionClient(api_version=api_version, + auth=auth, + endpoint_override=endpoint_override, + interface=endpoint_type, + logger=logger, + region_name=region_name, + service_name=service_name, + service_type=service_type, + session=session, + timings=timings, + user_agent=user_agent, + **kwargs) + + +def discover_extensions(*args, **kwargs): + """Returns the list of extensions, which can be discovered by python path + and by entry-point 'novaclient.extension'. + """ + chain = itertools.chain(_discover_via_python_path(), + _discover_via_entry_points()) + return [ext.Extension(name, module) for name, module in chain] - for element in kwargs['headers']: - header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) - string_parts.append(header) - if 'body' in kwargs: - string_parts.append(" -d '%s'" % (kwargs['body'])) - self._logger.debug("\nREQ: %s\n" % "".join(string_parts)) +def _discover_via_python_path(): + for (module_loader, name, _ispkg) in pkgutil.iter_modules(): + if name.endswith('_python_novaclient_ext'): + # NOTE(sdague): needed for python 2.x compatibility. + if not hasattr(module_loader, 'load_module'): + module_loader = module_loader.find_module(name) + module = module_loader.load_module(name) + if hasattr(module, 'extension_name'): + name = module.extension_name - def http_log_resp(self, resp, body): - if not self.http_log_debug: - return - self._logger.debug("RESP:%s %s\n", resp, body) + yield name, module - def request(self, *args, **kwargs): - kwargs.setdefault('headers', kwargs.get('headers', {})) - kwargs['headers']['User-Agent'] = self.USER_AGENT - kwargs['headers']['Accept'] = 'application/json' - if 'body' in kwargs: - kwargs['headers']['Content-Type'] = 'application/json' - kwargs['body'] = json.dumps(kwargs['body']) - - self.http_log_req(args, kwargs) - resp, body = super(HTTPClient, self).request(*args, **kwargs) - self.http_log_resp(resp, body) - - if body: - # NOTE(alaski): Because force_exceptions_to_status_code=True - # httplib2 returns a connection refused event as a 400 response. - # To determine if it is a bad request or refused connection we need - # to check the body. httplib2 tests check for 'Connection refused' - # or 'actively refused' in the body, so that's what we'll do. - if resp.status == 400: - if 'Connection refused' in body or 'actively refused' in body: - raise exceptions.ConnectionRefused(body) - try: - body = json.loads(body) - except ValueError: - pass - else: - body = None - if resp.status >= 400: - raise exceptions.from_response(resp, body) +def _make_discovery_manager(): + # This function provides a place to mock out the entry point scan + return stevedore.ExtensionManager('novaclient.extension') - return resp, body - def _time_request(self, url, method, **kwargs): - start_time = time.time() - resp, body = self.request(url, method, **kwargs) - self.times.append(("%s %s" % (method, url), - start_time, time.time())) - return resp, body +def _discover_via_entry_points(): + mgr = _make_discovery_manager() + for extension in mgr: + yield extension.name, extension.plugin - def _cs_request(self, url, method, **kwargs): - if not self.management_url: - self.authenticate() - - # Perform the request once. If we get a 401 back then it - # might be because the auth token expired, so try to - # re-authenticate and try again. If it still fails, bail. - try: - kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token - if self.projectid: - kwargs['headers']['X-Auth-Project-Id'] = self.projectid - - resp, body = self._time_request(self.management_url + url, method, - **kwargs) - return resp, body - except exceptions.Unauthorized, ex: - try: - self.authenticate() - kwargs['headers']['X-Auth-Token'] = self.auth_token - resp, body = self._time_request(self.management_url + url, - method, **kwargs) - return resp, body - except exceptions.Unauthorized: - raise ex - - def get(self, url, **kwargs): - return self._cs_request(url, 'GET', **kwargs) - - def post(self, url, **kwargs): - return self._cs_request(url, 'POST', **kwargs) - - def put(self, url, **kwargs): - return self._cs_request(url, 'PUT', **kwargs) - - def delete(self, url, **kwargs): - return self._cs_request(url, 'DELETE', **kwargs) - - def _extract_service_catalog(self, url, resp, body, extract_token=True): - """See what the auth service told us and process the response. - We may get redirected to another site, fail or actually get - back a service catalog with a token and our endpoints.""" - - if resp.status == 200: # content must always present - try: - self.auth_url = url - self.service_catalog = \ - service_catalog.ServiceCatalog(body) - if extract_token: - self.auth_token = self.service_catalog.get_token() - - management_url = self.service_catalog.url_for( - attr='region', - filter_value=self.region_name, - endpoint_type=self.endpoint_type, - service_type=self.service_type, - service_name=self.service_name, - volume_service_name=self.volume_service_name,) - self.management_url = management_url.rstrip('/') - return None - except exceptions.AmbiguousEndpoints: - print "Found more than one valid endpoint. Use a more " \ - "restrictive filter" - raise - except KeyError: - raise exceptions.AuthorizationFailure() - except exceptions.EndpointNotFound: - print "Could not find any suitable endpoint. Correct region?" - raise - - elif resp.status == 305: - return resp['location'] - else: - raise exceptions.from_response(resp, body) - - def _fetch_endpoints_from_auth(self, url): - """We have a token, but don't know the final endpoint for - the region. We have to go back to the auth service and - ask again. This request requires an admin-level token - to work. The proxy token supplied could be from a low-level enduser. - - We can't get this from the keystone service endpoint, we have to use - the admin endpoint. - - This will overwrite our admin token with the user token. - """ - - # GET ...:5001/v2.0/tokens/#####/endpoints - url = '/'.join([url, 'tokens', '%s?belongsTo=%s' - % (self.proxy_token, self.proxy_tenant_id)]) - self._logger.debug("Using Endpoint URL: %s" % url) - resp, body = self._time_request( - url, "GET", headers={'X-Auth_Token': self.auth_token}) - return self._extract_service_catalog(url, resp, body, - extract_token=False) - - def authenticate(self): - if has_keyring: - keys = [self.auth_url, self.projectid, self.user, self.region_name, - self.endpoint_type, self.service_type, self.service_name, - self.volume_service_name] - for index, key in enumerate(keys): - if key is None: - keys[index] = '?' - keyring_key = "/".join(keys) - if not self.no_cache and not self.used_keyring: - # Lookup the token/mgmt url from the keyring first time - # through. - # If we come through again, it's because the old token - # was rejected. - try: - block = keyring.get_password("novaclient_auth", - keyring_key) - if block: - self.used_keyring = True - self.auth_token, self.management_url = block.split('|') - return - except Exception: - pass - - magic_tuple = urlparse.urlsplit(self.auth_url) - scheme, netloc, path, query, frag = magic_tuple - port = magic_tuple.port - if port is None: - port = 80 - path_parts = path.split('/') - for part in path_parts: - if len(part) > 0 and part[0] == 'v': - self.version = part - break - - # TODO(sandy): Assume admin endpoint is 35357 for now. - # Ideally this is going to have to be provided by the service catalog. - new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,)) - admin_url = urlparse.urlunsplit( - (scheme, new_netloc, path, query, frag)) - - # FIXME(chmouel): This is to handle backward compatibiliy when - # we didn't have a plugin mechanism for the auth_system. This - # should be removed in the future and have people move to - # OS_AUTH_SYSTEM=rackspace instead. - if "NOVA_RAX_AUTH" in os.environ: - self.auth_system = "rackspace" - - auth_url = self.auth_url - if self.version == "v2.0": # FIXME(chris): This should be better. - while auth_url: - if not self.auth_system or self.auth_system == 'keystone': - auth_url = self._v2_auth(auth_url) - else: - auth_url = self._plugin_auth(auth_url) - - # Are we acting on behalf of another user via an - # existing token? If so, our actual endpoints may - # be different than that of the admin token. - if self.proxy_token: - self._fetch_endpoints_from_auth(admin_url) - # Since keystone no longer returns the user token - # with the endpoints any more, we need to replace - # our service account token with the user token. - self.auth_token = self.proxy_token - else: - try: - while auth_url: - auth_url = self._v1_auth(auth_url) - # In some configurations nova makes redirection to - # v2.0 keystone endpoint. Also, new location does not contain - # real endpoint, only hostname and port. - except exceptions.AuthorizationFailure: - if auth_url.find('v2.0') < 0: - auth_url = auth_url + '/v2.0' - self._v2_auth(auth_url) - - if self.bypass_url: - self.set_management_url(self.bypass_url) - - # Store the token/mgmt url in the keyring for later requests. - if has_keyring and not self.no_cache: - try: - keyring_value = "%s|%s" % (self.auth_token, - self.management_url) - keyring.set_password("novaclient_auth", - keyring_key, keyring_value) - except Exception: - pass - - def _v1_auth(self, url): - if self.proxy_token: - raise exceptions.NoTokenLookupException() - - headers = {'X-Auth-User': self.user, - 'X-Auth-Key': self.password} - if self.projectid: - headers['X-Auth-Project-Id'] = self.projectid - - resp, body = self._time_request(url, 'GET', headers=headers) - if resp.status in (200, 204): # in some cases we get No Content - try: - mgmt_header = 'x-server-management-url' - self.management_url = resp[mgmt_header].rstrip('/') - self.auth_token = resp['x-auth-token'] - self.auth_url = url - except KeyError: - raise exceptions.AuthorizationFailure() - elif resp.status == 305: - return resp['location'] - else: - raise exceptions.from_response(resp, body) - def _plugin_auth(self, auth_url): - """Load plugin-based authentication""" - ep_name = 'openstack.client.authenticate' - for ep in pkg_resources.iter_entry_points(ep_name): - if ep.name == self.auth_system: - return ep.load()(self, auth_url) - raise exceptions.AuthSystemNotFound(self.auth_system) +def _get_client_class_and_version(version): + if not isinstance(version, api_versions.APIVersion): + version = api_versions.get_api_version(version) + else: + api_versions.check_major_version(version) + if version.is_latest(): + raise exceptions.UnsupportedVersion( + _("The version should be explicit, not latest.")) + return version, importutils.import_class( + "novaclient.v%s.client.Client" % version.ver_major) - def _v2_auth(self, url): - """Authenticate against a v2.0 auth service.""" - body = {"auth": { - "passwordCredentials": {"username": self.user, - "password": self.password}}} - if self.projectid: - body['auth']['tenantName'] = self.projectid +def _check_arguments(kwargs, release, deprecated_name, right_name=None): + """Process deprecation of arguments. - self._authenticate(url, body) + Checks presence of deprecated argument in kwargs, prints proper warning + message, renames key to right one it needed. + """ + if deprecated_name in kwargs: + if right_name: + if right_name in kwargs: + msg = _("The '%(old)s' argument is deprecated in " + "%(release)s and its use may result in errors " + "in future releases. As '%(new)s' is provided, " + "the '%(old)s' argument will be ignored.") % { + "old": deprecated_name, "release": release, + "new": right_name} + kwargs.pop(deprecated_name) + else: + msg = _("The '%(old)s' argument is deprecated in " + "%(release)s and its use may result in errors in " + "future releases. Use '%(right)s' instead.") % { + "old": deprecated_name, "release": release, + "right": right_name} + kwargs[right_name] = kwargs.pop(deprecated_name) - def _authenticate(self, url, body): - """Authenticate and extract the service catalog.""" - token_url = url + "/tokens" + else: + msg = _("The '%(old)s' argument is deprecated in %(release)s " + "and its use may result in errors in future " + "releases.") % { + "old": deprecated_name, "release": release} + # just ignore it + kwargs.pop(deprecated_name) - # Make sure we follow redirects when trying to reach Keystone - tmp_follow_all_redirects = self.follow_all_redirects - self.follow_all_redirects = True + warnings.warn(msg) - try: - resp, body = self._time_request(token_url, "POST", body=body) - finally: - self.follow_all_redirects = tmp_follow_all_redirects - return self._extract_service_catalog(url, resp, body) +def Client(version, username=None, password=None, project_id=None, + auth_url=None, **kwargs): + """Initialize client object based on given version. + HOW-TO: + The simplest way to create a client instance is initialization with your + credentials:: -def get_client_class(version): - version_map = { - '1.1': 'novaclient.v1_1.client.Client', - '2': 'novaclient.v1_1.client.Client', - } - try: - client_path = version_map[str(version)] - except (KeyError, ValueError): - msg = "Invalid client version '%s'. must be one of: %s" % ( - (version, ', '.join(version_map.keys()))) - raise exceptions.UnsupportedVersion(msg) + >>> from novaclient import client + >>> nova = client.Client(VERSION, USERNAME, PASSWORD, + ... PROJECT_ID, AUTH_URL) - return utils.import_class(client_path) + Here ``VERSION`` can be a string or + ``novaclient.api_versions.APIVersion`` obj. If you prefer string value, + you can use ``1.1`` (deprecated now), ``2`` or ``2.X`` + (where X is a microversion). -def Client(version, *args, **kwargs): - client_class = get_client_class(version) - return client_class(*args, **kwargs) + Alternatively, you can create a client instance using the keystoneauth + session API. See "The novaclient Python API" page at + python-novaclient's doc. + """ + if password: + kwargs["password"] = password + if project_id: + kwargs["project_id"] = project_id + + _check_arguments(kwargs, "Ocata", "auth_plugin") + _check_arguments(kwargs, "Ocata", "auth_system") + if "no_cache" in kwargs: + _check_arguments(kwargs, "Ocata", "no_cache", right_name="os_cache") + # os_cache is not a fully compatible with no_cache, so we need to + # apply this custom processing + kwargs["os_cache"] = not kwargs["os_cache"] + _check_arguments(kwargs, "Ocata", "bypass_url", + right_name="endpoint_override") + _check_arguments(kwargs, "Ocata", "api_key", right_name="password") + # NOTE(andreykurilin): OpenStack projects use two variables with one + # meaning: 'endpoint_type' and 'interface'. 'endpoint_type' is an old + # name which was used by most OpenStack clients. Later it was replaced by + # 'interface' in keystone and later some other clients switched to new + # variable name too. In case of novaclient, there is no need to switch to + # 'interface' variable name due too several reasons: + # - novaclient uses 'endpoint_type' variable name long time ago and + # there is no real reasons to switch to new name; + # - 'interface' argument is used in several shell subcommands + # (for example in `nova floating-ip-bulk-create`), so we will need to + # modify these subcommands to not conflict with global flag + # 'interface' + # Actually, novaclient did not accept 'interface' before, but since we + # allow additional arguments(kwargs), someone can use this variable name + # and face issue about unexpected behavior. + _check_arguments(kwargs, "Ocata", "interface", right_name="endpoint_type") + _check_arguments(kwargs, "Ocata", "tenant_name", right_name="project_name") + _check_arguments(kwargs, "Ocata", "tenant_id", right_name="project_id") + _check_arguments(kwargs, "Ocata", "proxy_tenant_id") + _check_arguments(kwargs, "Ocata", "proxy_token") + _check_arguments(kwargs, "Ocata", "connection_pool") + _check_arguments(kwargs, "Ocata", "volume_service_name") + + api_version, client_class = _get_client_class_and_version(version) + kwargs.pop("direct_use", None) + + profile = kwargs.pop("profile", None) + if osprofiler_profiler and profile: + # Initialize the root of the future trace: the created trace ID will + # be used as the very first parent to which all related traces will be + # bound to. The given HMAC key must correspond to the one set in + # nova-api nova.conf, otherwise the latter will fail to check the + # request signature and will skip initialization of osprofiler on + # the server side. + osprofiler_profiler.init(profile) + + return client_class(api_version=api_version, + auth_url=auth_url, + direct_use=False, + username=username, + **kwargs) diff --git a/novaclient/crypto.py b/novaclient/crypto.py new file mode 100644 index 000000000..f6d77fa75 --- /dev/null +++ b/novaclient/crypto.py @@ -0,0 +1,41 @@ +# Copyright 2013 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import base64 +import subprocess # nosec: B404 + + +class DecryptionFailure(Exception): + pass + + +def decrypt_password(private_key, password): + """Base64 decodes password and unencrypts it with private key. + + Requires openssl binary available in the path. + """ + unencoded = base64.b64decode(password) + cmd = ['openssl', 'rsautl', '-decrypt', '-inkey', private_key] + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, + stdout=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 da6f8d4c8..4c0a5d8df 100644 --- a/novaclient/exceptions.py +++ b/novaclient/exceptions.py @@ -1,4 +1,17 @@ # Copyright 2010 Jacob Kaplan-Moss +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + """ Exception definitions. """ @@ -6,71 +19,87 @@ class UnsupportedVersion(Exception): """Indicates that the user is trying to use an unsupported - version of the API""" + version of the API. + """ pass -class CommandError(Exception): - pass +class UnsupportedConsoleType(Exception): + """Indicates that the user is trying to use an unsupported + console type when retrieving console urls of servers. + """ + def __init__(self, console_type): + self.message = 'Unsupported console_type "%s"' % console_type -class AuthorizationFailure(Exception): - pass +class UnsupportedAttribute(AttributeError): + """Indicates that the user is trying to transmit the argument to a method, + which is not supported by selected version. + """ + def __init__(self, argument_name, start_version, end_version=None): + if end_version: + self.message = ( + "'%(name)s' argument is only allowed for microversions " + "%(start)s - %(end)s." % {"name": argument_name, + "start": start_version, + "end": end_version}) + else: + 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 NoUniqueMatch(Exception): - pass +class CommandError(Exception): + pass -class AuthSystemNotFound(Exception): - """When the user specify a AuthSystem but not installed.""" - def __init__(self, auth_system): - self.auth_system = auth_system - def __str__(self): - return "AuthSystemNotFound: %s" % repr(self.auth_system) +class NoUniqueMatch(Exception): + pass -class NoTokenLookupException(Exception): - """This form of authentication does not support looking up - endpoints from an existing token.""" - pass +class ResourceInErrorState(Exception): + """Resource is in the error state.""" + def __init__(self, obj): + msg = "`%s` resource is in the error state" % obj.__class__.__name__ + fault_msg = getattr(obj, "fault", {}).get("message") + if fault_msg: + msg += "due to '%s'" % fault_msg + self.message = "%s." % msg -class EndpointNotFound(Exception): - """Could not find Service or Region in Service Catalog.""" - pass +class VersionNotFoundForAPIMethod(Exception): + msg_fmt = "API version '%(vers)s' is not supported on '%(method)s' method." -class AmbiguousEndpoints(Exception): - """Found more than one matching endpoint in Service Catalog.""" - def __init__(self, endpoints=None): - self.endpoints = endpoints + def __init__(self, version, method): + self.version = version + self.method = method def __str__(self): - return "AmbiguousEndpoints: %s" % repr(self.endpoints) - + return self.msg_fmt % {"vers": self.version, "method": self.method} -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 InstanceInDeletedState(Exception): + """Instance is in the deleted state.""" + pass class ClientException(Exception): """ The base exception class for all exceptions this library raises. """ - def __init__(self, code, message=None, details=None, request_id=None): + message = 'Unknown Error' + + def __init__(self, code, message=None, details=None, request_id=None, + url=None, method=None): self.code = code self.message = message or self.__class__.message self.details = details self.request_id = request_id + self.url = url + self.method = method def __str__(self): formatted_string = "%s (HTTP %s)" % (self.message, self.code) @@ -80,6 +109,19 @@ def __str__(self): return formatted_string +class RetryAfterException(ClientException): + """ + The base exception class for ClientExceptions that use Retry-After header. + """ + def __init__(self, *args, **kwargs): + try: + self.retry_after = int(kwargs.pop('retry_after')) + except (KeyError, ValueError): + self.retry_after = 0 + + super(RetryAfterException, self).__init__(*args, **kwargs) + + class BadRequest(ClientException): """ HTTP 400 - Bad request: you sent some malformed data. @@ -113,7 +155,31 @@ class NotFound(ClientException): message = "Not found" -class OverLimit(ClientException): +class MethodNotAllowed(ClientException): + """ + HTTP 405 - Method Not Allowed + """ + http_status = 405 + message = "Method Not Allowed" + + +class NotAcceptable(ClientException): + """ + HTTP 406 - Not Acceptable + """ + http_status = 406 + message = "Not Acceptable" + + +class Conflict(ClientException): + """ + HTTP 409 - Conflict + """ + http_status = 409 + message = "Conflict" + + +class OverLimit(RetryAfterException): """ HTTP 413 - Over limit: you're over the API limits for this time period. """ @@ -121,6 +187,14 @@ class OverLimit(ClientException): message = "Over limit" +class RateLimit(RetryAfterException): + """ + HTTP 429 - Rate limit: you've sent too many requests for this time period. + """ + http_status = 429 + message = "Rate limit" + + # NotImplemented is a python keyword. class HTTPNotImplemented(ClientException): """ @@ -136,31 +210,76 @@ class HTTPNotImplemented(ClientException): # for c in ClientException.__subclasses__()) # # Instead, we have to hardcode it: -_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, - Forbidden, NotFound, OverLimit, HTTPNotImplemented]) +_error_classes = [BadRequest, Unauthorized, Forbidden, NotFound, + MethodNotAllowed, NotAcceptable, Conflict, OverLimit, + RateLimit, HTTPNotImplemented] +_code_map = dict((c.http_status, c) for c in _error_classes) + + +class InvalidUsage(RuntimeError): + """This function call is invalid in the way you are using this client. + + Due to the transition to using keystoneauth some function calls are no + longer available. You should make a similar call to the session object + instead. + """ + pass -def from_response(response, body): +def from_response(response, body, url, method=None): """ Return an instance of an ClientException or subclass - based on an httplib2 response. + based on a requests response. Usage:: - resp, body = http.request(...) - if resp.status != 200: - raise exception_from_response(resp, body) + resp, body = requests.request(...) + if resp.status_code != 200: + raise exception_from_response(resp, rest.text) """ - cls = _code_map.get(response.status, ClientException) - request_id = response.get('x-compute-request-id') + cls = _code_map.get(response.status_code, ClientException) + + kwargs = { + 'code': response.status_code, + 'method': method, + 'url': url, + 'request_id': None, + } + + if response.headers: + kwargs['request_id'] = response.headers.get('x-compute-request-id') + + if (issubclass(cls, RetryAfterException) and + 'retry-after' in response.headers): + kwargs['retry_after'] = response.headers.get('retry-after') + if body: message = "n/a" details = "n/a" + if hasattr(body, 'keys'): - error = body[body.keys()[0]] - message = error.get('message', None) - details = error.get('details', None) - return cls(code=response.status, message=message, details=details, - request_id=request_id) - else: - return cls(code=response.status, request_id=request_id) + # NOTE(mriedem): WebOb<1.6.0 will return a nested dict structure + # where the error keys to the message/details/code. WebOb>=1.6.0 + # returns just a response body as a single dict, not nested, + # so we have to handle both cases (since we can't trust what we're + # given with content_type: application/json either way. + if 'message' in body: + # WebOb 1.6.0 case + message = body.get('message') + details = body.get('details') + else: + # WebOb<1.6.0 where we assume there is a single error message + # key to the body that has the message and details. + error = body[list(body)[0]] + message = error.get('message') + details = error.get('details') + + kwargs['message'] = message + kwargs['details'] = details + + return cls(**kwargs) + + +class ResourceNotFound(Exception): + """Error in getting the resource.""" + pass diff --git a/novaclient/extension.py b/novaclient/extension.py index 7c91a8e89..e297a636f 100644 --- a/novaclient/extension.py +++ b/novaclient/extension.py @@ -1,4 +1,4 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -17,7 +17,7 @@ from novaclient import utils -class Extension(utils.HookableMixin): +class Extension(base.HookableMixin): """Extension descriptor.""" SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') diff --git a/novaclient/i18n.py b/novaclient/i18n.py new file mode 100644 index 000000000..21c0addaa --- /dev/null +++ b/novaclient/i18n.py @@ -0,0 +1,25 @@ +# 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. + +"""oslo_i18n integration module for novaclient. + +See https://docs.openstack.org/oslo.i18n/latest/user/usage.html . + +""" + +import oslo_i18n + + +_translators = oslo_i18n.TranslatorFactory(domain='novaclient') + +# The primary translation function using the well-known name "_" +_ = _translators.primary diff --git a/novaclient/openstack/common/setup.py b/novaclient/openstack/common/setup.py deleted file mode 100644 index e6f72f034..000000000 --- a/novaclient/openstack/common/setup.py +++ /dev/null @@ -1,366 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# 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. - -""" -Utilities with minimum-depends for use in setup.py -""" - -import datetime -import os -import re -import subprocess -import sys - -from setuptools.command import sdist - - -def parse_mailmap(mailmap='.mailmap'): - mapping = {} - if os.path.exists(mailmap): - with open(mailmap, 'r') as fp: - for l in fp: - l = l.strip() - if not l.startswith('#') and ' ' in l: - canonical_email, alias = [x for x in l.split(' ') - if x.startswith('<')] - mapping[alias] = canonical_email - return mapping - - -def canonicalize_emails(changelog, mapping): - """Takes in a string and an email alias mapping and replaces all - instances of the aliases in the string with their real email. - """ - for alias, email in mapping.iteritems(): - changelog = changelog.replace(alias, email) - return changelog - - -# Get requirements from the first file that exists -def get_reqs_from_files(requirements_files): - for requirements_file in requirements_files: - if os.path.exists(requirements_file): - with open(requirements_file, 'r') as fil: - return fil.read().split('\n') - return [] - - -def parse_requirements(requirements_files=['requirements.txt', - 'tools/pip-requires']): - requirements = [] - for line in get_reqs_from_files(requirements_files): - # For the requirements list, we need to inject only the portion - # after egg= so that distutils knows the package it's looking for - # such as: - # -e git://github.com/openstack/nova/master#egg=nova - if re.match(r'\s*-e\s+', line): - requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', - line)) - # such as: - # http://github.com/openstack/nova/zipball/master#egg=nova - elif re.match(r'\s*https?:', line): - requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1', - line)) - # -f lines are for index locations, and don't get used here - elif re.match(r'\s*-f\s+', line): - pass - # argparse is part of the standard library starting with 2.7 - # adding it to the requirements list screws distro installs - elif line == 'argparse' and sys.version_info >= (2, 7): - pass - else: - requirements.append(line) - - return requirements - - -def parse_dependency_links(requirements_files=['requirements.txt', - 'tools/pip-requires']): - dependency_links = [] - # dependency_links inject alternate locations to find packages listed - # in requirements - for line in get_reqs_from_files(requirements_files): - # skip comments and blank lines - if re.match(r'(\s*#)|(\s*$)', line): - continue - # lines with -e or -f need the whole line, minus the flag - if re.match(r'\s*-[ef]\s+', line): - dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line)) - # lines that are only urls can go in unmolested - elif re.match(r'\s*https?:', line): - dependency_links.append(line) - return dependency_links - - -def write_requirements(): - venv = os.environ.get('VIRTUAL_ENV', None) - if venv is not None: - with open("requirements.txt", "w") as req_file: - output = subprocess.Popen(["pip", "-E", venv, "freeze", "-l"], - stdout=subprocess.PIPE) - requirements = output.communicate()[0].strip() - req_file.write(requirements) - - -def _run_shell_command(cmd): - if os.name == 'nt': - output = subprocess.Popen(["cmd.exe", "/C", cmd], - stdout=subprocess.PIPE) - else: - output = subprocess.Popen(["/bin/sh", "-c", cmd], - stdout=subprocess.PIPE) - out = output.communicate() - if len(out) == 0: - return None - if len(out[0].strip()) == 0: - return None - return out[0].strip() - - -def _get_git_next_version_suffix(branch_name): - datestamp = datetime.datetime.now().strftime('%Y%m%d') - if branch_name == 'milestone-proposed': - revno_prefix = "r" - else: - revno_prefix = "" - _run_shell_command("git fetch origin +refs/meta/*:refs/remotes/meta/*") - milestone_cmd = "git show meta/openstack/release:%s" % branch_name - milestonever = _run_shell_command(milestone_cmd) - if milestonever: - first_half = "%s~%s" % (milestonever, datestamp) - else: - first_half = datestamp - - post_version = _get_git_post_version() - # post version should look like: - # 0.1.1.4.gcc9e28a - # where the bit after the last . is the short sha, and the bit between - # the last and second to last is the revno count - (revno, sha) = post_version.split(".")[-2:] - second_half = "%s%s.%s" % (revno_prefix, revno, sha) - return ".".join((first_half, second_half)) - - -def _get_git_current_tag(): - return _run_shell_command("git tag --contains HEAD") - - -def _get_git_tag_info(): - return _run_shell_command("git describe --tags") - - -def _get_git_post_version(): - current_tag = _get_git_current_tag() - if current_tag is not None: - return current_tag - else: - tag_info = _get_git_tag_info() - if tag_info is None: - base_version = "0.0" - cmd = "git --no-pager log --oneline" - out = _run_shell_command(cmd) - revno = len(out.split("\n")) - sha = _run_shell_command("git describe --always") - else: - tag_infos = tag_info.split("-") - base_version = "-".join(tag_infos[:-2]) - (revno, sha) = tag_infos[-2:] - return "%s.%s.%s" % (base_version, revno, sha) - - -def write_git_changelog(): - """Write a changelog based on the git changelog.""" - new_changelog = 'ChangeLog' - if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'): - if os.path.isdir('.git'): - git_log_cmd = 'git log --stat' - changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() - with open(new_changelog, "w") as changelog_file: - changelog_file.write(canonicalize_emails(changelog, mailmap)) - else: - open(new_changelog, 'w').close() - - -def generate_authors(): - """Create AUTHORS file using git commits.""" - jenkins_email = 'jenkins@review.(openstack|stackforge).org' - old_authors = 'AUTHORS.in' - new_authors = 'AUTHORS' - if not os.getenv('SKIP_GENERATE_AUTHORS'): - if os.path.isdir('.git'): - # don't include jenkins email address in AUTHORS file - git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | " - "egrep -v '" + jenkins_email + "'") - changelog = _run_shell_command(git_log_cmd) - mailmap = parse_mailmap() - with open(new_authors, 'w') as new_authors_fh: - new_authors_fh.write(canonicalize_emails(changelog, mailmap)) - if os.path.exists(old_authors): - with open(old_authors, "r") as old_authors_fh: - new_authors_fh.write('\n' + old_authors_fh.read()) - else: - open(new_authors, 'w').close() - - -_rst_template = """%(heading)s -%(underline)s - -.. automodule:: %(module)s - :members: - :undoc-members: - :show-inheritance: -""" - - -def read_versioninfo(project): - """Read the versioninfo file. If it doesn't exist, we're in a github - zipball, and there's really no way to know what version we really - are, but that should be ok, because the utility of that should be - just about nil if this code path is in use in the first place.""" - versioninfo_path = os.path.join(project, 'versioninfo') - if os.path.exists(versioninfo_path): - with open(versioninfo_path, 'r') as vinfo: - version = vinfo.read().strip() - else: - version = "0.0.0" - return version - - -def write_versioninfo(project, version): - """Write a simple file containing the version of the package.""" - with open(os.path.join(project, 'versioninfo'), 'w') as fil: - fil.write("%s\n" % version) - - -def get_cmdclass(): - """Return dict of commands to run from setup.py.""" - - cmdclass = dict() - - def _find_modules(arg, dirname, files): - for filename in files: - if filename.endswith('.py') and filename != '__init__.py': - arg["%s.%s" % (dirname.replace('/', '.'), - filename[:-3])] = True - - class LocalSDist(sdist.sdist): - """Builds the ChangeLog and Authors files from VC first.""" - - def run(self): - write_git_changelog() - generate_authors() - # sdist.sdist is an old style class, can't use super() - sdist.sdist.run(self) - - cmdclass['sdist'] = LocalSDist - - # If Sphinx is installed on the box running setup.py, - # enable setup.py to build the documentation, otherwise, - # just ignore it - try: - from sphinx.setup_command import BuildDoc - - class LocalBuildDoc(BuildDoc): - def generate_autoindex(self): - print "**Autodocumenting from %s" % os.path.abspath(os.curdir) - modules = {} - option_dict = self.distribution.get_option_dict('build_sphinx') - source_dir = os.path.join(option_dict['source_dir'][1], 'api') - if not os.path.exists(source_dir): - os.makedirs(source_dir) - for pkg in self.distribution.packages: - if '.' not in pkg: - os.path.walk(pkg, _find_modules, modules) - module_list = modules.keys() - module_list.sort() - autoindex_filename = os.path.join(source_dir, 'autoindex.rst') - with open(autoindex_filename, 'w') as autoindex: - autoindex.write(""".. toctree:: - :maxdepth: 1 - -""") - for module in module_list: - output_filename = os.path.join(source_dir, - "%s.rst" % module) - heading = "The :mod:`%s` Module" % module - underline = "=" * len(heading) - values = dict(module=module, heading=heading, - underline=underline) - - print "Generating %s" % output_filename - with open(output_filename, 'w') as output_file: - output_file.write(_rst_template % values) - autoindex.write(" %s.rst\n" % module) - - def run(self): - if not os.getenv('SPHINX_DEBUG'): - self.generate_autoindex() - - for builder in ['html', 'man']: - self.builder = builder - self.finalize_options() - self.project = self.distribution.get_name() - self.version = self.distribution.get_version() - self.release = self.distribution.get_version() - BuildDoc.run(self) - cmdclass['build_sphinx'] = LocalBuildDoc - except ImportError: - pass - - return cmdclass - - -def get_git_branchname(): - for branch in _run_shell_command("git branch --color=never").split("\n"): - if branch.startswith('*'): - _branch_name = branch.split()[1].strip() - if _branch_name == "(no": - _branch_name = "no-branch" - return _branch_name - - -def get_pre_version(projectname, base_version): - """Return a version which is leading up to a version that will - be released in the future.""" - if os.path.isdir('.git'): - current_tag = _get_git_current_tag() - if current_tag is not None: - version = current_tag - else: - branch_name = os.getenv('BRANCHNAME', - os.getenv('GERRIT_REFNAME', - get_git_branchname())) - version_suffix = _get_git_next_version_suffix(branch_name) - version = "%s~%s" % (base_version, version_suffix) - write_versioninfo(projectname, version) - return version - else: - version = read_versioninfo(projectname) - return version - - -def get_post_version(projectname): - """Return a version which is equal to the tag that's on the current - revision if there is one, or tag plus number of additional revisions - if the current revision has no tag.""" - - if os.path.isdir('.git'): - version = _get_git_post_version() - write_versioninfo(projectname, version) - return version - return read_versioninfo(projectname) diff --git a/novaclient/openstack/common/timeutils.py b/novaclient/openstack/common/timeutils.py deleted file mode 100644 index 86004391d..000000000 --- a/novaclient/openstack/common/timeutils.py +++ /dev/null @@ -1,137 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack LLC. -# 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. - -""" -Time related utilities and helper functions. -""" - -import calendar -import datetime - -import iso8601 - - -TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" -PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" - - -def isotime(at=None): - """Stringify time in ISO 8601 format""" - if not at: - at = utcnow() - str = at.strftime(TIME_FORMAT) - tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' - str += ('Z' if tz == 'UTC' else tz) - return str - - -def parse_isotime(timestr): - """Parse time from ISO 8601 format""" - try: - return iso8601.parse_date(timestr) - except iso8601.ParseError as e: - raise ValueError(e.message) - except TypeError as e: - raise ValueError(e.message) - - -def strtime(at=None, fmt=PERFECT_TIME_FORMAT): - """Returns formatted utcnow.""" - if not at: - at = utcnow() - return at.strftime(fmt) - - -def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): - """Turn a formatted time back into a datetime.""" - return datetime.datetime.strptime(timestr, fmt) - - -def normalize_time(timestamp): - """Normalize time in arbitrary timezone to UTC naive object""" - offset = timestamp.utcoffset() - if offset is None: - return timestamp - return timestamp.replace(tzinfo=None) - offset - - -def is_older_than(before, seconds): - """Return True if before is older than seconds.""" - return utcnow() - before > datetime.timedelta(seconds=seconds) - - -def is_newer_than(after, seconds): - """Return True if after is newer than seconds.""" - return after - utcnow() > datetime.timedelta(seconds=seconds) - - -def utcnow_ts(): - """Timestamp version of our utcnow function.""" - return calendar.timegm(utcnow().timetuple()) - - -def utcnow(): - """Overridable version of utils.utcnow.""" - if utcnow.override_time: - return utcnow.override_time - return datetime.datetime.utcnow() - - -utcnow.override_time = None - - -def set_time_override(override_time=datetime.datetime.utcnow()): - """Override utils.utcnow to return a constant time.""" - utcnow.override_time = override_time - - -def advance_time_delta(timedelta): - """Advance overridden time using a datetime.timedelta.""" - assert(not utcnow.override_time is None) - utcnow.override_time += timedelta - - -def advance_time_seconds(seconds): - """Advance overridden time by seconds.""" - advance_time_delta(datetime.timedelta(0, seconds)) - - -def clear_time_override(): - """Remove the overridden time.""" - utcnow.override_time = None - - -def marshall_now(now=None): - """Make an rpc-safe datetime with microseconds. - - Note: tzinfo is stripped, but not required for relative times.""" - if not now: - now = utcnow() - return dict(day=now.day, month=now.month, year=now.year, hour=now.hour, - minute=now.minute, second=now.second, - microsecond=now.microsecond) - - -def unmarshall_time(tyme): - """Unmarshall a datetime dict.""" - return datetime.datetime(day=tyme['day'], - month=tyme['month'], - year=tyme['year'], - hour=tyme['hour'], - minute=tyme['minute'], - second=tyme['second'], - microsecond=tyme['microsecond']) diff --git a/novaclient/service_catalog.py b/novaclient/service_catalog.py deleted file mode 100644 index bb856239a..000000000 --- a/novaclient/service_catalog.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# Copyright 2011, Piston Cloud Computing, Inc. -# -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import novaclient.exceptions - - -class ServiceCatalog(object): - """Helper methods for dealing with a Keystone Service Catalog.""" - - def __init__(self, resource_dict): - self.catalog = resource_dict - - def get_token(self): - return self.catalog['access']['token']['id'] - - def url_for(self, attr=None, filter_value=None, - service_type=None, endpoint_type='publicURL', - service_name=None, volume_service_name=None): - """Fetch the public URL from the Compute service for - a particular endpoint attribute. If none given, return - the first. See tests for sample service catalog.""" - matching_endpoints = [] - if 'endpoints' in self.catalog: - # We have a bastardized service catalog. Treat it special. :/ - for endpoint in self.catalog['endpoints']: - if not filter_value or endpoint[attr] == filter_value: - # Ignore 1.0 compute endpoints - if endpoint.get("type") == 'compute' and \ - endpoint.get('versionId') in (None, '1.1', '2'): - matching_endpoints.append(endpoint) - if not matching_endpoints: - raise novaclient.exceptions.EndpointNotFound() - - # We don't always get a service catalog back ... - if not 'serviceCatalog' in self.catalog['access']: - return None - - # Full catalog ... - catalog = self.catalog['access']['serviceCatalog'] - - for service in catalog: - if service.get("type") != service_type: - continue - - if (service_name and service_type == 'compute' and - service.get('name') != service_name): - continue - - if (volume_service_name and service_type == 'volume' and - service.get('name') != volume_service_name): - continue - - endpoints = service['endpoints'] - for endpoint in endpoints: - # Ignore 1.0 compute endpoints - if service.get("type") == 'compute' and \ - endpoint.get('versionId', '2') not in ('1.1', '2'): - continue - if not filter_value or \ - endpoint.get(attr).lower() == filter_value.lower(): - endpoint["serviceName"] = service.get("name") - matching_endpoints.append(endpoint) - - if not matching_endpoints: - raise novaclient.exceptions.EndpointNotFound() - elif len(matching_endpoints) > 1: - raise novaclient.exceptions.AmbiguousEndpoints( - endpoints=matching_endpoints) - else: - return matching_endpoints[0][endpoint_type] diff --git a/novaclient/shell.py b/novaclient/shell.py index dd2a5c7de..1b95c0b40 100644 --- a/novaclient/shell.py +++ b/novaclient/shell.py @@ -1,5 +1,5 @@ # Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack LLC. +# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -19,29 +19,187 @@ """ import argparse -import glob -import httplib2 -import imp -import itertools +import logging import os -import pkgutil import sys -import logging + +from keystoneauth1 import loading +from oslo_utils import encodeutils +from oslo_utils import importutils +from oslo_utils import strutils import novaclient +from novaclient import api_versions from novaclient import client from novaclient import exceptions as exc import novaclient.extension +from novaclient.i18n import _ from novaclient import utils -from novaclient.v1_1 import shell as shell_v1_1 -DEFAULT_OS_COMPUTE_API_VERSION = "1.1" +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 +# '2.latest' means to that. This value never be changed until we decided to +# change the default behaviour of nova client CLI. +DEFAULT_OS_COMPUTE_API_VERSION = '2.latest' DEFAULT_NOVA_ENDPOINT_TYPE = 'publicURL' -DEFAULT_NOVA_SERVICE_TYPE = 'compute' +DEFAULT_NOVA_SERVICE_TYPE = "compute" + +HINT_HELP_MSG = (" [hint: use '--os-compute-api-version' flag to show help " + "message for proper version]") logger = logging.getLogger(__name__) +class DeprecatedAction(argparse.Action): + """An argparse action for deprecated options. + + This class is an ``argparse.Action`` subclass that allows command + line options to be explicitly deprecated. It modifies the help + text for the option to indicate that it's deprecated (unless help + has been suppressed using ``argparse.SUPPRESS``), and provides a + means to specify an alternate option to use using the ``use`` + keyword argument to ``argparse.ArgumentParser.add_argument()``. + The original action may be specified with the ``real_action`` + keyword argument, which has the same interpretation as the + ``action`` argument to ``argparse.ArgumentParser.add_argument()``, + with the addition of the special "nothing" action which completely + ignores the option (other than emitting the deprecation warning). + Note that the deprecation warning is only emitted once per + specific option string. + + Note: If the ``real_action`` keyword argument specifies an unknown + action, no warning will be emitted unless the action is used, due + to limitations with the method used to resolve the action names. + """ + + def __init__(self, option_strings, dest, help=None, + real_action=None, use=None, **kwargs): + """Initialize a ``DeprecatedAction`` instance. + + :param option_strings: The recognized option strings. + :param dest: The attribute that will be set. + :param help: Help text. This will be updated to indicate the + deprecation, and if ``use`` is provided, that + text will be included as well. + :param real_action: The actual action to invoke. This is + interpreted the same way as the ``action`` + parameter. + :param use: Text explaining which option to use instead. + """ + + # Update the help text + if not help: + if use: + help = _('Deprecated; %(use)s') % {'use': use} + else: + help = _('Deprecated') + elif help != argparse.SUPPRESS: + if use: + help = _('%(help)s (Deprecated; %(use)s)') % { + 'help': help, + 'use': use, + } + else: + help = _('%(help)s (Deprecated)') % {'help': help} + + # Initialize ourself appropriately + super(DeprecatedAction, self).__init__( + option_strings, dest, help=help, **kwargs) + + # 'emitted' tracks which warnings we've emitted + self.emitted = set() + self.use = use + + # Select the appropriate action + if real_action == 'nothing': + # NOTE(Vek): "nothing" is distinct from a real_action=None + # argument. When real_action=None, the argparse default + # action of "store" is used; when real_action='nothing', + # however, we explicitly inhibit doing anything with the + # option + self.real_action_args = False + self.real_action = None + 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) + self.real_action = real_action + else: + self.real_action_args = False + self.real_action = real_action( + option_strings, dest, help=help, **kwargs) + + def _get_action(self, parser): + """Retrieve the action callable. + + This internal method is used to retrieve the callable + implementing the action. If ``real_action`` was specified as + ``None`` or one of the standard string names, an internal + method of the ``argparse.ArgumentParser`` instance is used to + resolve it into an actual action class, which is then + instantiated. This is cached, in case the action is called + multiple times. + + :param parser: The ``argparse.ArgumentParser`` instance. + + :returns: The action callable. + """ + + # If a lookup is needed, look up the action in the parser + if self.real_action_args is not False: + option_strings, dest, help, kwargs = self.real_action_args + action_class = parser._registry_get('action', self.real_action) + + # Did we find the action class? + if action_class is None: + print(_('WARNING: Programming error: Unknown real action ' + '"%s"') % self.real_action, file=sys.stderr) + self.real_action = None + else: + # OK, instantiate the action class + self.real_action = action_class( + option_strings, dest, help=help, **kwargs) + + # It's been resolved, no further need to look it up + self.real_action_args = False + + return self.real_action + + def __call__(self, parser, namespace, values, option_string): + """Implement the action. + + Emits the deprecation warning message (only once for any given + option string), then calls the real action (if any). + + :param parser: The ``argparse.ArgumentParser`` instance. + :param namespace: The ``argparse.Namespace`` object which + should have an attribute set. + :param values: Any arguments provided to the option. + :param option_string: The option string that was used. + """ + + action = self._get_action(parser) + + # Only emit the deprecation warning once per option + if option_string not in self.emitted: + if self.use: + print(_('WARNING: Option "%(option)s" is deprecated; ' + '%(use)s') % { + 'option': option_string, + 'use': self.use, + }, file=sys.stderr) + else: + print(_('WARNING: Option "%(option)s" is deprecated') % + {'option': option_string}, file=sys.stderr) + self.emitted.add(option_string) + + if action: + action(parser, namespace, values, option_string) + + class NovaClientArgumentParser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): @@ -54,30 +212,88 @@ def error(self, message): exits. """ self.print_usage(sys.stderr) - #FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value + # FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value choose_from = ' (choose from' progparts = self.prog.partition(' ') - self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'" - " for more information.\n" % - {'errmsg': message.split(choose_from)[0], - 'mainp': progparts[0], - 'subp': progparts[2]}) + self.exit(2, _("error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'" + " for more information.\n") % + {'errmsg': message.split(choose_from)[0], + 'mainp': progparts[0], + 'subp': progparts[2]}) + + def _get_option_tuples(self, option_string): + """returns (action, option, value) candidates for an option prefix + + Returns [first candidate] if all candidates refers to current and + deprecated forms of the same options: "nova boot ... --key KEY" + parsing succeed because --key could only match --key-name, + --key_name which are current/deprecated forms of the same option. + """ + option_tuples = (super(NovaClientArgumentParser, self) + ._get_option_tuples(option_string)) + if len(option_tuples) > 1: + # 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 class OpenStackComputeShell(object): - - def get_base_parser(self): + times = [] + + def __init__(self): + self.client_logger = None + + 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-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: + default_auth_plugin = 'token' + loading.register_auth_argparse_arguments( + parser, argv, default=default_auth_plugin) + + parser.set_defaults(insecure=strutils.bool_from_string( + utils.env('NOVACLIENT_INSECURE', default=False))) + parser.set_defaults(os_auth_url=utils.env('OS_AUTH_URL', 'NOVA_URL')) + + parser.set_defaults(os_username=utils.env('OS_USERNAME', + 'NOVA_USERNAME')) + parser.set_defaults(os_password=utils.env('OS_PASSWORD', + 'NOVA_PASSWORD')) + parser.set_defaults(os_project_name=utils.env( + '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( prog='nova', description=__doc__.strip(), - epilog='See "nova help COMMAND" '\ + epilog='See "nova help COMMAND" ' 'for help on a specific command.', add_help=False, formatter_class=OpenStackHelpFormatter, ) # Global arguments - parser.add_argument('-h', '--help', + parser.add_argument( + '-h', '--help', action='store_true', help=argparse.SUPPRESS, ) @@ -86,277 +302,406 @@ def get_base_parser(self): action='version', version=novaclient.__version__) - parser.add_argument('--debug', + parser.add_argument( + '--debug', default=False, action='store_true', - help="Print debugging output") + help=_("Print debugging output.")) - parser.add_argument('--no-cache', - default=utils.env('OS_NO_CACHE', default=False), + parser.add_argument( + '--os-cache', + default=strutils.bool_from_string( + utils.env('OS_CACHE', default=False), True), action='store_true', - help="Don't use the auth token cache.") - parser.add_argument('--no_cache', - help=argparse.SUPPRESS) + help=_("Use the auth token cache. Defaults to False if " + "env[OS_CACHE] is not set.")) - parser.add_argument('--timings', + parser.add_argument( + '--timings', default=False, action='store_true', - help="Print call timing info") - - parser.add_argument('--os-username', - metavar='', - default=utils.env('OS_USERNAME', 'NOVA_USERNAME'), - help='Defaults to env[OS_USERNAME].') - parser.add_argument('--os_username', - help=argparse.SUPPRESS) - - parser.add_argument('--os-password', - metavar='', - default=utils.env('OS_PASSWORD', 'NOVA_PASSWORD'), - help='Defaults to env[OS_PASSWORD].') - parser.add_argument('--os_password', - help=argparse.SUPPRESS) - - parser.add_argument('--os-tenant-name', - metavar='', - default=utils.env('OS_TENANT_NAME', 'NOVA_PROJECT_ID'), - help='Defaults to env[OS_TENANT_NAME].') - parser.add_argument('--os_tenant_name', - help=argparse.SUPPRESS) - - parser.add_argument('--os-auth-url', - metavar='', - default=utils.env('OS_AUTH_URL', 'NOVA_URL'), - help='Defaults to env[OS_AUTH_URL].') - parser.add_argument('--os_auth_url', - help=argparse.SUPPRESS) - - parser.add_argument('--os-region-name', + help=_("Print call timing info.")) + + parser.add_argument( + '--os-region-name', metavar='', default=utils.env('OS_REGION_NAME', 'NOVA_REGION_NAME'), - help='Defaults to env[OS_REGION_NAME].') - parser.add_argument('--os_region_name', - help=argparse.SUPPRESS) - - parser.add_argument('--os-auth-system', - metavar='', - default=utils.env('OS_AUTH_SYSTEM'), - help='Defaults to env[OS_AUTH_SYSTEM].') - parser.add_argument('--os_auth_system', - help=argparse.SUPPRESS) - - parser.add_argument('--service-type', + help=_('Defaults to env[OS_REGION_NAME].')) + + parser.add_argument( + '--service-type', metavar='', - help='Defaults to compute for most actions') - parser.add_argument('--service_type', - help=argparse.SUPPRESS) + help=_('Defaults to compute for most actions.')) - parser.add_argument('--service-name', + parser.add_argument( + '--service-name', metavar='', default=utils.env('NOVA_SERVICE_NAME'), - help='Defaults to env[NOVA_SERVICE_NAME]') - parser.add_argument('--service_name', - help=argparse.SUPPRESS) - - parser.add_argument('--volume-service-name', - metavar='', - default=utils.env('NOVA_VOLUME_SERVICE_NAME'), - help='Defaults to env[NOVA_VOLUME_SERVICE_NAME]') - parser.add_argument('--volume_service_name', - help=argparse.SUPPRESS) - - parser.add_argument('--endpoint-type', + help=_('Defaults to env[NOVA_SERVICE_NAME].')) + + parser.add_argument( + '--os-endpoint-type', metavar='', - default=utils.env('NOVA_ENDPOINT_TYPE', - default=DEFAULT_NOVA_ENDPOINT_TYPE), - help='Defaults to env[NOVA_ENDPOINT_TYPE] or ' - + DEFAULT_NOVA_ENDPOINT_TYPE + '.') - # NOTE(dtroyer): We can't add --endpoint_type here due to argparse - # thinking usage-list --end is ambiguous; but it - # works fine with only --endpoint-type present - # Go figure. I'm leaving this here for doc purposes. - #parser.add_argument('--endpoint_type', - # help=argparse.SUPPRESS) - - parser.add_argument('--os-compute-api-version', + dest='endpoint_type', + default=utils.env( + 'NOVA_ENDPOINT_TYPE', default=utils.env( + 'OS_ENDPOINT_TYPE', + default=DEFAULT_NOVA_ENDPOINT_TYPE)), + help=_('Defaults to env[NOVA_ENDPOINT_TYPE], ' + 'env[OS_ENDPOINT_TYPE] or ') + + DEFAULT_NOVA_ENDPOINT_TYPE + '.') + + parser.add_argument( + '--os-compute-api-version', metavar='', default=utils.env('OS_COMPUTE_API_VERSION', - default=DEFAULT_OS_COMPUTE_API_VERSION), - help='Accepts 1.1, defaults to env[OS_COMPUTE_API_VERSION].') - parser.add_argument('--os_compute_api_version', - help=argparse.SUPPRESS) + default=DEFAULT_OS_COMPUTE_API_VERSION), + help=_('Accepts X, X.Y (where X is major and Y is minor part) or ' + '"X.latest", defaults to env[OS_COMPUTE_API_VERSION].')) - parser.add_argument('--insecure', - default=utils.env('NOVACLIENT_INSECURE', default=False), - action='store_true', - help="Explicitly allow novaclient to perform \"insecure\" " - "SSL (https) requests. The server's certificate will " - "not be verified against any certificate authorities. " - "This option should be used with caution.") - - # FIXME(dtroyer): The args below are here for diablo compatibility, - # remove them in folsum cycle - - # alias for --os-username, left in for backwards compatibility - parser.add_argument('--username', - help=argparse.SUPPRESS) - - # alias for --os-region-name, left in for backwards compatibility - parser.add_argument('--region_name', - help=argparse.SUPPRESS) - - # alias for --os-password, left in for backwards compatibility - parser.add_argument('--apikey', '--password', dest='apikey', - default=utils.env('NOVA_API_KEY'), - help=argparse.SUPPRESS) - - # alias for --os-tenant-name, left in for backward compatibility - parser.add_argument('--projectid', '--tenant_name', dest='projectid', - default=utils.env('NOVA_PROJECT_ID'), - help=argparse.SUPPRESS) - - # alias for --os-auth-url, left in for backward compatibility - parser.add_argument('--url', '--auth_url', dest='url', - default=utils.env('NOVA_URL'), - help=argparse.SUPPRESS) - - parser.add_argument('--bypass-url', + parser.add_argument( + '--os-endpoint-override', metavar='', - dest='bypass_url', - help="Use this API endpoint instead of the Service Catalog") - parser.add_argument('--bypass_url', - help=argparse.SUPPRESS) + dest='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[OS_ENDPOINT_OVERRIDE].")) + + parser.set_defaults(func=self.do_help) + parser.set_defaults(command='') + + if osprofiler_profiler: + parser.add_argument('--profile', + metavar='HMAC_KEY', + default=utils.env('OS_PROFILE'), + help='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.') + + self._append_global_identity_args(parser, argv) return parser - def get_subcommand_parser(self, version): - parser = self.get_base_parser() + def get_subcommand_parser(self, version, do_help=False, argv=None): + parser = self.get_base_parser(argv) self.subcommands = {} subparsers = parser.add_subparsers(metavar='') - try: - actions_module = { - '1.1': shell_v1_1, - '2': shell_v1_1, - }[version] - except KeyError: - actions_module = shell_v1_1 + actions_module = importutils.import_module( + "novaclient.v%s.shell" % version.ver_major) - self._find_actions(subparsers, actions_module) - self._find_actions(subparsers, self) + self._find_actions(subparsers, actions_module, version, do_help) + self._find_actions(subparsers, self, version, do_help) for extension in self.extensions: - self._find_actions(subparsers, extension.module) + self._find_actions(subparsers, extension.module, version, do_help) self._add_bash_completion_subparser(subparsers) return parser - def _discover_extensions(self, version): - extensions = [] - for name, module in itertools.chain( - self._discover_via_python_path(), - self._discover_via_contrib_path(version)): - - extension = novaclient.extension.Extension(name, module) - extensions.append(extension) - - return extensions - - def _discover_via_python_path(self): - for (module_loader, name, _ispkg) in pkgutil.iter_modules(): - if name.endswith('python_novaclient_ext'): - if not hasattr(module_loader, 'load_module'): - # Python 2.6 compat: actually get an ImpImporter obj - module_loader = module_loader.find_module(name) - - module = module_loader.load_module(name) - yield name, module - - def _discover_via_contrib_path(self, version): - module_path = os.path.dirname(os.path.abspath(__file__)) - version_str = "v%s" % version.replace('.', '_') - ext_path = os.path.join(module_path, version_str, 'contrib') - ext_glob = os.path.join(ext_path, "*.py") - - for ext_path in glob.iglob(ext_glob): - name = os.path.basename(ext_path)[:-3] - - if name == "__init__": - continue - - module = imp.load_source(name, ext_path) - yield name, module - def _add_bash_completion_subparser(self, subparsers): - subparser = subparsers.add_parser('bash_completion', + subparser = subparsers.add_parser( + 'bash_completion', add_help=False, formatter_class=OpenStackHelpFormatter ) self.subcommands['bash_completion'] = subparser subparser.set_defaults(func=self.do_bash_completion) - def _find_actions(self, subparsers, actions_module): + def _find_actions(self, subparsers, actions_module, version, do_help): + msg = _(" (Supported by API versions '%(start)s' - '%(end)s')") for attr in (a for a in dir(actions_module) if a.startswith('do_')): - # I prefer to be hypen-separated instead of underscores. + # I prefer to be hyphen-separated instead of underscores. command = attr[3:].replace('_', '-') callback = getattr(actions_module, attr) desc = callback.__doc__ or '' - action_help = desc.strip().split('\n')[0] + if hasattr(callback, "versioned"): + additional_msg = "" + subs = api_versions.get_substitutions(callback) + if do_help: + additional_msg = msg % { + 'start': subs[0].start_version.get_string(), + 'end': subs[-1].end_version.get_string()} + if version.is_latest(): + additional_msg += HINT_HELP_MSG + subs = [versioned_method for versioned_method in subs + if version.matches(versioned_method.start_version, + versioned_method.end_version)] + if subs: + # use the "latest" substitution + callback = subs[-1].func + else: + # there is no proper versioned method + continue + desc = callback.__doc__ or desc + desc += additional_msg + + action_help = desc.strip() arguments = getattr(callback, 'arguments', []) + groups = {} - subparser = subparsers.add_parser(command, + subparser = subparsers.add_parser( + command, help=action_help, description=desc, add_help=False, - formatter_class=OpenStackHelpFormatter - ) - subparser.add_argument('-h', '--help', + formatter_class=OpenStackHelpFormatter) + subparser.add_argument( + '-h', '--help', action='help', help=argparse.SUPPRESS, ) self.subcommands[command] = subparser for (args, kwargs) in arguments: - subparser.add_argument(*args, **kwargs) + 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) + if end_version: + end_version = api_versions.APIVersion(end_version) + else: + end_version = api_versions.APIVersion( + "%s.latest" % start_version.ver_major) + if do_help: + kwargs["help"] = kwargs.get("help", "") + (msg % { + "start": start_version.get_string(), + "end": end_version.get_string()}) + if not version.matches(start_version, end_version): + continue + + 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): if not debug: return - streamhandler = logging.StreamHandler() streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s" - streamhandler.setFormatter(logging.Formatter(streamformat)) - logger.setLevel(logging.DEBUG) - logger.addHandler(streamhandler) + # Set up the root logger to debug so that the submodules can + # print debug messages + logging.basicConfig(level=logging.DEBUG, + format=streamformat) + logging.getLogger('iso8601').setLevel(logging.WARNING) - httplib2.debuglevel = 1 + self.client_logger = logging.getLogger(client.__name__) + ch = logging.StreamHandler() + self.client_logger.setLevel(logging.DEBUG) + self.client_logger.addHandler(ch) def main(self, argv): # Parse args once to find version and debug settings - parser = self.get_base_parser() - (options, args) = parser.parse_known_args(argv) - self.setup_debugging(options.debug) + parser = self.get_base_parser(argv) + (args, args_list) = parser.parse_known_args(argv) + + self.setup_debugging(args.debug) + self.extensions = [] + do_help = args.help or not args_list or args_list[0] == 'help' + + # bash-completion should not require authentication + skip_auth = do_help or ( + 'bash-completion' in argv) + + if not args.os_compute_api_version: + api_version = api_versions.get_api_version( + DEFAULT_MAJOR_OS_COMPUTE_API_VERSION) + else: + api_version = api_versions.get_api_version( + args.os_compute_api_version) + + 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)) + os_project_id = getattr( + args, 'os_project_id', getattr(args, 'os_tenant_id', None)) + os_auth_url = args.os_auth_url + os_region_name = args.os_region_name + + if "v2.0" not in os_auth_url: + # NOTE(andreykurilin): assume that keystone V3 is used and try to + # be more user-friendly, i.e provide default values for domains + if (not args.os_project_domain_id and + not args.os_project_domain_name): + setattr(args, "os_project_domain_id", "default") + + # 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 = 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 + service_type = args.service_type + service_name = args.service_name + endpoint_override = args.endpoint_override + os_cache = args.os_cache + cacert = args.os_cacert + cert = args.os_cert + timeout = args.timeout + + keystone_session = None + keystone_auth = None + + if not endpoint_type: + endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE + + # This allow users to use endpoint_type as (internal, public or admin) + # just like other openstack clients (glance, cinder etc) + if endpoint_type in ['internal', 'public', 'admin']: + endpoint_type += 'URL' + + if not service_type: + # Note(alex_xu): We need discover version first, so if there isn't + # service type specified, we use default nova service type. + service_type = DEFAULT_NOVA_SERVICE_TYPE + + # We should always auth unless we have a token and we're passing a + # specific endpoint + # Expired tokens are handled by client.py:_cs_request + must_auth = not (auth_token and endpoint_override) + + # FIXME(usrleon): Here should be restrict for project id same as + # for os_username or os_password but for compatibility it is not. + if must_auth and not skip_auth: + + if not any([auth_token, os_username, os_user_id]): + raise exc.CommandError( + _("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" + " project ID via --os-project-name," + " --os-project-id, env[OS_PROJECT_ID]" + " or env[OS_PROJECT_NAME]. You may" + " use os-project and os-tenant" + " interchangeably.")) + + if not os_auth_url: + raise exc.CommandError( + _("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 = ( + loading.load_session_from_argparse_arguments(args)) + keystone_auth = ( + loading.load_auth_from_argparse_arguments(args)) + + if (not skip_auth and + not any([os_project_name, os_project_id])): + raise exc.CommandError(_("You must provide a project name or" + " project id via --os-project-name," + " --os-project-id, env[OS_PROJECT_ID]" + " or env[OS_PROJECT_NAME]. You may" + " use os-project and os-tenant" + " interchangeably.")) + + if not os_auth_url and not skip_auth: + raise exc.CommandError( + _("You must provide an auth url " + "via either --os-auth-url or env[OS_AUTH_URL]")) + + additional_kwargs = {} + if osprofiler_profiler: + additional_kwargs["profile"] = args.profile + + # This client is just used to discover api version. Version API needn't + # microversion, so we just pass version 2 at here. + self.cs = client.Client( + api_versions.APIVersion("2.0"), + os_username, os_password, project_id=os_project_id, + project_name=os_project_name, user_id=os_user_id, + auth_url=os_auth_url, insecure=insecure, + region_name=os_region_name, endpoint_type=endpoint_type, + extensions=self.extensions, service_type=service_type, + service_name=service_name, auth_token=auth_token, + timings=args.timings, endpoint_override=endpoint_override, + os_cache=os_cache, http_log_debug=args.debug, + cacert=cacert, cert=cert, timeout=timeout, + session=keystone_session, auth=keystone_auth, + logger=self.client_logger, + project_domain_id=os_project_domain_id, + project_domain_name=os_project_domain_name, + user_domain_id=os_user_domain_id, + user_domain_name=os_user_domain_name, + **additional_kwargs) + + if not skip_auth: + if not api_version.is_latest(): + if api_version > api_versions.APIVersion("2.0"): + if not api_version.matches(novaclient.API_MIN_VERSION, + novaclient.API_MAX_VERSION): + raise exc.CommandError( + _("The specified version isn't supported by " + "client. The valid version range is '%(min)s' " + "to '%(max)s'") % { + "min": novaclient.API_MIN_VERSION.get_string(), + "max": novaclient.API_MAX_VERSION.get_string()} + ) + api_version = api_versions.discover_version(self.cs, api_version) # build available subcommands based on version - self.extensions = self._discover_extensions( - options.os_compute_api_version) + self.extensions = client.discover_extensions(api_version) self._run_extension_hooks('__pre_parse_args__') - # NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse - # thinking usage-list --end is ambiguous; but it - # works fine with only --endpoint-type present - # Go figure. - if '--endpoint_type' in argv: - spot = argv.index('--endpoint_type') - argv[spot] = '--endpoint-type' - subcommand_parser = self.get_subcommand_parser( - options.os_compute_api_version) + api_version, do_help=do_help, argv=argv) self.parser = subcommand_parser - if options.help or not argv: + if args.help or not argv: subcommand_parser.print_help() return 0 @@ -371,103 +716,44 @@ def main(self, argv): self.do_bash_completion(args) return 0 - (os_username, os_password, os_tenant_name, os_auth_url, - os_region_name, os_auth_system, endpoint_type, insecure, - service_type, service_name, volume_service_name, - username, apikey, projectid, url, region_name, - bypass_url, no_cache) = ( - args.os_username, args.os_password, - args.os_tenant_name, args.os_auth_url, - args.os_region_name, args.os_auth_system, - args.endpoint_type, args.insecure, args.service_type, - args.service_name, args.volume_service_name, - args.username, args.apikey, args.projectid, - args.url, args.region_name, - args.bypass_url, args.no_cache) - - if not endpoint_type: - endpoint_type = DEFAULT_NOVA_ENDPOINT_TYPE - - if not service_type: - service_type = DEFAULT_NOVA_SERVICE_TYPE - service_type = utils.get_service_type(args.func) or service_type - - #FIXME(usrleon): Here should be restrict for project id same as - # for os_username or os_password but for compatibility it is not. - - if not utils.isunauthenticated(args.func): - if not os_username: - if not username: - raise exc.CommandError("You must provide a username " - "via either --os-username or env[OS_USERNAME]") - else: - os_username = username - - if not os_password: - if not apikey: - raise exc.CommandError("You must provide a password " - "via either --os-password or via " - "env[OS_PASSWORD]") - else: - os_password = apikey - - if not os_tenant_name: - if not projectid: - raise exc.CommandError("You must provide a tenant name " - "via either --os-tenant-name or " - "env[OS_TENANT_NAME]") - else: - os_tenant_name = projectid - - if not os_auth_url: - if not url: - if os_auth_system and os_auth_system != 'keystone': - os_auth_url = \ - client.get_auth_system_url(os_auth_system) - else: - os_auth_url = url - - if not os_auth_url: - raise exc.CommandError("You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL] " - "or specify an auth_system which defines a " - "default url with --os-auth-system " - "or env[OS_AUTH_SYSTEM") - - if not os_region_name and region_name: - os_region_name = region_name - - if (options.os_compute_api_version and - options.os_compute_api_version != '1.0'): - if not os_tenant_name: - raise exc.CommandError("You must provide a tenant name " - "via either --os-tenant-name or env[OS_TENANT_NAME]") - - if not os_auth_url: - raise exc.CommandError("You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL]") - - self.cs = client.Client(options.os_compute_api_version, os_username, - os_password, os_tenant_name, os_auth_url, insecure, - region_name=os_region_name, endpoint_type=endpoint_type, - extensions=self.extensions, service_type=service_type, - service_name=service_name, auth_system=os_auth_system, - volume_service_name=volume_service_name, - timings=args.timings, bypass_url=bypass_url, - no_cache=no_cache, http_log_debug=options.debug) - - try: - if not utils.isunauthenticated(args.func): - self.cs.authenticate() - except exc.Unauthorized: - raise exc.CommandError("Invalid OpenStack Nova credentials.") - except exc.AuthorizationFailure: - raise exc.CommandError("Unable to authorize user") + if not args.service_type: + service_type = (utils.get_service_type(args.func) or + DEFAULT_NOVA_SERVICE_TYPE) + + if utils.isunauthenticated(args.func): + # NOTE(alex_xu): We need authentication for discover microversion. + # But the subcommands may needn't it. If the subcommand needn't, + # we clear the session arguments. + keystone_session = None + keystone_auth = None + + # Recreate client object with discovered version. + self.cs = client.Client( + api_version, + os_username, os_password, project_id=os_project_id, + project_name=os_project_name, user_id=os_user_id, + auth_url=os_auth_url, insecure=insecure, + region_name=os_region_name, endpoint_type=endpoint_type, + extensions=self.extensions, service_type=service_type, + service_name=service_name, auth_token=auth_token, + timings=args.timings, endpoint_override=endpoint_override, + os_cache=os_cache, http_log_debug=args.debug, + cacert=cacert, cert=cert, timeout=timeout, + session=keystone_session, auth=keystone_auth, + project_domain_id=os_project_domain_id, + project_domain_name=os_project_domain_name, + user_domain_id=os_user_domain_id, + user_domain_name=os_user_domain_name) args.func(self.cs, args) + if osprofiler_profiler and args.profile: + trace_id = osprofiler_profiler.get().get_base_id() + print("To display trace use the command:\n\n" + " osprofiler trace show --html %s " % trace_id) + if args.timings: - self._dump_timings(self.cs.get_timings()) + self._dump_timings(self.times + self.cs.get_timings()) def _dump_timings(self, timings): class Tyme(object): @@ -500,10 +786,13 @@ def do_bash_completion(self, _args): commands.remove('bash-completion') commands.remove('bash_completion') - print ' '.join(commands | options) + print(' '.join(commands | options)) - @utils.arg('command', metavar='', nargs='?', - help='Display help for ') + @utils.arg( + 'command', + metavar='', + nargs='?', + help=_('Display help for .')) def do_help(self, args): """ Display help about this program or one of its subcommands. @@ -512,7 +801,7 @@ def do_help(self, args): if args.command in self.subcommands: self.subcommands[args.command].print_help() else: - raise exc.CommandError("'%s' is not a valid subcommand" % + raise exc.CommandError(_("'%s' is not a valid subcommand") % args.command) else: self.parser.print_help() @@ -520,20 +809,41 @@ def do_help(self, args): # I'm picky about my shell help. class OpenStackHelpFormatter(argparse.HelpFormatter): + def __init__(self, prog, indent_increment=2, max_help_position=32, + width=None): + super(OpenStackHelpFormatter, self).__init__(prog, indent_increment, + max_help_position, width) + def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) super(OpenStackHelpFormatter, self).start_section(heading) -def main(): +def main(argv=sys.argv[1:]): try: - OpenStackComputeShell().main(sys.argv[1:]) - - except Exception, e: - logger.debug(e, exc_info=1) - print >> sys.stderr, "ERROR: %s" % unicode(e) + # Special dansmith envvar to hide the warning. Don't rely on this + # because we will eventually remove all this stuff. + if os.environ.get("NOVACLIENT_ISHOULDNTBEDOINGTHIS") != "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) + print("ERROR (%(type)s): %(msg)s" % { + 'type': exc.__class__.__name__, + 'msg': exc}, + file=sys.stderr) sys.exit(1) + except KeyboardInterrupt: + print(_("... terminating nova client"), file=sys.stderr) + sys.exit(130) if __name__ == "__main__": diff --git a/novaclient/openstack/__init__.py b/novaclient/tests/__init__.py similarity index 100% rename from novaclient/openstack/__init__.py rename to novaclient/tests/__init__.py diff --git a/novaclient/tests/functional/README.rst b/novaclient/tests/functional/README.rst new file mode 100644 index 000000000..b343b33d4 --- /dev/null +++ b/novaclient/tests/functional/README.rst @@ -0,0 +1,55 @@ +==================================== +python-novaclient functional testing +==================================== + +Idea +---- + +Over time we have noticed two issues with novaclient unit tests. + +* Does not exercise the CLI +* We can get the expected server behavior wrong, and test the wrong thing. + +We are using functional tests, run against a running cloud +(primarily devstack), to address these two cases. + +Additionally these functional tests can be considered example uses +of python-novaclient. + +These tests started out in tempest as read only nova CLI tests, to make sure +the CLI didn't simply stacktrace when being used (which happened on +multiple occasions). + + +Testing Theory +-------------- + +We are treating python-novaclient as legacy code, so we do not want to spend a +lot of effort adding in missing features. In the future the CLI will move to +python-openstackclient, and the python API will be based on the OpenStack +SDK project. But until that happens we still need better functional testing, +to prevent regressions etc. + + +Since python-novaclient has two uses, CLI and python API, we should have two +sets of functional tests. CLI and python API. The python API tests should +never use the CLI. But the CLI tests can use the python API where adding +native support to the CLI for the required functionality would involve a +non trivial amount of work. + +Functional Test Guidelines +-------------------------- + +* Consume credentials via standard client environmental variables:: + + OS_USERNAME + OS_PASSWORD + OS_TENANT_NAME + OS_AUTH_URL + +* Usage of insecure SSL can be configured via the standard client environment + variable:: + + OS_INSECURE + +* Try not to require an additional configuration file diff --git a/novaclient/openstack/common/__init__.py b/novaclient/tests/functional/__init__.py similarity index 100% rename from novaclient/openstack/common/__init__.py rename to novaclient/tests/functional/__init__.py diff --git a/novaclient/v1_1/contrib/__init__.py b/novaclient/tests/functional/api/__init__.py similarity index 100% rename from novaclient/v1_1/contrib/__init__.py rename to novaclient/tests/functional/api/__init__.py diff --git a/novaclient/tests/functional/api/test_servers.py b/novaclient/tests/functional/api/test_servers.py new file mode 100644 index 000000000..4eca50671 --- /dev/null +++ b/novaclient/tests/functional/api/test_servers.py @@ -0,0 +1,36 @@ +# 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 time + +from novaclient.tests.functional import base + + +class TestServersAPI(base.ClientTestBase): + def test_server_ips(self): + server_name = "test_server" + initial_server = self.client.servers.create( + server_name, self.image, self.flavor, + nics=[{"net-id": self.network.id}]) + self.addCleanup(initial_server.delete) + + for x in range(60): + server = self.client.servers.get(initial_server) + if server.status == "ACTIVE": + break + else: + time.sleep(1) + else: + self.fail("Server %s did not go ACTIVE after 60s" % server) + + ips = self.client.servers.ips(server) + self.assertIn(self.network.name, ips) diff --git a/novaclient/tests/functional/base.py b/novaclient/tests/functional/base.py new file mode 100644 index 000000000..9a391d1a0 --- /dev/null +++ b/novaclient/tests/functional/base.py @@ -0,0 +1,573 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import time + +import fixtures +from keystoneauth1 import identity +from keystoneauth1 import session as ksession +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: " + "'gocubsgo'. use 'sudo' for root.") + + +# The following are simple filter functions that filter our available +# image / flavor list so that they can be used in standard testing. +def pick_flavor(flavors): + """Given a flavor list pick a reasonable one.""" + for flavor_priority in ('m1.nano', 'm1.micro', 'm1.tiny', 'm1.small'): + for flavor in flavors: + if flavor.name == flavor_priority: + return flavor + raise NoFlavorException() + + +def pick_image(images): + firstImage = None + for image in images: + firstImage = firstImage or image + if image.name.startswith('cirros') and ( + image.name.endswith('-uec') or + image.name.endswith('-disk.img')): + return image + + # We didn't find the specific cirros image we'd like to use, so just use + # the first available. + if firstImage: + return firstImage + + raise NoImageException() + + +def pick_network(networks): + network_name = os.environ.get('OS_NOVACLIENT_NETWORK') + if network_name: + for network in networks: + if network.name == network_name: + return network + raise NoNetworkException() + return networks[0] + + +class NoImageException(Exception): + """We couldn't find an acceptable image.""" + pass + + +class NoFlavorException(Exception): + """We couldn't find an acceptable flavor.""" + pass + + +class NoNetworkException(Exception): + """We couldn't find an acceptable network.""" + pass + + +class NoCloudConfigException(Exception): + """We couldn't find a cloud configuration.""" + pass + + +CACHE = {} + + +class ClientTestBase(testtools.TestCase): + """Base test class for read only python-novaclient commands. + + This is a first pass at a simple read only python-novaclient test. This + only exercises client commands that are read only. + + This should test commands: + * as a regular user + * as a admin user + * with and without optional parameters + * initially just check return codes, and later test command outputs + + """ + COMPUTE_API_VERSION = None + + log_format = ('%(asctime)s %(process)d %(levelname)-8s ' + '[%(name)s] %(message)s') + + def setUp(self): + super(ClientTestBase, self).setUp() + + test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0) + try: + test_timeout = int(test_timeout) + except ValueError: + test_timeout = 0 + if test_timeout > 0: + self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) + + if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or + os.environ.get('OS_STDOUT_CAPTURE') == '1'): + stdout = self.useFixture(fixtures.StringStream('stdout')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) + if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or + os.environ.get('OS_STDERR_CAPTURE') == '1'): + stderr = self.useFixture(fixtures.StringStream('stderr')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + + if (os.environ.get('OS_LOG_CAPTURE') != 'False' and + os.environ.get('OS_LOG_CAPTURE') != '0'): + self.useFixture(fixtures.LoggerFixture(nuke_handlers=False, + format=self.log_format, + level=None)) + + # Collecting of credentials: + # + # Grab the cloud config from a user's clouds.yaml file. + # First look for a functional_admin cloud, as this is a cloud + # that the user may have defined for functional testing that has + # admin credentials. + # If that is not found, get the devstack config and override the + # username and project_name to be admin so that admin credentials + # will be used. + # + # Finally, fall back to looking for environment variables to support + # existing users running these the old way. We should deprecate that + # as tox 2.0 blanks out environment. + # + # TODO(sdague): while we collect this information in + # 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 = openstack.config.OpenStackConfig() + try: + cloud_config = openstack_config.get_one_cloud('functional_admin') + except openstack.config.exceptions.OpenStackConfigException: + try: + cloud_config = openstack_config.get_one_cloud( + 'devstack', auth=dict( + username='admin', project_name='admin')) + except openstack.config.exceptions.OpenStackConfigException: + try: + cloud_config = openstack_config.get_one_cloud('envvars') + except openstack.config.exceptions.OpenStackConfigException: + cloud_config = None + + if cloud_config is None: + raise NoCloudConfigException( + "Could not find a cloud named functional_admin or a cloud" + " named devstack. Please check your clouds.yaml file and" + " try again.") + auth_info = cloud_config.config['auth'] + + user = auth_info['username'] + passwd = auth_info['password'] + self.project_name = auth_info['project_name'] + auth_url = auth_info['auth_url'] + user_domain_id = auth_info['user_domain_id'] + self.project_domain_id = auth_info['project_domain_id'] + + if 'insecure' in cloud_config.config: + 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, + project_name=self.project_name, + auth_url=auth_url, + project_domain_id=self.project_domain_id, + user_domain_id=user_domain_id) + session = ksession.Session( + cert=self.cert, + auth=auth, + verify=(self.cacert or not self.insecure) + ) + + self.client = self._get_novaclient(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.openstack.image.images()) + self.flavor = CACHE["flavor"] + self.image = CACHE["image"] + + if "network" not in CACHE: + # Get the networks from neutron. + neutron_networks = self.openstack.network.networks() + # Convert the neutron dicts to Network objects. + nets = [] + for network in neutron_networks: + nets.append(networks.Network( + networks.NeutronManager, network)) + # Keep track of whether or not there are multiple networks + # available to the given tenant because if so, a specific + # network ID has to be passed in on server create requests + # otherwise the server POST will fail with a 409. + CACHE['multiple_networks'] = len(nets) > 1 + CACHE["network"] = pick_network(nets) + self.network = CACHE["network"] + self.multiple_networks = CACHE['multiple_networks'] + + # create a CLI client in case we'd like to do CLI + # testing. tempest.lib does this really weird thing where it + # builds a giant factory of all the CLIs that it knows + # about. Eventually that should really be unwound into + # something more sensible. + cli_dir = os.environ.get( + 'OS_NOVACLIENT_EXEC_DIR', + os.path.join(os.environ['TOX_ENV_DIR'], 'bin')) + + self.cli_clients = tempest.lib.cli.base.CLIClient( + username=user, + password=passwd, + tenant_name=self.project_name, + uri=auth_url, + cli_dir=cli_dir, + insecure=self.insecure) + + def _get_novaclient(self, session): + nc = novaclient.client.Client("2", session=session) + + if self.COMPUTE_API_VERSION: + if "min_api_version" not in CACHE: + # Obtain supported versions by API side + v = nc.versions.get_current() + if not hasattr(v, 'version') or not v.version: + # API doesn't support microversions + CACHE["min_api_version"] = ( + novaclient.api_versions.APIVersion("2.0")) + CACHE["max_api_version"] = ( + novaclient.api_versions.APIVersion("2.0")) + else: + CACHE["min_api_version"] = ( + novaclient.api_versions.APIVersion(v.min_version)) + CACHE["max_api_version"] = ( + novaclient.api_versions.APIVersion(v.version)) + + if self.COMPUTE_API_VERSION == "2.latest": + requested_version = min(novaclient.API_MAX_VERSION, + CACHE["max_api_version"]) + else: + requested_version = novaclient.api_versions.APIVersion( + self.COMPUTE_API_VERSION) + + if not requested_version.matches(CACHE["min_api_version"], + CACHE["max_api_version"]): + msg = ("%s is not supported by Nova-API. Supported version" % + self.COMPUTE_API_VERSION) + if CACHE["min_api_version"] == CACHE["max_api_version"]: + msg += ": %s" % CACHE["min_api_version"].get_string() + else: + msg += "s: %s - %s" % ( + CACHE["min_api_version"].get_string(), + CACHE["max_api_version"].get_string()) + self.skipTest(msg) + + nc.api_version = requested_version + return nc + + def nova(self, action, flags='', params='', fail_ok=False, + endpoint_type='publicURL', merge_stderr=False): + if self.COMPUTE_API_VERSION: + flags += " --os-compute-api-version %s " % self.COMPUTE_API_VERSION + return self.cli_clients.nova(action, flags, params, fail_ok, + endpoint_type, merge_stderr) + + def wait_for_volume_status(self, volume, status, timeout=60, + poll_interval=1): + """Wait until volume reaches given status. + + :param volume: volume resource + :param status: expected status of volume + :param timeout: timeout in seconds + :param poll_interval: poll interval in seconds + """ + start_time = time.time() + while time.time() - start_time < timeout: + volume = self.openstack.block_storage.get_volume(volume) + if volume.status == status: + break + time.sleep(poll_interval) + else: + self.fail("Volume %s did not reach status %s after %d s" + % (volume.id, status, timeout)) + + def wait_for_server_os_boot(self, server_id, timeout=300, + poll_interval=1): + """Wait until instance's operating system is completely booted. + + :param server_id: uuid4 id of given instance + :param timeout: timeout in seconds + :param poll_interval: poll interval in seconds + """ + start_time = time.time() + console = None + while time.time() - start_time < timeout: + console = self.nova('console-log %s ' % server_id) + if BOOT_IS_COMPLETE in console: + break + time.sleep(poll_interval) + else: + self.fail("Server %s did not boot after %d s.\nConsole:\n%s" + % (server_id, timeout, console)) + + def wait_for_resource_delete(self, resource, manager, + timeout=60, poll_interval=1): + """Wait until getting the resource raises NotFound exception. + + :param resource: Resource object. + :param manager: Manager object with get method. + :param timeout: timeout in seconds + :param poll_interval: poll interval in seconds + """ + start_time = time.time() + while time.time() - start_time < timeout: + try: + manager.get(resource) + except Exception as e: + if getattr(e, "http_status", None) == 404: + break + else: + raise + time.sleep(poll_interval) + else: + 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. + + EXAMPLE of the table: + # +-------------+----------------------------------+ + # | Property | Value | + # +-------------+----------------------------------+ + # | description | | + # | enabled | True | + # | id | 582df899eabc47018c96713c2f7196ba | + # | name | admin | + # +-------------+----------------------------------+ + """ + lines = table.split("\n") + for line in lines: + if "|" in line: + l_property, l_value = line.split("|")[1:3] + if l_property.strip() == key: + return l_value.strip() + raise ValueError("Property '%s' is missing from the table:\n%s" % + (key, table)) + + def _get_column_value_from_single_row_table(self, table, column): + """Get the value for the column in the single-row table + + Example table: + + +----------+-------------+----------+----------+ + | address | cidr | hostname | host | + +----------+-------------+----------+----------+ + | 10.0.0.3 | 10.0.0.0/24 | test | myhost | + +----------+-------------+----------+----------+ + + :param table: newline-separated table with |-separated cells + :param column: name of the column to look for + :raises: ValueError if the column value is not found + """ + lines = table.split("\n") + # Determine the column header index first. + column_index = -1 + for line in lines: + if "|" in line: + if column_index == -1: + headers = line.split("|")[1:-1] + for index, header in enumerate(headers): + if header.strip() == column: + column_index = index + break + else: + # We expect a single-row table so we should be able to get + # the value now using the column index. + return line.split("|")[1:-1][column_index].strip() + + raise ValueError("Unable to find value for column '%s'." % column) + + def _get_list_of_values_from_single_column_table(self, table, column): + """Get the list of values for the column in the single-column table + + Example table: + + +------+ + | Tags | + +------+ + | tag1 | + | tag2 | + +------+ + + :param table: newline-separated table with |-separated cells + :param column: name of the column to look for + :raises: ValueError if the single column has some other name + """ + lines = table.split("\n") + column_name = None + values = [] + for line in lines: + if "|" in line: + if not column_name: + column_name = line.split("|")[1].strip() + if column_name != column: + raise ValueError( + "The table has no column %(expected)s " + "but has column %(actual)s." % { + 'expected': column, 'actual': column_name}) + else: + values.append(line.split("|")[1].strip()) + return values + + 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 + flavor = flavor or self.flavor + server = self.client.servers.create(name, self.image, flavor, + nics=nics, **kwargs) + if add_cleanup: + self.addCleanup(server.delete) + novaclient.v2.shell._poll_for_status( + self.client.servers.get, server.id, + 'building', ['active']) + return server + + def _wait_for_state_change(self, server_id, status): + novaclient.v2.shell._poll_for_status( + self.client.servers.get, server_id, None, [status], + show_progress=False, poll_period=1, silent=True) + + def _get_project_id(self, name): + """Obtain project id by project name.""" + 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.""" + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + + def _get_absolute_limits(self): + """Returns the absolute limits (quota usage) including reserved quota + usage for the given tenant running the test. + + :return: A dict where the key is the limit (or usage) and value. + """ + # The absolute limits are returned in a generator so convert to a dict. + return {limit.name: limit.value + for limit in self.client.limits.get(reserved=True).absolute} + + 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(ProjectTestBase, self).setUp() + user_name = uuidutils.generate_uuid() + project_name = uuidutils.generate_uuid() + password = 'password' + + 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.addCleanup( + self.openstack.identity.delete_user, self.user_id) + + self.cli_clients_2 = tempest.lib.cli.base.CLIClient( + username=user_name, + password=password, + tenant_name=project_name, + uri=self.cli_clients.uri, + cli_dir=self.cli_clients.cli_dir, + insecure=self.insecure) + + def another_nova(self, action, flags='', params='', fail_ok=False, + endpoint_type='publicURL', merge_stderr=False): + flags += " --os-compute-api-version %s " % self.COMPUTE_API_VERSION + return self.cli_clients_2.nova(action, flags, params, fail_ok, + endpoint_type, merge_stderr) diff --git a/novaclient/tests/functional/clouds.yaml.sample b/novaclient/tests/functional/clouds.yaml.sample new file mode 100644 index 000000000..baf23f695 --- /dev/null +++ b/novaclient/tests/functional/clouds.yaml.sample @@ -0,0 +1,9 @@ +clouds: + devstack: + auth: + username: admin + password: change_me + project_name: admin + auth_url: http://localhost:5000/v3 + user_domain_id: default + project_domain_id: default diff --git a/novaclient/v1_1/virtual_interfaces.py b/novaclient/tests/functional/hooks/check_resources.py similarity index 57% rename from novaclient/v1_1/virtual_interfaces.py rename to novaclient/tests/functional/hooks/check_resources.py index 64593e018..1d2d1bdba 100644 --- a/novaclient/v1_1/virtual_interfaces.py +++ b/novaclient/tests/functional/hooks/check_resources.py @@ -1,6 +1,3 @@ -# Copyright 2012 OpenStack LLC. -# 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 @@ -13,21 +10,21 @@ # License for the specific language governing permissions and limitations # under the License. -""" -Virtual Interfaces (1.1 extension). -""" +from novaclient.tests.functional import base -from novaclient import base +class ResourceChecker(base.ClientTestBase): -class VirtualInterface(base.Resource): - def __repr__(self): + def runTest(self): pass + def check(self): + self.setUp() + + print("$ nova list --all-tenants") + print(self.nova("list", params="--all-tenants")) + print("\n") -class VirtualInterfaceManager(base.ManagerWithFind): - resource_class = VirtualInterface - def list(self, instance_id): - return self._list('/servers/%s/os-virtual-interfaces' % instance_id, - 'virtual_interfaces') +if __name__ == "__main__": + ResourceChecker().check() diff --git a/novaclient/tests/functional/test_auth.py b/novaclient/tests/functional/test_auth.py new file mode 100644 index 000000000..77c3503ee --- /dev/null +++ b/novaclient/tests/functional/test_auth.py @@ -0,0 +1,95 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +from urllib import parse + +import tempest.lib.cli.base + +from novaclient import client +from novaclient.tests.functional import base + + +class TestAuthentication(base.ClientTestBase): + + def _get_url(self, identity_api_version): + url = parse.urlparse(self.cli_clients.uri) + return parse.urlunparse((url.scheme, url.netloc, + '/identity/v%s' % identity_api_version, + url.params, url.query, + url.fragment)) + + def nova_auth_with_password(self, action, 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 = f'{flags} --insecure' + + return tempest.lib.cli.base.execute( + "nova", action, flags, cli_dir=self.cli_clients.cli_dir) + + def nova_auth_with_token(self, identity_api_version): + auth_ref = self.client.client.session.auth.get_access( + self.client.client.session) + token = auth_ref.auth_token + auth_url = self._get_url(identity_api_version) + kw = {} + 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, + cacert=self.cacert, cert=self.cert, + **kw) + nova.servers.list() + + # 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) + + 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' + + tempest.lib.cli.base.execute( + "nova", "list", flags, cli_dir=self.cli_clients.cli_dir) + + def test_auth_via_keystone_v3(self): + self.nova_auth_with_password("list", identity_api_version="3") + self.nova_auth_with_token(identity_api_version="3") diff --git a/tests/__init__.py b/novaclient/tests/functional/v2/__init__.py similarity index 100% rename from tests/__init__.py rename to novaclient/tests/functional/v2/__init__.py diff --git a/novaclient/tests/functional/v2/fake_crypto.py b/novaclient/tests/functional/v2/fake_crypto.py new file mode 100644 index 000000000..56df5151a --- /dev/null +++ b/novaclient/tests/functional/v2/fake_crypto.py @@ -0,0 +1,49 @@ +# Copyright 2015 Cloudbase Solutions +# 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. + + +def get_x509_cert_and_fingerprint(): + fingerprint = "a1:6f:6d:ea:a6:36:d0:3a:c6:eb:b6:ee:07:94:3e:2a:90:98:2b:c9" + certif = ( + "-----BEGIN CERTIFICATE-----\n" + "MIIDIjCCAgqgAwIBAgIJAIE8EtWfZhhFMA0GCSqGSIb3DQEBCwUAMCQxIjAgBgNV\n" + "BAMTGWNsb3VkYmFzZS1pbml0LXVzZXItMTM1NTkwHhcNMTUwMTI5MTgyMzE4WhcN\n" + "MjUwMTI2MTgyMzE4WjAkMSIwIAYDVQQDExljbG91ZGJhc2UtaW5pdC11c2VyLTEz\n" + "NTU5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv4lv95ofkXLIbALU\n" + "UEb1f949TYNMUvMGNnLyLgGOY+D61TNG7RZn85cRg9GVJ7KDjSLN3e3LwH5rgv5q\n" + "pU+nM/idSMhG0CQ1lZeExTsMEJVT3bG7LoU5uJ2fJSf5+hA0oih2M7/Kap5ggHgF\n" + "h+h8MWvDC9Ih8x1aadkk/OEmJsTrziYm0C/V/FXPHEuXfZn8uDNKZ/tbyfI6hwEj\n" + "nLz5Zjgg29n6tIPYMrnLNDHScCwtNZOcnixmWzsxCt1bxsAEA/y9gXUT7xWUf52t\n" + "2+DGQbLYxo0PHjnPf3YnFXNavfTt+4c7ZdHhOQ6ZA8FGQ2LJHDHM1r2/8lK4ld2V\n" + "qgNTcQIDAQABo1cwVTATBgNVHSUEDDAKBggrBgEFBQcDAjA+BgNVHREENzA1oDMG\n" + "CisGAQQBgjcUAgOgJQwjY2xvdWRiYXNlLWluaXQtdXNlci0xMzU1OUBsb2NhbGhv\n" + "c3QwDQYJKoZIhvcNAQELBQADggEBAHHX/ZUOMR0ZggQnfXuXLIHWlffVxxLOV/bE\n" + "7JC/dtedHqi9iw6sRT5R6G1pJo0xKWr2yJVDH6nC7pfxCFkby0WgVuTjiu6iNRg2\n" + "4zNJd8TGrTU+Mst+PPJFgsxrAY6vjwiaUtvZ/k8PsphHXu4ON+oLurtVDVgog7Vm\n" + "fQCShx434OeJj1u8pb7o2WyYS5nDVrHBhlCAqVf2JPKu9zY+i9gOG2kimJwH7fJD\n" + "xXpMIwAQ+flwlHR7OrE0L8TNcWwKPRAY4EPcXrT+cWo1k6aTqZDSK54ygW2iWtni\n" + "ZBcstxwcB4GIwnp1DrPW9L2gw5eLe1Sl6wdz443TW8K/KPV9rWQ=\n" + "-----END CERTIFICATE-----\n") + return certif, fingerprint + + +def get_ssh_pub_key_and_fingerprint(): + fingerprint = "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c" + public_key = ("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGg" + "B4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0l" + "RE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv" + "9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYc" + "pSxsIbECHw== Generated-by-Nova") + return public_key, fingerprint diff --git a/tests/v1_1/__init__.py b/novaclient/tests/functional/v2/legacy/__init__.py similarity index 100% rename from tests/v1_1/__init__.py rename to novaclient/tests/functional/v2/legacy/__init__.py diff --git a/novaclient/tests/functional/v2/legacy/test_consoles.py b/novaclient/tests/functional/v2/legacy/test_consoles.py new file mode 100644 index 000000000..094531e42 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_consoles.py @@ -0,0 +1,61 @@ +# Copyright 2015 IBM Corp. +# 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 tempest.lib import exceptions + +from novaclient.tests.functional import base + + +class TestConsolesNovaClient(base.ClientTestBase): + """Consoles functional tests.""" + + COMPUTE_API_VERSION = "2.1" + + def _test_console_get(self, command, expected_response_type): + server = self._create_server() + completed_command = command % server.id + + try: + output = self.nova(completed_command) + # if we didn't fail, check that the expected response type is in + # the output + console_type = self._get_column_value_from_single_row_table( + output, 'Type') + self.assertEqual(expected_response_type, console_type, output) + except exceptions.CommandFailed as cf: + self.assertIn('HTTP 400', str(cf.stderr)) + + def _test_vnc_console_get(self): + self._test_console_get('get-vnc-console %s novnc', 'novnc') + + def _test_spice_console_get(self): + self._test_console_get('get-spice-console %s spice-html5', + 'spice-html5') + + def _test_rdp_console_get(self): + self._test_console_get('get-rdp-console %s rdp-html5', 'rdp-html5') + + def _test_serial_console_get(self): + self._test_console_get('get-serial-console %s', 'serial') + + def test_vnc_console_get(self): + self._test_vnc_console_get() + + def test_spice_console_get(self): + self._test_spice_console_get() + + def test_rdp_console_get(self): + self._test_rdp_console_get() + + def test_serial_console_get(self): + self._test_serial_console_get() diff --git a/novaclient/tests/functional/v2/legacy/test_extended_attributes.py b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py new file mode 100644 index 000000000..2b2378485 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_extended_attributes.py @@ -0,0 +1,53 @@ +# 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_serialization import jsonutils + +from novaclient.tests.functional import base + + +class TestExtAttrNovaClient(base.ClientTestBase): + """Functional tests for os-extended-server-attributes""" + + COMPUTE_API_VERSION = "2.1" + + def _create_server_and_attach_volume(self): + server = self._create_server() + 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) + self.wait_for_volume_status(volume, 'in-use') + return server, volume + + def _release_volume(self, server, volume): + self.nova('volume-detach', params="%s %s" % (server.id, volume.id)) + self.wait_for_volume_status(volume, 'available') + + def test_extended_server_attributes(self): + server, volume = self._create_server_and_attach_volume() + table = self.nova('show %s' % server.id) + # Check that attributes listed below exist in 'nova show' table and + # they are exactly Property attributes (not an instance's name, e.g.) + # The _get_value_from_the_table() will raise an exception + # if attr is not a key (first column) of the table dict + for attr in ['OS-EXT-SRV-ATTR:host', + 'OS-EXT-SRV-ATTR:hypervisor_hostname', + 'OS-EXT-SRV-ATTR:instance_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 + 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', 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 new file mode 100644 index 000000000..aa8bb415c --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_flavor_access.py @@ -0,0 +1,64 @@ +# 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 TestFlvAccessNovaClient(base.ProjectTestBase): + """Functional tests for flavors with public and non-public access""" + + COMPUTE_API_VERSION = "2.1" + + def test_public_flavor_list(self): + # Check that flavors with public access are available for both admin + # and non-admin tenants + flavor_list1 = self.nova('flavor-list') + flavor_list2 = self.another_nova('flavor-list') + self.assertEqual(flavor_list1, flavor_list2) + + 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() + 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') + self.assertNotIn(flv_name, flavor_list1) + flavor_list2 = self.nova('flavor-list --all') + flavor_list3 = self.another_nova('flavor-list --all') + self.assertIn(flv_name, flavor_list2) + self.assertNotIn(flv_name, flavor_list3) + + 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() + 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" % + (flv_name, self.project_id)) + self.assertIn(self.project_id, + self.nova('flavor-access-list --flavor %s' % flv_name)) + + def test_add_access_public_flavor(self): + # For microversion < 2.7 the 'flavor-access-add' operation is executed + # successfully for public flavor, but the next operation, + # 'flavor-access-list --flavor %(name_of_public_flavor)' returns + # a CommandError + 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)) + output = self.nova('flavor-access-list --flavor %s' % flv_name, + fail_ok=True, merge_stderr=True) + self.assertIn("CommandError", output) diff --git a/novaclient/tests/functional/v2/legacy/test_hypervisors.py b/novaclient/tests/functional/v2/legacy/test_hypervisors.py new file mode 100644 index 000000000..621401f9b --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_hypervisors.py @@ -0,0 +1,42 @@ +# 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 +from novaclient import utils + + +class TestHypervisors(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.1" + + def _test_list(self, cpu_info_type, uuid_as_id=False): + hypervisors = self.client.hypervisors.list() + if not len(hypervisors): + self.fail("No hypervisors detected.") + for hypervisor in hypervisors: + self.assertIsInstance(hypervisor.cpu_info, cpu_info_type) + if uuid_as_id: + # microversion >= 2.53 returns a uuid for the id + self.assertFalse(utils.is_integer_like(hypervisor.id), + 'Expected hypervisor.id to be a UUID.') + self.assertFalse( + utils.is_integer_like(hypervisor.service['id']), + 'Expected hypervisor.service.id to be a UUID.') + else: + self.assertTrue(utils.is_integer_like(hypervisor.id), + 'Expected hypervisor.id to be an integer.') + self.assertTrue( + utils.is_integer_like(hypervisor.service['id']), + 'Expected hypervisor.service.id to be an integer.') + + def test_list(self): + self._test_list(str) diff --git a/novaclient/tests/functional/v2/legacy/test_instances.py b/novaclient/tests/functional/v2/legacy/test_instances.py new file mode 100644 index 000000000..f933ec1e7 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_instances.py @@ -0,0 +1,70 @@ +# 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 TestInstanceCLI(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.1" + + def test_attach_volume(self): + """Test we can attach a volume via the cli. + + This test was added after bug 1423695. That bug exposed + inconsistencies in how to talk to API services from the CLI + vs. API level. The volumes api calls that were designed to + populate the completion cache were incorrectly routed to the + Nova endpoint. Novaclient volumes support actually talks to + Cinder endpoint directly. + + This would case volume-attach to return a bad error code, + however it does this *after* the attach command is correctly + dispatched. So the volume-attach still works, but the user is + presented a 404 error. + + This test ensures we can do a through path test of: boot, + create volume, attach volume, detach volume, delete volume, + destroy. + + """ + name = self.name_generate() + + # Boot via the cli, as we're primarily testing the cli in this test + self.nova('boot', + params="--flavor %s --image %s %s --nic net-id=%s --poll" % + (self.flavor.name, self.image.name, name, self.network.id)) + + # Be nice about cleaning up, however, use the API for this to avoid + # parsing text. + servers = self.client.servers.list(search_opts={"name": name}) + # the name is a random uuid, there better only be one + self.assertEqual(1, len(servers), servers) + server = servers[0] + self.addCleanup(server.delete) + + # create a volume for attachment + 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') + + # attach the volume + self.nova('volume-attach', params="%s %s" % (name, volume.id)) + + # volume needs to transition to 'in-use' to be attached + self.wait_for_volume_status(volume, 'in-use') + + # clean up on success + self.nova('volume-detach', params="%s %s" % (name, volume.id)) + 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 new file mode 100644 index 000000000..1e04781df --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_keypairs.py @@ -0,0 +1,92 @@ +# 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 tempfile + +from tempest.lib import exceptions + +from novaclient.tests.functional import base +from novaclient.tests.functional.v2 import fake_crypto + + +class TestKeypairsNovaClient(base.ClientTestBase): + """Keypairs functional tests.""" + + COMPUTE_API_VERSION = "2.1" + + def _serialize_kwargs(self, kwargs): + kwargs_pairs = ['--%(key)s %(val)s' % {'key': key.replace('_', '-'), + 'val': val} + for key, val in kwargs.items()] + return " ".join(kwargs_pairs) + + def _create_keypair(self, **kwargs): + key_name = self._raw_create_keypair(**kwargs) + self.addCleanup(self.nova, 'keypair-delete %s' % key_name) + return key_name + + def _raw_create_keypair(self, **kwargs): + key_name = self.name_generate() + kwargs_str = self._serialize_kwargs(kwargs) + self.nova('keypair-add %s %s' % (kwargs_str, key_name)) + return key_name + + def _show_keypair(self, key_name): + return self.nova('keypair-show %s' % key_name) + + def _list_keypairs(self): + return self.nova('keypair-list') + + def _delete_keypair(self, key_name): + self.nova('keypair-delete %s' % key_name) + + def _create_public_key_file(self, public_key): + pubfile = tempfile.mkstemp()[1] + with open(pubfile, 'w') as f: + f.write(public_key) + return pubfile + + def test_create_keypair(self): + key_name = self._create_keypair() + keypair = self._show_keypair(key_name) + self.assertIn(key_name, keypair) + + return keypair + + def _test_import_keypair(self, fingerprint, **create_kwargs): + key_name = self._create_keypair(**create_kwargs) + keypair = self._show_keypair(key_name) + self.assertIn(key_name, keypair) + self.assertIn(fingerprint, keypair) + + return keypair + + def test_import_keypair(self): + pub_key, fingerprint = fake_crypto.get_ssh_pub_key_and_fingerprint() + pub_key_file = self._create_public_key_file(pub_key) + self._test_import_keypair(fingerprint, pub_key=pub_key_file) + + def test_list_keypair(self): + key_name = self._create_keypair() + keypairs = self._list_keypairs() + self.assertIn(key_name, keypairs) + + def test_delete_keypair(self): + key_name = self._raw_create_keypair() + keypair = self._show_keypair(key_name) + self.assertIsNotNone(keypair) + + self._delete_keypair(key_name) + + # keypair-show should fail if no keypair with given name is found. + self.assertRaises(exceptions.CommandFailed, + self._show_keypair, key_name) diff --git a/novaclient/tests/functional/v2/legacy/test_os_services.py b/novaclient/tests/functional/v2/legacy/test_os_services.py new file mode 100644 index 000000000..92d426518 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_os_services.py @@ -0,0 +1,74 @@ +# 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 TestOsServicesNovaClient(base.ClientTestBase): + """Functional tests for os-services attributes""" + + COMPUTE_API_VERSION = "2.1" + + def test_os_services_list(self): + table = self.nova('service-list') + for serv in self.client.services.list(): + self.assertIn(serv.binary, table) + + def test_os_service_disable_enable(self): + # Disable and enable Nova services in accordance with list of nova + # 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.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 + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + 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' % 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' % host) + status = self._get_column_value_from_single_row_table( + service, 'Status') + self.assertEqual('enabled', status) + + def test_os_service_disable_log_reason(self): + for serv in self.client.services.list(): + # In Pike the os-services API was made multi-cell aware and it + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + 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' % 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( + service, 'Disabled Reason') + self.assertEqual('disabled', status) + self.assertEqual('test_disable', log_reason) diff --git a/novaclient/tests/functional/v2/legacy/test_quotas.py b/novaclient/tests/functional/v2/legacy/test_quotas.py new file mode 100644 index 000000000..6eeddbe19 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_quotas.py @@ -0,0 +1,49 @@ +# 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 TestQuotasNovaClient(base.ClientTestBase): + """Nova quotas functional tests.""" + + COMPUTE_API_VERSION = "2.1" + + _quota_resources = ['instances', 'cores', 'ram', + 'floating_ips', 'fixed_ips', 'metadata_items', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes', 'key_pairs', + 'security_groups', 'security_group_rules', + 'server_groups', 'server_group_members'] + + def test_quotas_update(self): + # `nova quota-update` requires tenant-id. + tenant_id = self._get_project_id(self.cli_clients.tenant_name) + + self.addCleanup(self.client.quotas.delete, tenant_id) + + original_quotas = self.client.quotas.get(tenant_id) + + difference = 10 + params = [tenant_id] + for quota_name in self._quota_resources: + params.append("--%(name)s %(value)s" % { + "name": quota_name.replace("_", "-"), + "value": getattr(original_quotas, quota_name) + difference}) + + self.nova("quota-update", params=" ".join(params)) + + updated_quotas = self.client.quotas.get(tenant_id) + + for quota_name in self._quota_resources: + self.assertEqual(getattr(original_quotas, quota_name), + getattr(updated_quotas, quota_name) - difference) diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py new file mode 100644 index 000000000..7dffba414 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py @@ -0,0 +1,140 @@ +# 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 tempest.lib import decorators +from tempest.lib import exceptions + +from novaclient.tests.functional import base + + +class SimpleReadOnlyNovaClientTest(base.ClientTestBase): + + """Read only functional python-novaclient tests. + + This only exercises client commands that are read only. + """ + + COMPUTE_API_VERSION = "2.1" + + def test_admin_fake_action(self): + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'this-does-nova-exist') + + # NOTE(jogo): Commands in order listed in 'nova help' + + def test_admin_aggregate_list(self): + self.nova('aggregate-list') + + def test_admin_availability_zone_list(self): + self.assertIn("internal", self.nova('availability-zone-list')) + + def test_admin_flavor_access_list(self): + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'flavor-access-list') + # Failed to get access list for public flavor type + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'flavor-access-list', + params='--flavor m1.tiny') + + def test_admin_flavor_list(self): + self.assertIn("Memory_MiB", self.nova('flavor-list')) + + def test_admin_hypervisor_list(self): + self.nova('hypervisor-list') + + @decorators.skip_because(bug="1157349") + def test_admin_interface_list(self): + self.nova('interface-list') + + def test_admin_keypair_list(self): + self.nova('keypair-list') + + def test_admin_list(self): + self.nova('list') + self.nova('list', params='--all-tenants 1') + self.nova('list', params='--all-tenants 0') + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'list', + params='--all-tenants bad') + + def test_admin_server_group_list(self): + self.nova('server-group-list') + + def test_admin_service_list(self): + self.nova('service-list') + + def test_admin_usage(self): + self.nova('usage') + + def test_admin_usage_list(self): + self.nova('usage-list') + + def test_admin_help(self): + self.nova('help') + + def test_agent_list(self): + 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') + self.nova('migration-list', flags='--debug') + + def test_version_list(self): + self.nova('version-list', flags='--debug') + + def test_quota_defaults(self): + self.nova('quota-defaults') + self.nova('quota-defaults', flags='--debug') + + def test_bash_completion(self): + self.nova('bash-completion') + + # Optional arguments: + + def test_admin_version(self): + self.nova('', flags='--version') + + def test_admin_debug_list(self): + self.nova('list', flags='--debug') + + def test_admin_timeout(self): + self.nova('list', flags='--timeout %d' % 60) + + def test_admin_timing(self): + self.nova('list', flags='--timing') + + def test_admin_invalid_bypass_url(self): + self.assertRaises(exceptions.CommandFailed, + self.nova, + 'list', + 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 new file mode 100644 index 000000000..a1ab151b4 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_server_groups.py @@ -0,0 +1,47 @@ +# Copyright 2015 Huawei Technology corp. +# 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 TestServerGroupClient(base.ClientTestBase): + """Server groups v2.1 functional tests.""" + + COMPUTE_API_VERSION = "2.1" + + def _create_sg(self, policy): + 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 + + def test_create_server_group(self): + sg_id = self._create_sg("affinity") + 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) + + def test_list_server_group(self): + sg_id = self._create_sg("affinity") + self.addCleanup(self.nova, 'server-group-delete %s' % sg_id) + sg = self.nova('server-group-list') + result = self._get_column_value_from_single_row_table(sg, "Id") + self.assertEqual(sg_id, result) + + def test_delete_server_group(self): + sg_id = self._create_sg("affinity") + sg = self.nova('server-group-get %s' % sg_id) + result = self._get_column_value_from_single_row_table(sg, "Id") + self.assertIsNotNone(result) + self.nova('server-group-delete %s' % sg_id) diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py new file mode 100644 index 000000000..ded7eb3a5 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -0,0 +1,178 @@ +# 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 TestServersBootNovaClient(base.ClientTestBase): + """Servers boot functional tests.""" + + COMPUTE_API_VERSION = "2.1" + + def _boot_server_with_legacy_bdm(self, bdm_params=()): + volume_size = 1 + 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'): + delete_volume = False + else: + delete_volume = True + + bdm_params = ':'.join(bdm_params) + if bdm_params: + bdm_params = ''.join((':', bdm_params)) + + params = ( + "%(name)s --flavor %(flavor)s --poll " + "--block-device-mapping vda=%(volume_id)s%(bdm_params)s" % { + "name": self.name_generate(), "flavor": + self.flavor.id, + "volume_id": volume.id, + "bdm_params": bdm_params}) + # 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.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + + if delete_volume: + 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) + + def test_boot_server_with_legacy_bdm_volume_id_only(self): + self._boot_server_with_legacy_bdm() + + 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": self.name_generate(), + "image": self.image.id, + "flavor": self.flavor.id, + "net-name": self.network.name})) + server_id = self._get_value_from_the_table(server_info, "id") + + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + + def test_boot_server_using_image_with(self): + """Scenario test which does the following: + + 1. Create a server. + 2. Create a snapshot image of the server with a special meta key. + 3. Create a second server using the --image-with option using the meta + key stored in the snapshot image created in step 2. + """ + params = ( + '--flavor %(flavor)s --image %(image)s --poll ' + 'image-with-server-1' % {'image': self.image.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) + + # create a snapshot of the server with an image metadata key + snapshot_info = self.nova('image-create', params=( + '--metadata image_with_meta=%(meta_value)s ' + '--show --poll %(server_id)s image-with-snapshot' % { + 'meta_value': server_id, + 'server_id': server_id})) + + # 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.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) + + 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}) + # 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) + + +class TestServersListNovaClient(base.ClientTestBase): + """Servers list functional tests.""" + + COMPUTE_API_VERSION = "2.1" + + def _create_servers(self, name, number): + return [self._create_server(name) for i in range(number)] + + def test_list_with_limit(self): + 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 + servers = output.split("\n")[3:-2] + self.assertEqual(1, len(servers), output) + + def test_list_with_changes_since(self): + now = datetime.datetime.isoformat(timeutils.utcnow()) + name = self.name_generate() + self._create_servers(name, 1) + output = self.nova("list", params="--changes-since %s" % now) + self.assertIn(name, output, output) + now = datetime.datetime.isoformat(timeutils.utcnow()) + output = self.nova("list", params="--changes-since %s" % now) + self.assertNotIn(name, output, output) + + def test_list_all_servers(self): + 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 + output = self.nova("list", params="--limit -1 --name %s" % name) + # Cut header and footer of the table + for server in precreated_servers: + self.assertIn(server.id, output) + + def test_list_minimal(self): + 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, 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 new file mode 100644 index 000000000..11080c135 --- /dev/null +++ b/novaclient/tests/functional/v2/legacy/test_usage.py @@ -0,0 +1,74 @@ +# 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 novaclient.tests.functional import base + + +class TestUsageCLI(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.1' + + def _get_num_servers_from_usage_output(self): + output = self.nova('usage') + servers = self._get_column_value_from_single_row_table( + output, 'Servers') + return int(servers) + + def _get_num_servers_by_tenant_from_usage_output(self): + tenant_id = self._get_project_id(self.cli_clients.tenant_name) + output = self.nova('usage --tenant=%s' % tenant_id) + servers = self._get_column_value_from_single_row_table( + output, 'Servers') + return int(servers) + + def test_usage(self): + before = self._get_num_servers_from_usage_output() + 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() + after = self._get_num_servers_by_tenant_from_usage_output() + self.assertGreater(after, before) + + +class TestUsageClient(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.1' + + def _create_servers_in_time_window(self): + start = datetime.datetime.now() + self._create_server() + self._create_server() + end = datetime.datetime.now() + return start, end + + def test_get(self): + start, end = self._create_servers_in_time_window() + tenant_id = self._get_project_id(self.cli_clients.tenant_name) + usage = self.client.usage.get(tenant_id, start=start, end=end) + self.assertEqual(tenant_id, usage.tenant_id) + self.assertGreaterEqual(len(usage.server_usages), 2) + + def test_list(self): + start, end = self._create_servers_in_time_window() + tenant_id = self._get_project_id(self.cli_clients.tenant_name) + usages = self.client.usage.list(start=start, end=end, detailed=True) + tenant_ids = [usage.tenant_id for usage in usages] + self.assertIn(tenant_id, tenant_ids) + for usage in usages: + if usage.tenant_id == tenant_id: + self.assertGreaterEqual(len(usage.server_usages), 2) diff --git a/novaclient/tests/functional/v2/test_aggregates.py b/novaclient/tests/functional/v2/test_aggregates.py new file mode 100644 index 000000000..f3f5f032f --- /dev/null +++ b/novaclient/tests/functional/v2/test_aggregates.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.functional import base + + +class TestAggregatesNovaClient(base.ClientTestBase): + COMPUTE_API_VERSION = '2.1' + + def setUp(self): + super(TestAggregatesNovaClient, self).setUp() + self.agg1 = self.name_generate() + self.agg2 = self.name_generate() + self.addCleanup(self._clean_aggregates) + + def _clean_aggregates(self): + for a in (self.agg1, self.agg2): + try: + self.nova('aggregate-delete', params=a) + except Exception: + pass + + def test_aggregate_update_name(self): + self.nova('aggregate-create', params=self.agg1) + self.nova('aggregate-update', + params='--name=%s %s' % (self.agg2, self.agg1)) + output = self.nova('aggregate-show', params=self.agg2) + self.assertIn(self.agg2, output) + self.nova('aggregate-delete', params=self.agg2) + + def test_aggregate_update_az(self): + self.nova('aggregate-create', params=self.agg2) + self.nova('aggregate-update', + params='--availability-zone=myaz %s' % self.agg2) + output = self.nova('aggregate-show', params=self.agg2) + self.assertIn('myaz', output) + self.nova('aggregate-delete', params=self.agg2) diff --git a/novaclient/tests/functional/v2/test_consoles.py b/novaclient/tests/functional/v2/test_consoles.py new file mode 100644 index 000000000..eaad45d49 --- /dev/null +++ b/novaclient/tests/functional/v2/test_consoles.py @@ -0,0 +1,41 @@ +# Copyright 2015 IBM Corp. +# 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.v2.legacy import test_consoles + + +class TestConsolesNovaClientV26(test_consoles.TestConsolesNovaClient): + """Consoles functional tests for >=v2.6 api microversions.""" + + COMPUTE_API_VERSION = "2.6" + + def test_vnc_console_get(self): + self._test_vnc_console_get() + + def test_spice_console_get(self): + self._test_spice_console_get() + + def test_rdp_console_get(self): + self._test_rdp_console_get() + + def test_serial_console_get(self): + self._test_serial_console_get() + + +class TestConsolesNovaClientV28(test_consoles.TestConsolesNovaClient): + """Consoles functional tests for >=v2.8 api microversions.""" + + COMPUTE_API_VERSION = "2.8" + + def test_webmks_console_get(self): + self._test_console_get('get-mks-console %s ', 'webmks') diff --git a/novaclient/tests/functional/v2/test_device_tagging.py b/novaclient/tests/functional/v2/test_device_tagging.py new file mode 100644 index 000000000..5909137ea --- /dev/null +++ b/novaclient/tests/functional/v2/test_device_tagging.py @@ -0,0 +1,169 @@ +# Copyright (C) 2016, 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. + +from tempest.lib import exceptions + +from novaclient.tests.functional import base + + +class TestBlockDeviceTaggingCLIError(base.ClientTestBase): + """Negative test that asserts that creating a server with a tagged + block device with a specific microversion will fail. + """ + + COMPUTE_API_VERSION = "2.31" + + def test_boot_server_with_tagged_block_devices_with_error(self): + try: + output = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--nic net-id=%(net-uuid)s ' + '--block-device ' + 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' + 'shutdown=remove,tag=bar' % {'name': self.name_generate(), + 'flavor': self.flavor.id, + 'net-uuid': self.network.id, + 'image': self.image.id})) + except exceptions.CommandFailed as e: + self.assertIn("ERROR (CommandError): " + "'tag' in block device mapping is not supported " + "in API version %s." % self.COMPUTE_API_VERSION, + str(e)) + else: + server_id = self._get_value_from_the_table(output, 'id') + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + self.fail("Booting a server with block device tag is not failed.") + + +class TestNICDeviceTaggingCLIError(base.ClientTestBase): + """Negative test that asserts that creating a server with a tagged + nic with a specific microversion will fail. + """ + + COMPUTE_API_VERSION = "2.31" + + def test_boot_server_with_tagged_nic_devices_with_error(self): + try: + output = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--nic net-id=%(net-uuid)s,tag=foo ' + '--block-device ' + 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' + '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', str(e)) + else: + server_id = self._get_value_from_the_table(output, 'id') + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + self.fail("Booting a server with network interface tag " + "is not failed.") + + +class TestBlockDeviceTaggingCLI(base.ClientTestBase): + """Tests that creating a server with a tagged block device will work + with the 2.32 microversion, where the feature was originally added. + """ + + COMPUTE_API_VERSION = "2.32" + + def test_boot_server_with_tagged_block_devices(self): + server_info = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--nic net-id=%(net-uuid)s ' + '--block-device ' + 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' + 'shutdown=remove,tag=bar' % {'name': self.name_generate(), + 'flavor': self.flavor.id, + 'net-uuid': self.network.id, + 'image': self.image.id})) + server_id = self._get_value_from_the_table(server_info, 'id') + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + + +class TestNICDeviceTaggingCLI(base.ClientTestBase): + """Tests that creating a server with a tagged nic will work + with the 2.32 microversion, where the feature was originally added. + """ + + COMPUTE_API_VERSION = "2.32" + + def test_boot_server_with_tagged_nic_devices(self): + server_info = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--nic net-id=%(net-uuid)s,tag=foo ' + '--block-device ' + 'source=image,dest=volume,id=%(image)s,size=1,bootindex=0,' + 'shutdown=remove' % {'name': self.name_generate(), + 'flavor': self.flavor.id, + 'net-uuid': self.network.id, + 'image': self.image.id})) + server_id = self._get_value_from_the_table(server_info, 'id') + self.client.servers.delete(server_id) + self.wait_for_resource_delete(server_id, self.client.servers) + + +class TestDeviceTaggingCLIV233(TestBlockDeviceTaggingCLIError, + TestNICDeviceTaggingCLI): + """Tests that in microversion 2.33, creating a server with a tagged + block device will fail, but creating a server with a tagged nic will + succeed. + """ + + COMPUTE_API_VERSION = "2.33" + + +class TestDeviceTaggingCLIV236(TestBlockDeviceTaggingCLIError, + TestNICDeviceTaggingCLI): + """Tests that in microversion 2.36, creating a server with a tagged + block device will fail, but creating a server with a tagged nic will + succeed. This is testing the boundary before 2.37 where nic tagging + was broken. + """ + + COMPUTE_API_VERSION = "2.36" + + +class TestDeviceTaggingCLIV237(TestBlockDeviceTaggingCLIError, + TestNICDeviceTaggingCLIError): + """Tests that in microversion 2.37, creating a server with either a + tagged block device or tagged nic would fail. + """ + + COMPUTE_API_VERSION = "2.37" + + +class TestDeviceTaggingCLIV241(TestBlockDeviceTaggingCLIError, + TestNICDeviceTaggingCLIError): + """Tests that in microversion 2.41, creating a server with either a + tagged block device or tagged nic would fail. This is testing the + boundary before 2.42 where block device tags and nic tags were fixed + for server create requests. + """ + + COMPUTE_API_VERSION = "2.41" + + +class TestDeviceTaggingCLIV242(TestBlockDeviceTaggingCLI, + TestNICDeviceTaggingCLI): + """Tests that in microversion 2.42 you could once again create a server + with a tagged block device or a tagged nic. + """ + + COMPUTE_API_VERSION = "2.42" diff --git a/novaclient/tests/functional/v2/test_extended_attributes.py b/novaclient/tests/functional/v2/test_extended_attributes.py new file mode 100644 index 000000000..bf06875c6 --- /dev/null +++ b/novaclient/tests/functional/v2/test_extended_attributes.py @@ -0,0 +1,43 @@ +# 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_serialization import jsonutils + +from novaclient.tests.functional.v2.legacy import test_extended_attributes + + +class TestExtAttrNovaClientV23(test_extended_attributes.TestExtAttrNovaClient): + """Functional tests for os-extended-server-attributes, microversion 2.3""" + + COMPUTE_API_VERSION = "2.3" + + def test_extended_server_attributes(self): + server, volume = self._create_server_and_attach_volume() + table = self.nova('show %s' % server.id) + # Check that attributes listed below exist in 'nova show' table and + # they are exactly Property attributes (not an instance's name, e.g.) + # The _get_value_from_the_table() will raise an exception + # if attr is not a key of the table dict (first column) + for attr in ['OS-EXT-SRV-ATTR:reservation_id', + 'OS-EXT-SRV-ATTR:launch_index', + 'OS-EXT-SRV-ATTR:ramdisk_id', + 'OS-EXT-SRV-ATTR:kernel_id', + 'OS-EXT-SRV-ATTR:hostname', + '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 + volume_attr = self._get_value_from_the_table( + table, 'os-extended-volumes:volumes_attached') + # Check that 'delete_on_termination' exists as a key + # of volume_attr dict + 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 new file mode 100644 index 000000000..23ef030ec --- /dev/null +++ b/novaclient/tests/functional/v2/test_flavor_access.py @@ -0,0 +1,33 @@ +# 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.v2.legacy import test_flavor_access + + +class TestFlvAccessNovaClientV27(test_flavor_access.TestFlvAccessNovaClient): + """Check that an attempt to grant an access to a public flavor + for the given tenant fails with Conflict error in accordance with + 2.7 microversion REST API History + """ + + COMPUTE_API_VERSION = "2.7" + + def test_add_access_public_flavor(self): + 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' % + (flv_name, self.project_id), + fail_ok=True, merge_stderr=True) + self.assertIn("ERROR (Conflict): " + "Can not add access to a public flavor. (HTTP 409) ", + output) diff --git a/novaclient/tests/functional/v2/test_hypervisors.py b/novaclient/tests/functional/v2/test_hypervisors.py new file mode 100644 index 000000000..bb58648b9 --- /dev/null +++ b/novaclient/tests/functional/v2/test_hypervisors.py @@ -0,0 +1,41 @@ +# 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.v2.legacy import test_hypervisors + + +class TestHypervisorsV28(test_hypervisors.TestHypervisors): + + COMPUTE_API_VERSION = "2.28" + + def test_list(self): + self._test_list(dict) + + +class TestHypervisorsV2_53(TestHypervisorsV28): + COMPUTE_API_VERSION = "2.53" + + 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_image_meta.py b/novaclient/tests/functional/v2/test_image_meta.py new file mode 100644 index 000000000..83a285a22 --- /dev/null +++ b/novaclient/tests/functional/v2/test_image_meta.py @@ -0,0 +1,31 @@ +# Copyright 2016 Mirantis, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.functional import base + + +class TestImageMetaV239(base.ClientTestBase): + """Functional tests for image-meta proxy API.""" + + # 'image-metadata' proxy API was deprecated in 2.39 but the CLI should + # fallback to 2.35 and emit a warning. + COMPUTE_API_VERSION = "2.39" + + def test_limits(self): + """Tests that 2.39 won't return 'maxImageMeta' resource limit and + the CLI output won't show it. + """ + output = self.nova('limits') + # assert that MaxImageMeta isn't in the table output + self.assertRaises(ValueError, self._get_value_from_the_table, + output, 'maxImageMeta') diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py new file mode 100644 index 000000000..1578d1012 --- /dev/null +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -0,0 +1,215 @@ +# 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 time + +from oslo_utils import timeutils +from oslo_utils import uuidutils +from tempest.lib import exceptions + +from novaclient.tests.functional import base + + +class TestInstanceActionCLI(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.21" + + 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):", str(e)) + else: + self.fail("%s is not failed on non existing instance." % cmd) + + def test_show_action_with_not_existing_instance(self): + name_or_uuid = uuidutils.generate_uuid() + request_id = uuidutils.generate_uuid() + self._test_cmd_with_not_existing_instance( + "instance-action", "%s %s" % (name_or_uuid, request_id)) + + def test_list_actions_with_not_existing_instance(self): + name_or_uuid = uuidutils.generate_uuid() + self._test_cmd_with_not_existing_instance("instance-action-list", + name_or_uuid) + + def test_show_and_list_actions_on_deleted_instance(self): + server = self._create_server(add_cleanup=False) + server.delete() + self.wait_for_resource_delete(server, self.client.servers) + + output = self.nova("instance-action-list %s" % server.id) + # NOTE(andreykurilin): output is not a single row table, so we can + # obtain just "create" action. It should be enough for testing + # "nova instance-action " command + request_id = self._get_column_value_from_single_row_table( + output, "Request_ID") + + output = self.nova("instance-action %s %s" % (server.id, request_id)) + + # 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/v1_1/floating_ip_pools.py b/novaclient/tests/functional/v2/test_instances.py similarity index 54% rename from novaclient/v1_1/floating_ip_pools.py rename to novaclient/tests/functional/v2/test_instances.py index 7666bd57f..289c53df4 100644 --- a/novaclient/v1_1/floating_ip_pools.py +++ b/novaclient/tests/functional/v2/test_instances.py @@ -1,7 +1,3 @@ -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# -# All Rights Reserved. -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -14,19 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient import base - - -class FloatingIPPool(base.Resource): - def __repr__(self): - return "" % self.name +from novaclient.tests.functional.v2.legacy import test_instances -class FloatingIPPoolManager(base.ManagerWithFind): - resource_class = FloatingIPPool +class TestInstanceCLI(test_instances.TestInstanceCLI): - def list(self): - """ - Retrieve a list of all floating ip pools. - """ - return self._list('/os-floating-ip-pools', 'floating_ip_pools') + COMPUTE_API_VERSION = "2.latest" diff --git a/novaclient/tests/functional/v2/test_keypairs.py b/novaclient/tests/functional/v2/test_keypairs.py new file mode 100644 index 000000000..14133be12 --- /dev/null +++ b/novaclient/tests/functional/v2/test_keypairs.py @@ -0,0 +1,120 @@ +# 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 +from novaclient.tests.functional.v2 import fake_crypto +from novaclient.tests.functional.v2.legacy import test_keypairs + + +class TestKeypairsNovaClientV22(test_keypairs.TestKeypairsNovaClient): + """Keypairs functional tests for v2.2 nova-api microversion.""" + + COMPUTE_API_VERSION = "2.2" + + def test_create_keypair(self): + keypair = super(TestKeypairsNovaClientV22, self).test_create_keypair() + self.assertIn('ssh', keypair) + + def test_create_keypair_x509(self): + key_name = self._create_keypair(key_type='x509') + keypair = self._show_keypair(key_name) + self.assertIn(key_name, keypair) + self.assertIn('x509', keypair) + + def test_import_keypair(self): + pub_key, fingerprint = fake_crypto.get_ssh_pub_key_and_fingerprint() + pub_key_file = self._create_public_key_file(pub_key) + keypair = self._test_import_keypair(fingerprint, pub_key=pub_key_file) + self.assertIn('ssh', keypair) + + def test_import_keypair_x509(self): + certif, fingerprint = fake_crypto.get_x509_cert_and_fingerprint() + pub_key_file = self._create_public_key_file(certif) + keypair = self._test_import_keypair(fingerprint, key_type='x509', + pub_key=pub_key_file) + self.assertIn('x509', keypair) + + +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() + 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") + self.assertRaises(ValueError, self._get_value_from_the_table, + output, name) + output_1 = self.another_nova("keypair-list") + output_2 = self.nova("keypair-list --user %s" % self.user_id) + self.assertEqual(output_1, output_2) + # it should be table with one key-pair + self.assertEqual(name, self._get_column_value_from_single_row_table( + output_1, "Name")) + + output_1 = self.another_nova("keypair-show %s " % name) + output_2 = self.nova("keypair-show --user %s %s" % (self.user_id, + name)) + self.assertEqual(output_1, output_2) + self.assertEqual(self.user_id, + self._get_value_from_the_table(output_1, "user_id")) + + def test_create_and_delete(self): + name = self.name_generate() + + def cleanup(): + # We should check keypair existence and remove it from correct user + # if keypair is presented + o = self.another_nova("keypair-list") + if name in o: + self.another_nova("keypair-delete %s" % name) + + self.nova("keypair-add %s --user %s" % (name, self.user_id)) + self.addCleanup(cleanup) + output = self.another_nova("keypair-list") + self.assertEqual(name, self._get_column_value_from_single_row_table( + output, "Name")) + + self.nova("keypair-delete %s --user %s " % (name, self.user_id)) + output = self.another_nova("keypair-list") + self.assertRaises( + ValueError, + self._get_column_value_from_single_row_table, output, "Name") + + +class TestKeypairsNovaClientV235(base.ProjectTestBase): + """Keypairs functional tests for v2.35 nova-api microversion.""" + + COMPUTE_API_VERSION = "2.35" + + def test_create_and_list_keypair_with_marker_and_limit(self): + names = [] + for i in range(3): + 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]) + + # sort keypairs before pagination + names = sorted(names) + + # list only one keypair after the first + output_1 = self.another_nova("keypair-list --limit 1 --marker %s" % + names[0]) + output_2 = self.nova("keypair-list --limit 1 --marker %s --user %s" % + (names[0], self.user_id)) + self.assertEqual(output_1, output_2) + # it should be table with only one second key-pair + self.assertEqual( + names[1], self._get_column_value_from_single_row_table(output_1, + "Name")) 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_networks.py b/novaclient/tests/functional/v2/test_networks.py new file mode 100644 index 000000000..49d99ec96 --- /dev/null +++ b/novaclient/tests/functional/v2/test_networks.py @@ -0,0 +1,40 @@ +# Copyright 2016 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. + +from novaclient.tests.functional import base + + +class TestNetworkCommandsV2_36(base.ClientTestBase): + """Deprecated network command functional tests.""" + + # Proxy APIs were deprecated in 2.36 but the CLI should fallback to 2.35 + # and emit a warning. + COMPUTE_API_VERSION = "2.36" + + def test_limits(self): + """Tests that 2.36 won't return network-related resource limits and + the CLI output won't show them. + """ + output = self.nova('limits') + # assert that SecurityGroups isn't in the table output + self.assertRaises(ValueError, self._get_value_from_the_table, + output, 'SecurityGroups') + + def test_quota_show(self): + """Tests that 2.36 won't return network-related resource quotas and + the CLI output won't show them. + """ + output = self.nova('quota-show') + # assert that security_groups isn't in the table output + self.assertRaises(ValueError, self._get_value_from_the_table, + output, 'security_groups') diff --git a/novaclient/tests/functional/v2/test_os_services.py b/novaclient/tests/functional/v2/test_os_services.py new file mode 100644 index 000000000..8550bba6c --- /dev/null +++ b/novaclient/tests/functional/v2/test_os_services.py @@ -0,0 +1,147 @@ +# 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 +from novaclient.tests.functional.v2.legacy import test_os_services +from novaclient import utils + + +class TestOsServicesNovaClientV211(test_os_services.TestOsServicesNovaClient): + """Functional tests for os-services attributes, microversion 2.11""" + + COMPUTE_API_VERSION = "2.11" + + def test_os_services_force_down_force_up(self): + for serv in self.client.services.list(): + # In Pike the os-services API was made multi-cell aware and it + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + continue + service_list = self.nova('service-list --binary %s' % serv.binary) + # Check the 'service-list' table has the 'Forced down' column + status = self._get_column_value_from_single_row_table( + service_list, 'Forced down') + self.assertEqual('False', status) + + host = self._get_column_value_from_single_row_table(service_list, + 'Host') + service = self.nova('service-force-down %s' % host) + self.addCleanup(self.nova, 'service-force-down --unset', + 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' % host) + status = self._get_column_value_from_single_row_table( + service, 'Forced down') + self.assertEqual('False', status) + + +class TestOsServicesNovaClientV2_53(base.ClientTestBase): + """Tests the nova service-* commands using the 2.53 microversion. + + The main difference with the 2.53 microversion in these commands is + the host/binary combination is replaced with the service.id as the + unique identifier for a service. + """ + COMPUTE_API_VERSION = "2.53" + + def test_os_services_list(self): + table = self.nova('service-list') + for serv in self.client.services.list(): + self.assertIn(serv.binary, table) + # the id should not be an integer and should be in the table + self.assertFalse(utils.is_integer_like(serv.id)) + self.assertIn(serv.id, table) + + def test_os_service_disable_enable(self): + # Disable and enable Nova services in accordance with list of nova + # 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.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 + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + continue + service = self.nova('service-disable %s' % serv.id) + self.addCleanup(self.nova, 'service-enable', params="%s" % serv.id) + service_id = self._get_column_value_from_single_row_table( + service, 'ID') + self.assertEqual(serv.id, service_id) + status = self._get_column_value_from_single_row_table( + service, 'Status') + self.assertEqual('disabled', status) + service = self.nova('service-enable %s' % serv.id) + service_id = self._get_column_value_from_single_row_table( + service, 'ID') + self.assertEqual(serv.id, service_id) + status = self._get_column_value_from_single_row_table( + service, 'Status') + self.assertEqual('enabled', status) + + def test_os_service_disable_log_reason(self): + for serv in self.client.services.list(): + # In Pike the os-services API was made multi-cell aware and it + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + continue + service = self.nova('service-disable --reason test_disable %s' + % serv.id) + self.addCleanup(self.nova, 'service-enable', params="%s" % serv.id) + service_id = self._get_column_value_from_single_row_table( + service, 'ID') + self.assertEqual(serv.id, service_id) + status = self._get_column_value_from_single_row_table( + service, 'Status') + log_reason = self._get_column_value_from_single_row_table( + service, 'Disabled Reason') + self.assertEqual('disabled', status) + self.assertEqual('test_disable', log_reason) + + def test_os_services_force_down_force_up(self): + for serv in self.client.services.list(): + # In Pike the os-services API was made multi-cell aware and it + # looks up services by host, which uses the host mapping record + # in the API DB which is only populated for nova-compute services, + # effectively making it impossible to perform actions like enable + # or disable non-nova-compute services since the API won't be able + # to find them. So filter out anything that's not nova-compute. + if serv.binary != 'nova-compute': + continue + service = self.nova('service-force-down %s' % serv.id) + self.addCleanup(self.nova, 'service-force-down --unset', + params="%s" % serv.id) + service_id = self._get_column_value_from_single_row_table( + service, 'ID') + self.assertEqual(serv.id, service_id) + forced_down = self._get_column_value_from_single_row_table( + service, 'Forced down') + self.assertEqual('True', forced_down) + service = self.nova('service-force-down --unset %s' % serv.id) + forced_down = self._get_column_value_from_single_row_table( + service, 'Forced down') + self.assertEqual('False', forced_down) diff --git a/novaclient/tests/functional/v2/test_quota_classes.py b/novaclient/tests/functional/v2/test_quota_classes.py new file mode 100644 index 000000000..837759f7a --- /dev/null +++ b/novaclient/tests/functional/v2/test_quota_classes.py @@ -0,0 +1,133 @@ +# Copyright 2017 Huawei Technologies Co.,LTD. +# +# 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 tempest.lib import exceptions + +from novaclient.tests.functional import base + + +class TestQuotaClassesNovaClient(base.ClientTestBase): + """Nova quota classes functional tests for the v2.1 microversion.""" + + COMPUTE_API_VERSION = '2.1' + + # The list of quota class resources we expect in the output table. + _included_resources = ['instances', 'cores', 'ram', + 'floating_ips', 'fixed_ips', 'metadata_items', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes', 'key_pairs', + 'security_groups', 'security_group_rules'] + + # The list of quota class resources we do not expect in the output table. + _excluded_resources = ['server_groups', 'server_group_members'] + + # Any resources that are not shown but can be updated. For example, before + # microversion 2.50 you can update server_groups and server_groups_members + # quota class values but they are not shown in the GET response. + _extra_update_resources = _excluded_resources + + # The list of resources which are blocked from being updated. + _blocked_update_resources = [] + + 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_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. + self.assertIn(quota_name, expected_values) + expected_value = expected_values[quota_name] + actual_value = self._get_value_from_the_table(output, quota_name) + self.assertEqual(expected_value, actual_value) + + # Now make sure anything that we don't expect in the output table is + # actually not showing up. + for quota_name in self._excluded_resources: + # ValueError is raised when the key isn't found in the table. + self.assertRaises(ValueError, + self._get_value_from_the_table, + output, quota_name) + + def test_quota_class_show(self): + """Tests showing quota class values for a fake non-existing quota + class. The API will return the defaults if the quota class does not + actually exist. We use a fake class to avoid any interaction with the + real default quota class values. + """ + default_quota_class_set = self.client.quota_classes.get('default') + default_values = { + quota_name: str(getattr(default_quota_class_set, quota_name)) + for quota_name in self._included_resources + } + output = self.nova('quota-class-show %s' % + self._get_quota_class_name()) + 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 + is that if the quota class is not found, it is created. So in this + test we can use a fake quota class with fake values and they will all + get set. We don't use the default quota class because it is global + and we don't want to interfere with other tests. + """ + class_name = self._get_quota_class_name() + params = [class_name] + expected_values = {} + for quota_name in ( + self._included_resources + self._extra_update_resources): + params.append("--%s 99" % quota_name.replace("_", "-")) + expected_values[quota_name] = '99' + + # Note that the quota-class-update CLI doesn't actually output any + # information from the response. + 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_quota_class_show_output(output, expected_values) + + # Assert that attempting to update resources that are blocked will + # result in a failure. + for quota_name in self._blocked_update_resources: + self.assertRaises( + exceptions.CommandFailed, + self.nova, "quota-class-update %s --%s 99" % + (class_name, quota_name.replace("_", "-"))) + + +class TestQuotasNovaClient2_50(TestQuotaClassesNovaClient): + """Nova quota classes functional tests for the v2.50 microversion.""" + + COMPUTE_API_VERSION = '2.50' + + # 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-quota-sets API. + _included_resources = ['instances', 'cores', 'ram', 'metadata_items', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes', 'key_pairs', + 'server_groups', 'server_group_members'] + + # The list of quota class resources we do not expect in the output table. + _excluded_resources = ['floating_ips', 'fixed_ips', + 'security_groups', 'security_group_rules'] + + # In 2.50, server_groups and server_group_members can be both updated + # in a PUT request and shown in a GET response. + _extra_update_resources = [] + + # In 2.50, you can't update the network-related resources. + _blocked_update_resources = _excluded_resources diff --git a/novaclient/tests/functional/v2/test_quotas.py b/novaclient/tests/functional/v2/test_quotas.py new file mode 100644 index 000000000..effddf8a6 --- /dev/null +++ b/novaclient/tests/functional/v2/test_quotas.py @@ -0,0 +1,74 @@ +# 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.v2.legacy import test_quotas + + +class TestQuotasNovaClient2_35(test_quotas.TestQuotasNovaClient): + """Nova quotas functional tests.""" + + COMPUTE_API_VERSION = "2.35" + + _quota_resources = ['instances', 'cores', 'ram', + 'floating_ips', 'fixed_ips', 'metadata_items', + 'injected_files', 'injected_file_content_bytes', + 'injected_file_path_bytes', 'key_pairs', + 'security_groups', 'security_group_rules', + 'server_groups', 'server_group_members'] + + def test_quotas_update(self): + # `nova quota-update` requires tenant-id. + tenant_id = self._get_project_id(self.cli_clients.tenant_name) + + self.addCleanup(self.client.quotas.delete, tenant_id) + + original_quotas = self.client.quotas.get(tenant_id) + + difference = 10 + params = [tenant_id] + for quota_name in self._quota_resources: + params.append("--%(name)s %(value)s" % { + "name": quota_name.replace("_", "-"), + "value": getattr(original_quotas, quota_name) + difference}) + + self.nova("quota-update", params=" ".join(params)) + + updated_quotas = self.client.quotas.get(tenant_id) + + for quota_name in self._quota_resources: + self.assertEqual(getattr(original_quotas, quota_name), + getattr(updated_quotas, quota_name) - difference) + + +class TestQuotasNovaClient2_36(TestQuotasNovaClient2_35): + """Nova quotas functional tests.""" + + COMPUTE_API_VERSION = "2.36" + + # The 2.36 microversion stops proxying network quota resources like + # floating/fixed IPs and security groups/rules. + _quota_resources = ['instances', 'cores', 'ram', + 'metadata_items', 'injected_files', + '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 new file mode 100644 index 000000000..075a2f9a7 --- /dev/null +++ b/novaclient/tests/functional/v2/test_resize.py @@ -0,0 +1,115 @@ +# Copyright 2017 Huawei Technologies Co.,LTD. +# +# 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 TestServersResize(base.ClientTestBase): + """Servers resize functional tests.""" + + COMPUTE_API_VERSION = '2.1' + + 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. + self.assertEqual(old_usage['totalInstancesUsed'], + new_usage['totalInstancesUsed'], + 'totalInstancesUsed does not match') + # For the resize we're doing, those flavors have the same vcpus so we + # don't expect any quota change. + self.assertEqual(old_usage['totalCoresUsed'], + new_usage['totalCoresUsed'], + 'totalCoresUsed does not match') + # RAM is the only thing that will change for these flavors in a resize. + if expect_diff: + self.assertNotEqual(old_usage['totalRAMUsed'], + new_usage['totalRAMUsed'], + 'totalRAMUsed should have changed') + else: + self.assertEqual(old_usage['totalRAMUsed'], + new_usage['totalRAMUsed'], + 'totalRAMUsed does not match') + + 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(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 + alternate_flavor = self._pick_alternate_flavor() + self.nova('resize', + params='%s %s --poll' % (server_id, alternate_flavor)) + resize_usage = self._get_absolute_limits() + # compare the starting usage against the resize usage + self._compare_quota_usage(starting_usage, resize_usage) + # now confirm the resize + self.nova('resize-confirm', params='%s' % server_id) + # we have to wait for the server to be ACTIVE before we can check quota + self._wait_for_state_change(server_id, 'active') + # get the final quota usage which should be the same as the resize + # usage before confirm + confirm_usage = self._get_absolute_limits() + self._compare_quota_usage( + resize_usage, confirm_usage, expect_diff=False) + + def _create_resize_down_flavors(self): + """Creates two flavors with different size ram but same size vcpus + and disk. + + :returns: tuple of 2 IDs which represents larger_flavor for resize and + smaller 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) + + 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 larger_id, smaller_id + + def test_resize_down_revert(self): + """Tests creating a server and resizes down and reverts the resize. + Compares quota before, during and after the resize. + """ + # devstack's m1.tiny and m1.small have different size disks so we + # can't use those as you can't resize down the disk. So we have to + # 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(flavor=larger_flavor).id + # get the starting quota now that we've created a server + starting_usage = self._get_absolute_limits() + # now resize down + self.nova('resize', + params='%s %s --poll' % (server_id, smaller_flavor)) + resize_usage = self._get_absolute_limits() + # compare the starting usage against the resize usage; with counting + # quotas in the server there are no reservations, so the + # usage changes after the resize happens before it's confirmed. + self._compare_quota_usage(starting_usage, resize_usage) + # now revert the resize + self.nova('resize-revert', params='%s' % server_id) + # we have to wait for the server to be ACTIVE before we can check quota + self._wait_for_state_change(server_id, 'active') + # get the final quota usage which will be different from the resize + # usage since we've reverted back *up* to the original flavor; the API + # code checks quota again if we revert up in size + revert_usage = self._get_absolute_limits() + self._compare_quota_usage(resize_usage, revert_usage) diff --git a/novaclient/tests/functional/v2/test_server_groups.py b/novaclient/tests/functional/v2/test_server_groups.py new file mode 100644 index 000000000..37ebaa66d --- /dev/null +++ b/novaclient/tests/functional/v2/test_server_groups.py @@ -0,0 +1,120 @@ +# Copyright 2015 Huawei Technology corp. +# 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.v2.legacy import test_server_groups + + +class TestServerGroupClientV213(test_server_groups.TestServerGroupClient): + """Server groups v2.13 functional tests.""" + + COMPUTE_API_VERSION = "2.13" + expected_metadata = True + expected_policy_rules = False + + def test_create_server_group(self): + sg_id = self._create_sg("affinity") + 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._get_column_value_from_single_row_table( + sg, "User Id") + 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") + self.addCleanup(self.nova, 'server-group-delete %s' % sg_id) + sg = self.nova("server-group-list") + result = self._get_column_value_from_single_row_table(sg, "Id") + self._get_column_value_from_single_row_table( + sg, "User Id") + 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") + 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._get_column_value_from_single_row_table( + sg, "User Id") + 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 new file mode 100644 index 000000000..3de668dd4 --- /dev/null +++ b/novaclient/tests/functional/v2/test_servers.py @@ -0,0 +1,386 @@ +# -*- coding: utf-8 -*- +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import random +import string + +from tempest.lib import decorators + +from novaclient.tests.functional import base +from novaclient.tests.functional.v2.legacy import test_servers +from novaclient.v2 import shell + + +class TestServersBootNovaClient(test_servers.TestServersBootNovaClient): + """Servers boot functional tests.""" + + COMPUTE_API_VERSION = "2.latest" + + +class TestServersListNovaClient(test_servers.TestServersListNovaClient): + """Servers list functional tests.""" + + COMPUTE_API_VERSION = "2.latest" + + +class TestServerLockV29(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.9" + + def _show_server_and_check_lock_attr(self, server, value): + output = self.nova("show %s" % server.id) + self.assertEqual(str(value), + self._get_value_from_the_table(output, "locked")) + + def test_attribute_presented(self): + # prepare + server = self._create_server() + + # testing + self._show_server_and_check_lock_attr(server, False) + + self.nova("lock %s" % server.id) + self._show_server_and_check_lock_attr(server, True) + + self.nova("unlock %s" % server.id) + self._show_server_and_check_lock_attr(server, False) + + +class TestServersDescription(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.19" + + def _boot_server_with_description(self): + descr = "Some words about this test VM." + server = self._create_server(description=descr) + + self.assertEqual(descr, server.description) + + return server, descr + + def test_create(self): + # Add a description to the tests that create a server + server, descr = self._boot_server_with_description() + output = self.nova("show %s" % server.id) + self.assertEqual(descr, self._get_value_from_the_table(output, + "description")) + + def test_list_servers_with_description(self): + # Check that the description is returned as part of server details + # for a server list + server, descr = self._boot_server_with_description() + output = self.nova("list --fields description") + self.assertEqual(server.id, + self._get_column_value_from_single_row_table( + output, "ID")) + self.assertEqual(descr, + self._get_column_value_from_single_row_table( + output, "Description")) + + @decorators.skip_because(bug="1694371") + def test_rebuild(self): + # Add a description to the tests that rebuild a server + server, descr = self._boot_server_with_description() + descr = "New description for rebuilt VM." + self.nova("rebuild --description '%s' %s %s" % + (descr, server.id, self.image.name)) + shell._poll_for_status( + self.client.servers.get, server.id, + 'rebuild', ['active']) + output = self.nova("show %s" % server.id) + self.assertEqual(descr, self._get_value_from_the_table(output, + "description")) + + def test_remove_description(self): + # Remove description from server booted with it + server, descr = self._boot_server_with_description() + self.nova("update %s --description ''" % server.id) + output = self.nova("show %s" % server.id) + self.assertEqual("-", self._get_value_from_the_table(output, + "description")) + + def test_add_remove_description_on_existing_server(self): + # Set and remove the description on an existing server + server = self._create_server() + descr = "Add a description for previously-booted VM." + self.nova("update %s --description '%s'" % (server.id, descr)) + output = self.nova("show %s" % server.id) + self.assertEqual(descr, self._get_value_from_the_table(output, + "description")) + self.nova("update %s --description ''" % server.id) + output = self.nova("show %s" % server.id) + self.assertEqual("-", self._get_value_from_the_table(output, + "description")) + + def test_update_with_description_longer_than_255_symbols(self): + # Negative case for description longer than 255 characters + server = self._create_server() + descr = ''.join(random.choice(string.ascii_letters) + for i in range(256)) + 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. '%s' is too long (HTTP 400)" + % (descr, descr), output) + + +class TestServersTagsV226(base.ClientTestBase): + + COMPUTE_API_VERSION = "2.26" + + def _boot_server_with_tags(self, tags=["t1", "t2"]): + uuid = self._create_server().id + self.client.servers.set_tags(uuid, tags) + return uuid + + def test_show(self): + uuid = self._boot_server_with_tags() + output = self.nova("show %s" % uuid) + self.assertEqual('["t1", "t2"]', self._get_value_from_the_table( + output, "tags")) + + def test_unicode_tag_correctly_displayed(self): + """Regression test for bug #1669683. + + List and dict fields with unicode cannot be correctly + displayed. + + Ensure that once we fix this it doesn't regress. + """ + # create an instance with chinese tag + uuid = self._boot_server_with_tags(tags=["中文标签"]) + output = self.nova("show %s" % uuid) + self.assertEqual('["中文标签"]', self._get_value_from_the_table( + output, "tags")) + + def test_list(self): + uuid = self._boot_server_with_tags() + output = self.nova("server-tag-list %s" % uuid) + tags = self._get_list_of_values_from_single_column_table( + output, "Tag") + self.assertEqual(["t1", "t2"], tags) + + def test_add(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-add %s t3" % uuid) + self.assertEqual(["t1", "t2", "t3"], + self.client.servers.tag_list(uuid)) + + def test_add_many(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-add %s t3 t4" % uuid) + self.assertEqual(["t1", "t2", "t3", "t4"], + self.client.servers.tag_list(uuid)) + + def test_set(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-set %s t3 t4" % uuid) + self.assertEqual(["t3", "t4"], self.client.servers.tag_list(uuid)) + + def test_delete(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-delete %s t2" % uuid) + self.assertEqual(["t1"], self.client.servers.tag_list(uuid)) + + def test_delete_many(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-delete %s t1 t2" % uuid) + self.assertEqual([], self.client.servers.tag_list(uuid)) + + def test_delete_all(self): + uuid = self._boot_server_with_tags() + self.nova("server-tag-delete-all %s" % uuid) + self.assertEqual([], self.client.servers.tag_list(uuid)) + + +class TestServersAutoAllocateNetworkCLI(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.37' + + def _find_network_in_table(self, table): + # Example: + # +-----------------+-----------------------------------+ + # | Property | Value | + # +-----------------+-----------------------------------+ + # | private network | 192.168.154.128 | + # +-----------------+-----------------------------------+ + for line in table.split('\n'): + if '|' in line: + l_property, l_value = line.split('|')[1:3] + if ' network' in l_property.strip(): + return ' '.join(l_property.strip().split()[:-1]) + + def test_boot_server_with_auto_network(self): + """Tests that the CLI defaults to 'auto' when --nic isn't specified. + """ + # check to see if multiple networks are available because if so we + # have to skip this test as auto will fail with a 409 conflict as it's + # an ambiguous request and nova won't know which network to pick + if self.multiple_networks: + # we could potentially get around this by extending TenantTestBase + self.skipTest('multiple networks available') + server_info = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--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') + self.addCleanup(self.wait_for_resource_delete, + server_id, self.client.servers) + self.addCleanup(self.client.servers.delete, server_id) + # get the server details to verify there is a network, we don't care + # what the network name is, we just want to see an entry show up + server_info = self.nova('show', params=server_id) + network = self._find_network_in_table(server_info) + self.assertIsNotNone( + network, 'Auto-allocated network not found: %s' % server_info) + + def test_boot_server_with_no_network(self): + """Tests that '--nic none' is honored. + """ + server_info = self.nova('boot', params=( + '%(name)s --flavor %(flavor)s --poll ' + '--image %(image)s --nic none' % + {'name': self.name_generate(), + 'flavor': self.flavor.id, + 'image': self.image.id})) + server_id = self._get_value_from_the_table(server_info, 'id') + self.addCleanup(self.wait_for_resource_delete, + server_id, self.client.servers) + self.addCleanup(self.client.servers.delete, server_id) + # get the server details to verify there is not a network + server_info = self.nova('show', params=server_id) + network = self._find_network_in_table(server_info) + self.assertIsNone( + network, 'Unexpected network allocation: %s' % server_info) + + +class TestServersDetailsFlavorInfo(base.ClientTestBase): + + COMPUTE_API_VERSION = '2.47' + + def _validate_flavor_details(self, flavor_details, server_details): + # This is a mapping between the keys used in the flavor GET response + # and the keys used for the flavor information embedded in the server + # details. + flavor_key_mapping = { + "OS-FLV-EXT-DATA:ephemeral": "flavor:ephemeral", + "disk": "flavor:disk", + "extra_specs": "flavor:extra_specs", + "name": "flavor:original_name", + "ram": "flavor:ram", + "swap": "flavor:swap", + "vcpus": "flavor:vcpus", + } + + for key in flavor_key_mapping: + flavor_val = self._get_value_from_the_table( + flavor_details, key) + server_flavor_val = self._get_value_from_the_table( + server_details, flavor_key_mapping[key]) + if key == "swap" and flavor_val == "": + # "flavor-show" displays zero swap as empty string. + flavor_val = '0' + self.assertEqual(flavor_val, server_flavor_val) + + def _setup_extra_specs(self, flavor_id): + extra_spec_key = "dummykey" + self.nova('flavor-key', params=('%(flavor)s set %(key)s=dummyval' % + {'flavor': flavor_id, + 'key': extra_spec_key})) + unset_params = ('%(flavor)s unset %(key)s' % + {'flavor': flavor_id, 'key': extra_spec_key}) + self.addCleanup(self.nova, 'flavor-key', params=unset_params) + + def test_show(self): + self._setup_extra_specs(self.flavor.id) + uuid = self._create_server().id + server_output = self.nova("show %s" % uuid) + flavor_output = self.nova("flavor-show %s" % self.flavor.id) + self._validate_flavor_details(flavor_output, server_output) + + def test_show_minimal(self): + uuid = self._create_server().id + server_output = self.nova("show --minimal %s" % uuid) + server_output_flavor = self._get_value_from_the_table( + server_output, 'flavor') + self.assertEqual(self.flavor.name, server_output_flavor) + + def test_list(self): + self._setup_extra_specs(self.flavor.id) + self._create_server() + server_output = self.nova("list --fields flavor:disk") + # namespaced fields get reformatted slightly as column names + server_flavor_val = self._get_column_value_from_single_row_table( + server_output, 'flavor: Disk') + 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 new file mode 100644 index 000000000..19dcbd96e --- /dev/null +++ b/novaclient/tests/functional/v2/test_trigger_crash_dump.py @@ -0,0 +1,136 @@ +# 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 time + +from tempest.lib import decorators + +from novaclient.tests.functional import base +from novaclient.v2 import shell + + +@decorators.skip_because(bug="1675526") +class TestTriggerCrashDumpNovaClientV217(base.ProjectTestBase): + """Functional tests for trigger crash dump""" + + COMPUTE_API_VERSION = "2.17" + + # It's a resource-consuming task to implement full-flow (up to getting + # and reading a dump file) functional test for trigger-crash-dump. + # We need to upload Ubuntu image for booting an instance based on it, + # and to install kdump with its further configuring on this instance. + # Here, the "light" version of functional test is proposed. + # It's based on knowledge that trigger-crash-dump uses a NMI injection, + # and when the 'trigger-crash-dump' operation is executed, + # instance's kernel receives the NMI signal, and an appropriate + # message will appear in the instance's log. + + # The server status must be ACTIVE, PAUSED, RESCUED, RESIZED or ERROR. + # If not, the conflictingRequest(409) code is returned + + def _assert_nmi(self, server_id, timeout=60, poll_interval=1): + start_time = time.time() + while time.time() - start_time < timeout: + if 'trigger_crash_dump' in self.nova('instance-action-list %s ' % + server_id): + break + time.sleep(poll_interval) + else: + self.fail("Trigger crash dump hasn't been executed for server %s" + % server_id) + + def test_trigger_crash_dump_in_active_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('trigger-crash-dump %s ' % server.id) + self._assert_nmi(server.id) + + def test_trigger_crash_dump_in_error_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('reset-state %s ' % server.id) + shell._poll_for_status( + self.client.servers.get, server.id, + 'active', ['error']) + self.nova('trigger-crash-dump %s ' % server.id) + self._assert_nmi(server.id) + + def test_trigger_crash_dump_in_paused_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('pause %s ' % server.id) + shell._poll_for_status( + self.client.servers.get, server.id, + 'active', ['paused']) + self.nova('trigger-crash-dump %s ' % server.id) + self._assert_nmi(server.id) + + def test_trigger_crash_dump_in_rescued_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('rescue %s ' % server.id) + shell._poll_for_status( + self.client.servers.get, server.id, + 'active', ['rescue']) + self.wait_for_server_os_boot(server.id) + self.nova('trigger-crash-dump %s ' % server.id) + self._assert_nmi(server.id) + + def test_trigger_crash_dump_in_resized_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('resize %s %s' % (server.id, 'm1.small')) + shell._poll_for_status( + self.client.servers.get, server.id, + 'active', ['verify_resize']) + self.nova('trigger-crash-dump %s ' % server.id) + self._assert_nmi(server.id) + + def test_trigger_crash_dump_in_shutoff_state(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('stop %s ' % server.id) + shell._poll_for_status( + self.client.servers.get, server.id, + 'active', ['shutoff']) + output = self.nova('trigger-crash-dump %s ' % + server.id, fail_ok=True, merge_stderr=True) + self.assertIn("ERROR (Conflict): " + "Cannot 'trigger_crash_dump' instance %s " + "while it is in vm_state stopped (HTTP 409) " % + server.id, output) + + # If the specified server is locked, the conflictingRequest(409) code + # is returned to a user without administrator privileges. + def test_trigger_crash_dump_in_locked_state_admin(self): + server = self._create_server() + self.wait_for_server_os_boot(server.id) + self.nova('lock %s ' % server.id) + self.nova('trigger-crash-dump %s ' % server.id) + self._assert_nmi(server.id) + + def test_trigger_crash_dump_in_locked_state_nonadmin(self): + 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) + server_id = self._get_value_from_the_table( + server, 'id') + self.wait_for_server_os_boot(server_id) + self.another_nova('lock %s ' % server_id) + self.addCleanup(self.another_nova, 'unlock', params=name) + output = self.another_nova('trigger-crash-dump %s ' % + server_id, fail_ok=True, merge_stderr=True) + # NOTE(mriedem): Depending on the version of the server you can get + # different error messages back from this, so just assert that it's a + # 409 either way. + self.assertIn("ERROR (Conflict)", output) diff --git a/novaclient/tests/functional/v2/test_usage.py b/novaclient/tests/functional/v2/test_usage.py new file mode 100644 index 000000000..1a19ad935 --- /dev/null +++ b/novaclient/tests/functional/v2/test_usage.py @@ -0,0 +1,38 @@ +# 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.v2.legacy import test_usage + + +class TestUsageCLI_V240(test_usage.TestUsageCLI): + + COMPUTE_API_VERSION = '2.40' + + +class TestUsageClient_V240(test_usage.TestUsageClient): + + COMPUTE_API_VERSION = '2.40' + + def test_get(self): + start, end = self._create_servers_in_time_window() + tenant_id = self._get_project_id(self.cli_clients.tenant_name) + usage = self.client.usage.get( + tenant_id, start=start, end=end, limit=1) + self.assertEqual(tenant_id, usage.tenant_id) + self.assertEqual(1, len(usage.server_usages)) + + def test_list(self): + start, end = self._create_servers_in_time_window() + usages = self.client.usage.list( + start=start, end=end, detailed=True, limit=1) + self.assertEqual(1, len(usages)) + self.assertEqual(1, len(usages[0].server_usages)) diff --git a/tests/v1_1/contrib/__init__.py b/novaclient/tests/unit/__init__.py similarity index 100% rename from tests/v1_1/contrib/__init__.py rename to novaclient/tests/unit/__init__.py diff --git a/novaclient/tests/unit/fake_actions_module.py b/novaclient/tests/unit/fake_actions_module.py new file mode 100644 index 000000000..eb180486b --- /dev/null +++ b/novaclient/tests/unit/fake_actions_module.py @@ -0,0 +1,57 @@ +# 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 api_versions +from novaclient import utils + + +@api_versions.wraps("2.10", "2.20") +def do_fake_action(): + return 1 + + +@api_versions.wraps("2.21", "2.30") +def do_fake_action(): + return 2 + + +@api_versions.wraps("2.0") +def do_another_fake_action(): + return 0 + + +@utils.arg( + '--foo', + start_version='2.1', + end_version='2.2') +@utils.arg( + '--bar', + start_version='2.3', + end_version='2.4') +def do_fake_action2(): + return 3 + + +@utils.arg( + '--foo', + help='first foo', + start_version='2.10', + end_version='2.20') +@utils.arg( + '--foo', + help='second foo', + start_version='2.21') +def do_fake_action3(): + return 3 diff --git a/novaclient/tests/unit/fakes.py b/novaclient/tests/unit/fakes.py new file mode 100644 index 000000000..312cca4fb --- /dev/null +++ b/novaclient/tests/unit/fakes.py @@ -0,0 +1,156 @@ +# +# 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. + +""" +A fake server that "responds" to API methods with pre-canned responses. + +All of these responses come from the spec, so if for some reason the spec's +wrong the tests might raise AssertionError. I've indicated in comments the +places where actual behavior differs from the spec. +""" + +from novaclient import base + +# fake request id +FAKE_REQUEST_ID = 'req-3fdea7c2-e3e3-48b5-a656-6b12504c49a1' +FAKE_REQUEST_ID_LIST = [FAKE_REQUEST_ID] + + +def assert_has_keys(dict, required=None, optional=None): + required = required or [] + optional = optional or [] + keys = dict.keys() + for k in required: + try: + assert k in keys + except AssertionError: + extra_keys = set(keys).difference(set(required + optional)) + raise AssertionError("found unexpected keys: %s" % + list(extra_keys)) + + +class FakeClient(object): + + def assert_called(self, method, url, body=None, pos=-1): + """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 + :param body: Expected request body to be called with given method + and url. Default is None. + :param pos: Order of the expected method call. If multiple methods + calls are made in single API request, then, order of each + method call can be checked by passing expected order to + this arg. + Default is -1 which means most recent call. + + Usage:: + 1. self.run_command('flavor-list --extra-specs') + self.assert_called('GET', '/flavors/aa1/os-extra_specs') + + 2. self.run_command(["boot", "--image", "1", + "--flavor", "512 MiB Server", + "--max-count", "3", "server"]) + self.assert_called('GET', '/images/1', pos=0) + 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( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '2', + 'name': 'server', + 'imageRef': '1', + 'min_count': 1, + 'max_count': 3, + } + }, pos=4) + """ + expected = (method, url) + + assert self.client.callstack, \ + "Expected %s %s but no calls were made." % expected + + called = self.client.callstack[pos][0:2] + + assert expected == called, \ + ('\nExpected: %(expected)s' + '\nActual: %(called)s' + '\nCall position: %(pos)s' + '\nCalls:\n%(calls)s' % + {'expected': expected, 'called': called, 'pos': pos, + 'calls': '\n'.join(str(c) for c in self.client.callstack)}) + + if body is not None: + if self.client.callstack[pos][2] != body: + raise AssertionError('%r != %r' % + (self.client.callstack[pos][2], body)) + + def assert_called_anytime(self, method, url, body=None): + """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 + :param body: Expected request body to be called with given method + and url. Default is None. + Usage:: + self.run_command('flavor-list --extra-specs') + self.assert_called_anytime('GET', '/flavors/detail') + """ + expected = (method, url) + + assert self.client.callstack, \ + "Expected %s %s but no calls were made." % expected + + found = False + for entry in self.client.callstack: + if expected == entry[0:2]: + found = True + break + + assert found, 'Expected %s; got %s' % (expected, self.client.callstack) + if body is not None: + try: + assert entry[2] == body + except AssertionError: + print(entry[2]) + print("!=") + print(body) + raise + + 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 = [] + + def authenticate(self): + pass + + +# Fake class that will be used as an extension +class FakeManager(base.Manager): + pass diff --git a/novaclient/tests/unit/fixture_data/__init__.py b/novaclient/tests/unit/fixture_data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/novaclient/tests/unit/fixture_data/agents.py b/novaclient/tests/unit/fixture_data/agents.py new file mode 100644 index 000000000..44cbd9343 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/agents.py @@ -0,0 +1,54 @@ +# 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-agents' + + def setUp(self): + super(Fixture, self).setUp() + + post_os_agents = { + 'agent': { + 'url': '/xxx/xxx/xxx', + 'hypervisor': 'kvm', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'version': '7.0', + 'architecture': 'x86', + 'os': 'win', + 'id': 1 + } + } + + self.requests_mock.post(self.url(), + json=post_os_agents, + headers=self.json_headers) + + put_os_agents_1 = { + "agent": { + "url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546", + 'id': 1 + } + } + + self.requests_mock.put(self.url(1), + json=put_os_agents_1, + headers=self.json_headers) + + self.requests_mock.delete(self.url(1), + headers=self.json_headers, + status_code=202) diff --git a/novaclient/tests/unit/fixture_data/aggregates.py b/novaclient/tests/unit/fixture_data/aggregates.py new file mode 100644 index 000000000..b3ab88c5f --- /dev/null +++ b/novaclient/tests/unit/fixture_data/aggregates.py @@ -0,0 +1,60 @@ +# 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-aggregates' + + def setUp(self): + super(Fixture, self).setUp() + + get_os_aggregates = {"aggregates": [ + {'id': '1', + 'name': 'test', + 'availability_zone': 'nova1'}, + {'id': '2', + 'name': 'test2', + 'availability_zone': 'nova1'}, + ]} + + self.requests_mock.get(self.url(), + json=get_os_aggregates, + headers=self.json_headers) + + get_aggregates_1 = {'aggregate': get_os_aggregates['aggregates'][0]} + + self.requests_mock.post(self.url(), + json=get_aggregates_1, + headers=self.json_headers) + + for agg_id in (1, 2): + for method in ('GET', 'PUT'): + self.requests_mock.register_uri(method, self.url(agg_id), + json=get_aggregates_1, + headers=self.json_headers) + + self.requests_mock.post(self.url(agg_id, 'action'), + json=get_aggregates_1, + headers=self.json_headers) + + 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/availability_zones.py b/novaclient/tests/unit/fixture_data/availability_zones.py new file mode 100644 index 000000000..da1619019 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/availability_zones.py @@ -0,0 +1,91 @@ +# 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 V1(base.Fixture): + + base_url = 'os-availability-zone' + + zone_info_key = 'availabilityZoneInfo' + zone_name_key = 'zoneName' + zone_state_key = 'zoneState' + + def setUp(self): + super(V1, self).setUp() + + get_os_availability_zone = { + self.zone_info_key: [ + { + self.zone_name_key: "zone-1", + self.zone_state_key: {"available": True}, + "hosts": None + }, + { + self.zone_name_key: "zone-2", + self.zone_state_key: {"available": False}, + "hosts": None + } + ] + } + + self.requests_mock.get(self.url(), + json=get_os_availability_zone, + headers=self.json_headers) + + get_os_zone_detail = { + self.zone_info_key: [ + { + self.zone_name_key: "zone-1", + self.zone_state_key: {"available": True}, + "hosts": { + "fake_host-1": { + "nova-compute": { + "active": True, + "available": True, + "updated_at": '2012-12-26 14:45:25' + } + } + } + }, + { + self.zone_name_key: "internal", + self.zone_state_key: {"available": True}, + "hosts": { + "fake_host-1": { + "nova-sched": { + "active": True, + "available": True, + "updated_at": '2012-12-26 14:45:25' + } + }, + "fake_host-2": { + "nova-network": { + "active": True, + "available": False, + "updated_at": '2012-12-26 14:45:24' + } + } + } + }, + { + self.zone_name_key: "zone-2", + self.zone_state_key: {"available": False}, + "hosts": None + } + ] + } + + self.requests_mock.get(self.url('detail'), + json=get_os_zone_detail, + headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/base.py b/novaclient/tests/unit/fixture_data/base.py new file mode 100644 index 000000000..e08d82db6 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/base.py @@ -0,0 +1,44 @@ +# 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 + +import fixtures + +from novaclient.tests.unit.v2 import fakes + +COMPUTE_URL = 'http://compute.host' + + +class Fixture(fixtures.Fixture): + + base_url = None + json_headers = {'Content-Type': 'application/json', + 'x-openstack-request-id': fakes.FAKE_REQUEST_ID} + + def __init__(self, requests_mock, compute_url=COMPUTE_URL): + super(Fixture, self).__init__() + self.requests_mock = requests_mock + self.compute_url = compute_url + + def url(self, *args, **kwargs): + url_args = [self.compute_url] + + if self.base_url: + url_args.append(self.base_url) + + url = '/'.join(str(a).strip('/') for a in tuple(url_args) + args) + + if kwargs: + url += '?%s' % parse.urlencode(kwargs, doseq=True) + + return url diff --git a/novaclient/tests/unit/fixture_data/client.py b/novaclient/tests/unit/fixture_data/client.py new file mode 100644 index 000000000..d72ed6a35 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/client.py @@ -0,0 +1,75 @@ +# 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 fixtures +from keystoneauth1 import fixture +from keystoneauth1 import loading +from keystoneauth1 import session + +from novaclient import client + +IDENTITY_URL = 'http://identity.host/v3' +COMPUTE_URL = 'http://compute.host' + + +class V1(fixtures.Fixture): + + def __init__(self, requests_mock, + compute_url=COMPUTE_URL, identity_url=IDENTITY_URL, + **client_kwargs): + super(V1, self).__init__() + self.identity_url = identity_url + self.compute_url = compute_url + self.client = None + self.requests_mock = requests_mock + + self.token = fixture.V2Token() + self.token.set_scope() + self.discovery = fixture.V2Discovery(href=self.identity_url) + + s = self.token.add_service('compute') + s.add_endpoint(self.compute_url) + + s = self.token.add_service('computev3') + s.add_endpoint(self.compute_url) + + self._client_kwargs = client_kwargs + + def setUp(self): + super(V1, self).setUp() + + auth_url = '%s/tokens' % self.identity_url + headers = {'X-Content-Type': 'application/json'} + self.requests_mock.post(auth_url, + json=self.token, + headers=headers) + self.requests_mock.get(self.identity_url, + json=self.discovery, + headers=headers) + self.client = self.new_client(**self._client_kwargs) + + def new_client(self, **client_kwargs): + return client.Client("2", username='xx', + password='xx', + project_id='xx', + auth_url=self.identity_url, + **client_kwargs) + + +class SessionV1(V1): + + def new_client(self): + self.session = session.Session() + loader = loading.get_plugin_loader('password') + self.session.auth = loader.load_from_options( + auth_url=self.identity_url, username='xx', password='xx') + return client.Client("2", session=self.session) diff --git a/novaclient/tests/unit/fixture_data/floatingips.py b/novaclient/tests/unit/fixture_data/floatingips.py new file mode 100644 index 000000000..eae3be743 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/floatingips.py @@ -0,0 +1,47 @@ +# 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 FloatingFixture(base.Fixture): + + base_url = 'os-floating-ips' + + def setUp(self): + super(FloatingFixture, self).setUp() + + floating_ips = [{'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, + {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}] + + get_os_floating_ips = {'floating_ips': floating_ips} + self.requests_mock.get(self.url(), + json=get_os_floating_ips, + headers=self.json_headers) + + for ip in floating_ips: + get_os_floating_ip = {'floating_ip': ip} + self.requests_mock.get(self.url(ip['id']), + json=get_os_floating_ip, + headers=self.json_headers) + + self.requests_mock.delete(self.url(ip['id']), + headers=self.json_headers, + status_code=204) + + def post_os_floating_ips(request, context): + ip = floating_ips[0].copy() + ip['pool'] = request.json().get('pool') + return {'floating_ip': ip} + self.requests_mock.post(self.url(), + json=post_os_floating_ips, + headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/hypervisors.py b/novaclient/tests/unit/fixture_data/hypervisors.py new file mode 100644 index 000000000..3c04daa74 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/hypervisors.py @@ -0,0 +1,298 @@ +# 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 oslo_utils import encodeutils + +from novaclient import api_versions +from novaclient.tests.unit.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'os-hypervisors' + api_version = '2.1' + hyper_id_1 = 1234 + hyper_id_2 = 5678 + 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() + + api_version = api_versions.APIVersion(self.api_version) + + get_os_hypervisors = { + 'hypervisors': [ + { + 'id': self.hyper_id_1, + 'hypervisor_hostname': 'hyper1', + 'state': 'up', + 'status': 'enabled', + }, + { + 'id': self.hyper_id_2, + 'hypervisor_hostname': 'hyper2', + 'state': 'up', + 'status': 'enabled', + }, + ] + } + + self.headers = self.json_headers + + self.requests_mock.get(self.url(), + json=get_os_hypervisors, + headers=self.headers) + + get_os_hypervisors_detail = { + 'hypervisors': [ + { + 'id': self.hyper_id_1, + 'service': { + 'id': self.service_id_1, + 'host': 'compute1', + }, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'hypervisor_hostname': 'hyper1', + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100, + 'state': 'up', + 'status': 'enabled', + }, + { + 'id': self.hyper_id_2, + 'service': { + 'id': self.service_id_2, + 'host': 'compute2', + }, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'hypervisor_hostname': 'hyper2', + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + '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) + + get_os_hypervisors_stats = { + 'hypervisor_statistics': { + 'count': 2, + 'vcpus': 8, + 'memory_mb': 20 * 1024, + 'local_gb': 500, + 'vcpus_used': 4, + 'memory_mb_used': 10 * 1024, + 'local_gb_used': 250, + 'free_ram_mb': 10 * 1024, + 'free_disk_gb': 250, + 'current_workload': 4, + 'running_vms': 4, + 'disk_available_least': 200, + } + } + + self.requests_mock.get(self.url('statistics'), + json=get_os_hypervisors_stats, + headers=self.headers) + + get_os_hypervisors_search = { + 'hypervisors': [ + { + '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 api_version >= api_versions.APIVersion('2.53'): + url = self.url(hypervisor_hostname_pattern='hyper') + else: + url = self.url('hyper', 'search') + self.requests_mock.get(url, + 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'} + ] + }, + { + 'id': self.hyper_id_2, + 'hypervisor_hostname': 'hyper2', + 'state': 'up', + 'status': 'enabled', + 'servers': [ + {'name': 'inst3', 'uuid': 'uuid3'}, + {'name': 'inst4', 'uuid': 'uuid4'} + ] + } + ] + } + + if api_version >= api_versions.APIVersion('2.53'): + url = self.url(hypervisor_hostname_pattern='hyper', + with_servers=True) + else: + url = self.url('hyper', 'servers') + self.requests_mock.get(url, + json=get_hyper_server, + headers=self.headers) + + get_os_hypervisors_hyper1 = { + 'hypervisor': { + 'id': self.hyper_id_1, + 'service': {'id': self.service_id_1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': 'xen', + 'hypervisor_version': 3, + 'hypervisor_hostname': 'hyper1', + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + '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) + + get_os_hypervisors_uptime = { + 'hypervisor': { + 'id': self.hyper_id_1, + 'hypervisor_hostname': 'hyper1', + 'uptime': 'fake uptime', + 'state': 'up', + 'status': 'enabled', + } + } + + self.requests_mock.get(self.url(self.hyper_id_1, 'uptime'), + json=get_os_hypervisors_uptime, + headers=self.headers) + + +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/images.py b/novaclient/tests/unit/fixture_data/images.py new file mode 100644 index 000000000..5f8b30df4 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/images.py @@ -0,0 +1,80 @@ +# 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 import fakes +from novaclient.tests.unit.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'v2/images' + + def setUp(self): + super(V1, self).setUp() + + get_images = { + 'images': [ + {'id': 1, 'name': 'CentOS 5.2'}, + {'id': 2, 'name': 'My Server Backup'} + ] + } + + headers = self.json_headers + + self.requests_mock.get(self.url(), + json=get_images, + headers=headers) + + image_1 = { + 'id': 1, + 'name': 'CentOS 5.2', + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "ACTIVE", + "metadata": { + "test_key": "test_value", + }, + "links": {}, + } + + image_2 = { + "id": 2, + "name": "My Server Backup", + "serverId": 1234, + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "SAVING", + "progress": 80, + "links": {}, + } + + self.requests_mock.get(self.url('detail'), + json={'images': [image_1, image_2]}, + headers=headers) + + self.requests_mock.get(self.url(1), + json={'image': image_1}, + headers=headers) + + def post_images_1_metadata(request, context): + body = request.json() + assert list(body) == ['metadata'] + fakes.assert_has_keys(body['metadata'], required=['test_key']) + return {'metadata': image_1['metadata']} + + self.requests_mock.post(self.url(1, 'metadata'), + json=post_images_1_metadata, + headers=headers) + + for u in (1, '1/metadata/test_key'): + self.requests_mock.delete(self.url(u), status_code=204, + headers=headers) diff --git a/novaclient/tests/unit/fixture_data/keypairs.py b/novaclient/tests/unit/fixture_data/keypairs.py new file mode 100644 index 000000000..5ac1b4ad7 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/keypairs.py @@ -0,0 +1,57 @@ +# 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 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 + + self.requests_mock.get(self.url(), + json={'keypairs': [keypair]}, + headers=headers) + + self.requests_mock.get(self.url('test'), + json={'keypair': keypair}, + headers=headers) + + self.requests_mock.delete(self.url('test'), + status_code=202, + headers=headers) + + def post_os_keypairs(request, context): + body = request.json() + assert list(body) == ['keypair'] + 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(), + json=post_os_keypairs, + headers=headers) diff --git a/novaclient/tests/unit/fixture_data/limits.py b/novaclient/tests/unit/fixture_data/limits.py new file mode 100644 index 000000000..0e7986580 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/limits.py @@ -0,0 +1,91 @@ +# 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 = 'limits' + absolute = { + "maxTotalRAMSize": 51200, + "maxServerMeta": 5, + "maxImageMeta": 5, + "maxPersonality": 5, + "maxPersonalitySize": 10240 + } + + def setUp(self): + super(Fixture, self).setUp() + + get_limits = { + "limits": { + "rate": [ + { + "uri": "*", + "regex": ".*", + "limit": [ + { + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + }, + { + "value": 10, + "verb": "PUT", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + }, + { + "value": 100, + "verb": "DELETE", + "remaining": 100, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + } + ] + }, + { + "uri": "*/servers", + "regex": "^/servers", + "limit": [ + { + "verb": "POST", + "value": 25, + "remaining": 24, + "unit": "DAY", + "next-available": "2011-12-15T22:42:45Z" + } + ] + } + ], + "absolute": self.absolute, + }, + } + + headers = self.json_headers + 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 new file mode 100644 index 000000000..1ffa8c265 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/quotas.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. + +from novaclient.tests.unit.fixture_data import base + + +class V1(base.Fixture): + + base_url = 'os-quota-sets' + + def setUp(self): + super(V1, self).setUp() + + uuid = '97f4c221-bff4-4578-b030-0df4ef119353' + uuid2 = '97f4c221bff44578b0300df4ef119353' + test_json = {'quota_set': self.test_quota('test')} + self.headers = self.json_headers + + for u in ('test', 'tenant-id', 'tenant-id/defaults', + '%s/defaults' % uuid2, 'test/detail'): + self.requests_mock.get(self.url(u), + json=test_json, + headers=self.headers) + + self.requests_mock.put(self.url(uuid), + json={'quota_set': self.test_quota(uuid)}, + headers=self.headers) + + self.requests_mock.get(self.url(uuid), + json={'quota_set': self.test_quota(uuid)}, + headers=self.headers) + + self.requests_mock.put(self.url(uuid2), + json={'quota_set': self.test_quota(uuid2)}, + headers=self.headers) + self.requests_mock.get(self.url(uuid2), + json={'quota_set': self.test_quota(uuid2)}, + headers=self.headers) + + for u in ('test', uuid2): + self.requests_mock.delete(self.url(u), status_code=202, + headers=self.headers) + + def test_quota(self, tenant_id='test'): + return { + '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, + 'key_pairs': 1, + 'security_groups': 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 new file mode 100644 index 000000000..d2fd43c7d --- /dev/null +++ b/novaclient/tests/unit/fixture_data/server_groups.py @@ -0,0 +1,114 @@ +# 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-server-groups' + + def setUp(self): + super(Fixture, self).setUp() + + server_groups = [ + { + "members": [], + "metadata": {}, + "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", + "policies": [], + "name": "ig1" + }, + { + "members": [], + "metadata": {}, + "id": "4473bb03-4370-4bfb-80d3-dc8cffc47d94", + "policies": ["anti-affinity"], + "name": "ig2" + }, + { + "members": [], + "metadata": {"key": "value"}, + "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", + "policies": [], "name": "ig3" + }, + { + "members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], + "metadata": {}, + "id": "4890bb03-7070-45fb-8453-d34556c87d94", + "policies": ["anti-affinity"], + "name": "ig2" + } + ] + + other_project_server_groups = [ + { + "members": [], + "metadata": {}, + "id": "11111111-1111-1111-1111-111111111111", + "policies": [], + "name": "ig4" + }, + { + "members": [], + "metadata": {}, + "id": "22222222-2222-2222-2222-222222222222", + "policies": ["anti-affinity"], + "name": "ig5" + }, + { + "members": [], + "metadata": {"key": "value"}, + "id": "33333333-3333-3333-3333-333333333333", + "policies": [], "name": "ig6" + }, + { + "members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], + "metadata": {}, + "id": "44444444-4444-4444-4444-444444444444", + "policies": ["anti-affinity"], + "name": "ig5" + } + ] + + headers = self.json_headers + + self.requests_mock.get(self.url(), + json={'server_groups': server_groups}, + headers=headers) + + self.requests_mock.get(self.url(all_projects=True), + json={'server_groups': server_groups + + other_project_server_groups}, + headers=headers) + + self.requests_mock.get(self.url(limit=2, offset=1), + json={'server_groups': server_groups[1:3]}, + headers=headers) + + server = server_groups[0] + + def _register(method, *args): + self.requests_mock.register_uri(method, + self.url(*args), + json={'server_group': server}, + headers=headers) + + _register('POST') + _register('POST', server['id']) + _register('GET', server['id']) + _register('PUT', server['id']) + _register('POST', server['id'], '/action') + + self.requests_mock.delete(self.url(server['id']), + status_code=202, + headers=headers) diff --git a/novaclient/tests/unit/fixture_data/server_migrations.py b/novaclient/tests/unit/fixture_data/server_migrations.py new file mode 100644 index 000000000..4fe68e130 --- /dev/null +++ b/novaclient/tests/unit/fixture_data/server_migrations.py @@ -0,0 +1,80 @@ +# Copyright 2016 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.tests.unit.fixture_data import base + + +class Fixture(base.Fixture): + base_url = 'servers' + + def setUp(self): + super(Fixture, self).setUp() + url = self.url('1234', 'migrations', '1', 'action') + self.requests_mock.post(url, + status_code=202, + headers=self.json_headers) + + get_migrations = {'migrations': [ + { + "created_at": "2016-01-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1, + "server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe", + "source_compute": "compute1", + "source_node": "node1", + "status": "running", + "memory_total_bytes": 123456, + "memory_processed_bytes": 12345, + "memory_remaining_bytes": 120000, + "disk_total_bytes": 234567, + "disk_processed_bytes": 23456, + "disk_remaining_bytes": 230000, + "updated_at": "2016-01-29T13:42:02.000000" + }]} + + url = self.url('1234', 'migrations') + self.requests_mock.get(url, + json=get_migrations, + headers=self.json_headers) + + get_migration = {'migration': { + "created_at": "2016-01-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1, + "server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe", + "source_compute": "compute1", + "source_node": "node1", + "status": "running", + "memory_total_bytes": 123456, + "memory_processed_bytes": 12345, + "memory_remaining_bytes": 120000, + "disk_total_bytes": 234567, + "disk_processed_bytes": 23456, + "disk_remaining_bytes": 230000, + "updated_at": "2016-01-29T13:42:02.000000" + }} + + url = self.url('1234', 'migrations', '1') + self.requests_mock.get(url, + json=get_migration, + headers=self.json_headers) + url = self.url('1234', 'migrations', '1') + self.requests_mock.delete(url, + status_code=202, + headers=self.json_headers) diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py new file mode 100644 index 000000000..1635e33dc --- /dev/null +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -0,0 +1,509 @@ +# 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 fakes +from novaclient.tests.unit.fixture_data import base +from novaclient.tests.unit.v2 import fakes as v2_fakes + + +class Base(base.Fixture): + + base_url = 'servers' + + def setUp(self): + super(Base, self).setUp() + + get_servers = { + "servers": [ + {'id': 1234, 'name': 'sample-server'}, + {'id': 5678, 'name': 'sample-server2'} + ] + } + + self.requests_mock.get(self.url(), + json=get_servers, + headers=self.json_headers) + + self.server_1234 = { + "id": 1234, + "name": "sample-server", + "image": { + "id": 2, + "name": "sample image", + }, + "flavor": { + "id": 1, + "name": "256 MiB Server", + }, + "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", + "status": "BUILD", + "progress": 60, + "addresses": { + "public": [ + { + "version": 4, + "addr": "1.2.3.4", + }, + { + "version": 4, + "addr": "5.6.7.8", + }], + "private": [{ + "version": 4, + "addr": "10.11.12.13", + }], + }, + "metadata": { + "Server Label": "Web Head 1", + "Image Version": "2.1" + }, + "OS-EXT-SRV-ATTR:host": "computenode1", + "security_groups": [{ + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], + "OS-EXT-MOD:some_thing": "mod_some_thing_value", + } + + self.server_5678 = { + "id": 5678, + "name": "sample-server2", + "image": { + "id": 2, + "name": "sample image", + }, + "flavor": { + "id": 1, + "name": "256 MiB Server", + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], + "private": [{ + "version": 4, + "addr": "10.13.12.13", + }], + }, + "metadata": { + "Server Label": "DB 1" + }, + "OS-EXT-SRV-ATTR:host": "computenode2", + "security_groups": [ + { + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }, + { + 'id': 2, 'name': 'securitygroup2', + 'description': 'ANOTHER_FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], + } + + self.server_9012 = { + "id": 9012, + "name": "sample-server3", + "image": "", + "flavor": { + "id": 1, + "name": "256 MiB Server", + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], + "private": [{ + "version": 4, + "addr": "10.13.12.13", + }], + }, + "metadata": { + "Server Label": "DB 1" + } + } + + servers = [self.server_1234, self.server_5678, self.server_9012] + get_servers_detail = {"servers": servers} + + self.requests_mock.get(self.url('detail'), + json=get_servers_detail, + headers=self.json_headers) + + self.requests_mock.get( + self.url('detail', marker=self.server_1234["id"]), + json={"servers": [self.server_1234, self.server_5678]}, + headers=self.json_headers, complete_qs=True) + + self.requests_mock.get( + self.url('detail', marker=self.server_5678["id"]), + json={"servers": []}, + headers=self.json_headers, complete_qs=True) + + self.server_1235 = self.server_1234.copy() + self.server_1235['id'] = 1235 + self.server_1235['status'] = 'error' + self.server_1235['fault'] = {'message': 'something went wrong!'} + + for s in servers + [self.server_1235]: + self.requests_mock.get(self.url(s['id']), + json={'server': s}, + headers=self.json_headers) + + for s in (1234, 5678): + self.requests_mock.delete(self.url(s), + status_code=202, + headers=self.json_headers) + + for k in ('test_key', 'key1', 'key2'): + self.requests_mock.delete(self.url(1234, 'metadata', k), + status_code=204, + headers=self.json_headers) + + metadata1 = {'metadata': {'test_key': 'test_value'}} + self.requests_mock.post(self.url(1234, 'metadata'), + json=metadata1, + headers=self.json_headers) + self.requests_mock.put(self.url(1234, 'metadata', 'test_key'), + json=metadata1, + headers=self.json_headers) + + self.diagnostic = {'data': 'Fake diagnostics'} + + metadata2 = {'metadata': {'key1': 'val1'}} + for u in ('uuid1', 'uuid2', 'uuid3', 'uuid4'): + self.requests_mock.post(self.url(u, 'metadata'), + json=metadata2, status_code=204) + 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": [{ + 'id': 1, + 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'rules': []}] + } + + self.requests_mock.get(self.url('1234', 'os-security-groups'), + json=get_security_groups, + headers=self.json_headers) + + self.requests_mock.post(self.url(), + json=self.post_servers, + headers=self.json_headers) + + self.requests_mock.post(self.url('1234', 'remote-consoles'), + json=self.post_servers_1234_remote_consoles, + headers=self.json_headers) + + self.requests_mock.post(self.url('1234', 'action'), + json=self.post_servers_1234_action, + headers=self.json_headers) + + get_os_interface = { + "interfaceAttachments": [ + { + "port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }, + { + "port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + } + ] + } + + self.requests_mock.get(self.url('1234', 'os-interface'), + json=get_os_interface, + headers=self.json_headers) + + interface_data = {'interfaceAttachment': {}} + self.requests_mock.post(self.url('1234', 'os-interface'), + json=interface_data, + headers=self.json_headers) + + def put_servers_1234(request, context): + body = request.json() + assert list(body) == ['server'] + fakes.assert_has_keys(body['server'], + optional=['name', 'adminPass']) + return request.body + + self.requests_mock.put(self.url(1234), + text=put_servers_1234, + status_code=204, + headers=self.json_headers) + + # + # Server password + # + + self.requests_mock.delete(self.url(1234, 'os-server-password'), + status_code=202, + headers=self.json_headers) + # + # Server tags + # + + self.requests_mock.get(self.url(1234, 'tags'), + json={'tags': ['tag1', 'tag2']}, + headers=self.json_headers) + + self.requests_mock.get(self.url(1234, 'tags', 'tag'), + status_code=204, + headers=self.json_headers) + + self.requests_mock.delete(self.url(1234, 'tags', 'tag'), + status_code=204, + headers=self.json_headers) + + self.requests_mock.delete(self.url(1234, 'tags'), + status_code=204, + headers=self.json_headers) + + def put_server_tag(request, context): + assert request.text is None + context.status_code = 201 + return None + + self.requests_mock.put(self.url(1234, 'tags', 'tag'), + json=put_server_tag, + headers=self.json_headers) + + def put_server_tags(request, context): + body = request.json() + assert list(body) == ['tags'] + return body + + self.requests_mock.put(self.url(1234, 'tags'), + json=put_server_tags, + headers=self.json_headers) + + +class V1(Base): + + def setUp(self): + super(V1, self).setUp() + + # + # Server Addresses + # + + add = self.server_1234['addresses'] + self.requests_mock.get(self.url(1234, 'ips'), + json={'addresses': add}, + headers=self.json_headers) + + self.requests_mock.get(self.url(1234, 'ips', 'public'), + json={'public': add['public']}, + headers=self.json_headers) + + self.requests_mock.get(self.url(1234, 'ips', 'private'), + json={'private': add['private']}, + headers=self.json_headers) + + self.requests_mock.delete(self.url(1234, 'ips', 'public', '1.2.3.4'), + status_code=202) + + self.requests_mock.get(self.url('1234', 'diagnostics'), + json=self.diagnostic, + headers=self.json_headers) + + 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 + # + # RSA Private Key: novaclient/tests/unit/idfake.pem + # + # Encrypted password + # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r + # qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho + # QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw + # /y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N + # tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk + # Hi/fmZZNQQqj1Ijq0caOIw== + + get_server_password = { + 'password': + 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' + 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' + 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' + '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' + 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' + 'Hi/fmZZNQQqj1Ijq0caOIw=='} + self.requests_mock.get(self.url(1234, 'os-server-password'), + json=get_server_password, + headers=self.json_headers) + + def post_servers(self, request, context): + body = request.json() + context.status_code = 202 + assert (set(body.keys()) <= + set(['server', 'os:scheduler_hints'])) + fakes.assert_has_keys(body['server'], + required=['name', 'imageRef', 'flavorRef'], + optional=['metadata', 'personality']) + if 'personality' in body['server']: + for pfile in body['server']['personality']: + fakes.assert_has_keys(pfile, required=['path', 'contents']) + if ('return_reservation_id' in body['server'].keys() and + body['server']['return_reservation_id']): + return {'reservation_id': 'r-3fhpjulh'} + if body['server']['name'] == 'some-bad-server': + body = self.server_1235 + else: + body = self.server_1234 + + return {'server': body} + + def post_servers_1234_remote_consoles(self, request, context): + _body = '' + body = request.json() + context.status_code = 202 + assert len(body.keys()) == 1 + assert 'remote_console' in body.keys() + assert 'protocol' in body['remote_console'].keys() + protocol = body['remote_console']['protocol'] + + _body = {'protocol': protocol, 'type': 'novnc', + 'url': 'http://example.com:6080/vnc_auto.html?token=XYZ'} + + return {'remote_console': _body} + + def post_servers_1234_action(self, request, context): + _body = '' + body = request.json() + context.status_code = 202 + assert len(body.keys()) == 1 + action = list(body)[0] + api_version = api_versions.APIVersion( + request.headers.get('X-OpenStack-Nova-API-Version', '2.1')) + + if v2_fakes.FakeSessionClient.check_server_actions(body): + # NOTE(snikitin): No need to do any operations here. This 'pass' + # is needed to avoid AssertionError in the last 'else' statement + # if we found 'action' in method check_server_actions and + # raise AssertionError if we didn't find 'action' at all. + pass + elif action == 'os-migrateLive': + # Fixme(eliqiao): body of os-migrateLive changes from v2.25 + # 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') + assert 'imageRef' in body + _body = self.server_1234.copy() + _body['adminPass'] = adminPass + elif action == 'confirmResize': + assert body[action] is None + # This one method returns a different response code + context.status_code = 204 + return None + elif action == 'rescue': + if body[action]: + keys = set(body[action].keys()) + assert not (keys - set(['adminPass', 'rescue_image_ref'])) + else: + assert body[action] is None + _body = {'adminPass': 'RescuePassword'} + elif action == 'createImage': + assert set(body[action].keys()) == set(['name', 'metadata']) + if api_version >= api_versions.APIVersion('2.45'): + return {'image_id': '456'} + context.headers['location'] = "http://blah/images/456" + elif action == 'createBackup': + assert set(body[action].keys()) == set(['name', 'backup_type', + 'rotation']) + if api_version >= api_versions.APIVersion('2.45'): + return {'image_id': '456'} + context.headers['location'] = "http://blah/images/456" + elif action == 'os-getConsoleOutput': + assert list(body[action]) == ['length'] + context.status_code = 202 + return {'output': 'foo'} + elif action == 'os-getSerialConsole': + assert list(body[action]) == ['type'] + elif action == 'evacuate': + keys = list(body[action]) + if 'adminPass' in keys: + keys.remove('adminPass') + assert 'host' in keys + else: + raise AssertionError("Unexpected server action: %s" % action) + return {'server': _body} diff --git a/novaclient/tests/unit/idfake.pem b/novaclient/tests/unit/idfake.pem new file mode 100644 index 000000000..f7b446641 --- /dev/null +++ b/novaclient/tests/unit/idfake.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA9QstF/7prDY7a9La7GS9TpMX+MWWXQgK6pHRLakDFp1WX1Q3 +Vly7rWitaZUGirUPMm181oJXBwkKlAxFD7hKjyHYaSswNszPYIAsVkc1+AO5epXz +g9kUBNtfg44Pg72UecwLrZ8JpmNZpJlKQOx6vF+yi7JmHrrIf6il/grIGUPzoT2L +yReimpyPoBrGtXhJYaCJ/XbKg1idRZiQdmwh1F/OmZWn9p0wunnsv08a0+qIywuw +WhG9/Zy9fjnEByfusS6gI0GIxDRL4RWzOqphd3PZzunwIBgEKFhgiki9+2DgcRVO +9I5wnDvfwQREJRZWh1uJa5ZTcfPa1EzZryVeOQIDAQABAoIBABxO3Te/cBk/7p9n +LXlPrfrszUEk+ljm+/PbQpIGy1+Kb5b1sKrebaP7ysS+vZG6lvXZZimVxx398mXm +APhu7tYYL9r+bUR3ZqGcTQLumRJ8w6mgtxANPN3Oxfr5p1stxIBJjTPSgpfhNFLq +joRvjUJDv+mZg2ibZVwyDHMLpdAdKp+3XMdyTLZcH9esqwii+natix7rHd1RuF85 +L1dfpxjkItwhgHsfdYS++5X3fRByFOhQ+Nhabh/kPQbQMcteRn1bN6zeCWBSglNb +Ka/ZrXb6ApRUc22Ji62mNO2ZPPekLJeCHk2h2E7ezYX+sGDNvvd/jHVDJJ20FjD1 +Z9KXuK0CgYEA/2vniy9yWd925QQtWbmrxgy6yj89feMH/LTv4qP298rGZ2nqxsyd +9pdBdb4NMsi4HmV5PG1hp3VRNBHl53DNh5eqzT8WEXnIF+sbrIU3KzrCVAx1kZTl ++OWKA6aVUsvvO3y85SOvInnsV+IsOGmU4/WBSjYoe39Bo7mq/YuZB9MCgYEA9ZlB +KBm6PjFdHQGNgedXahWzRcwC+ALCYqequPYqJolNzhrK4Uc2sWPSGdnldcHZ4XCQ +wbfCxUSwrMpA1oyuIQ0U4aowmOw5DjIueBWI8XBYEVRBlwvJwbXpBZ/DspGzTUDx +MBrrEwEaMadQvxhRnAzhp0rQAepatcz6Fgb1JkMCgYBMwDLiew5kfSav6JJsDMPW +DksurNQgeNEUmZYfx19V1EPMHWKj/CZXS9oqtEIpCXFyCNHmW4PlmvYcrGgmJJpN +7UAwzo0mES8UKNy2+Yy7W7u7H8dQSKrWILtZH3xtVcR8Xp4wSIm+1V40hkz9YpSP +71y7XQzLF1E1DnyYFZOVawKBgAFrmHfd5jjT2kD/sEzPBK9lXrsJmf7LLUqaw578 +NXQxmRSXDRNOcR+Hf0CNBQmwTE1EdGHaaTLw2cC2Drfu6lbgl31SmaNYwl+1pJUn +MrqKtseq4BI6jDkljypsKRqQQyQwOvTXQwLCH9+nowzn3Bj17hwkj51jOJESlWOp +OKO3AoGBALm+jjqyqX7gSnqK3FAumB8mlhv3yI1Wr1ctwe18mKfKbz17HxXRu9pF +K/6e7WMCA1p+jhoE8gj1h2WBcH0nV2qt8Ye8gJBbCi4dhI08o4AfrIV47oZx1RlO +qYcA1U9lyaODY5SL8+6PHOy5J/aYtuA+wvfEnWiCIdKQrhWetcn3 +-----END RSA PRIVATE KEY----- diff --git a/novaclient/tests/unit/test_api_versions.py b/novaclient/tests/unit/test_api_versions.py new file mode 100644 index 000000000..a5c8cb1c2 --- /dev/null +++ b/novaclient/tests/unit/test_api_versions.py @@ -0,0 +1,478 @@ +# Copyright 2016 Mirantis +# 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 unittest import mock + +import novaclient +from novaclient import api_versions +from novaclient import exceptions +from novaclient.tests.unit import utils +from novaclient import utils as nutils +from novaclient.v2 import versions + + +class APIVersionTestCase(utils.TestCase): + def test_valid_version_strings(self): + def _test_string(version, exp_major, exp_minor): + v = api_versions.APIVersion(version) + self.assertEqual(v.ver_major, exp_major) + self.assertEqual(v.ver_minor, exp_minor) + + _test_string("1.1", 1, 1) + _test_string("2.10", 2, 10) + _test_string("5.234", 5, 234) + _test_string("12.5", 12, 5) + _test_string("2.0", 2, 0) + _test_string("2.200", 2, 200) + + def test_null_version(self): + v = api_versions.APIVersion() + self.assertTrue(v.is_null()) + + def test_invalid_version_strings(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "200") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.1.4") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "200.23.66.3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5 .3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5. 3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5.03") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "02.1") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.001") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, " 2.1") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.1 ") + + def test_version_comparisons(self): + v1 = api_versions.APIVersion("2.0") + v2 = api_versions.APIVersion("2.5") + v3 = api_versions.APIVersion("5.23") + v4 = api_versions.APIVersion("2.0") + v_null = api_versions.APIVersion() + + self.assertTrue(v1.__lt__(v2)) + self.assertTrue(v3.__gt__(v2)) + self.assertTrue(v1.__ne__(v2)) + self.assertTrue(v1.__eq__(v4)) + self.assertTrue(v1.__ne__(v_null)) + self.assertTrue(v_null.__eq__(v_null)) + self.assertRaises(TypeError, v1.__le__, "2.1") + + def test_version_matches(self): + v1 = api_versions.APIVersion("2.0") + v2 = api_versions.APIVersion("2.5") + v3 = api_versions.APIVersion("2.45") + v4 = api_versions.APIVersion("3.3") + v5 = api_versions.APIVersion("3.23") + v6 = api_versions.APIVersion("2.0") + v7 = api_versions.APIVersion("3.3") + v8 = api_versions.APIVersion("4.0") + v_null = api_versions.APIVersion() + + self.assertTrue(v2.matches(v1, v3)) + self.assertTrue(v2.matches(v1, v_null)) + self.assertTrue(v1.matches(v6, v2)) + self.assertTrue(v4.matches(v2, v7)) + self.assertTrue(v4.matches(v_null, v7)) + self.assertTrue(v4.matches(v_null, v8)) + self.assertFalse(v1.matches(v2, v3)) + self.assertFalse(v5.matches(v2, v4)) + self.assertFalse(v2.matches(v3, v1)) + + self.assertRaises(ValueError, v_null.matches, v1, v3) + + def test_get_string(self): + v1_string = "3.23" + v1 = api_versions.APIVersion(v1_string) + self.assertEqual(v1_string, v1.get_string()) + + self.assertRaises(ValueError, + api_versions.APIVersion().get_string) + + +class UpdateHeadersTestCase(utils.TestCase): + def test_api_version_is_null(self): + headers = {} + api_versions.update_headers(headers, api_versions.APIVersion()) + self.assertEqual({}, headers) + + def test_api_version_is_major(self): + headers = {} + api_versions.update_headers(headers, api_versions.APIVersion("7.0")) + self.assertEqual({}, headers) + + def test_api_version_is_not_null(self): + api_version = api_versions.APIVersion("2.3") + headers = {} + api_versions.update_headers(headers, api_version) + self.assertEqual( + {"X-OpenStack-Nova-API-Version": api_version.get_string()}, + headers) + + def test_api_version_is_gte_27(self): + api_version = api_versions.APIVersion("2.27") + headers = {} + api_versions.update_headers(headers, api_version) + self.assertIn('X-OpenStack-Nova-API-Version', headers) + self.assertIn('OpenStack-API-Version', headers) + self.assertEqual(api_version.get_string(), + headers['X-OpenStack-Nova-API-Version']) + self.assertEqual('%s %s' % (api_versions.SERVICE_TYPE, + api_version.get_string()), + headers['OpenStack-API-Version']) + + +class CheckHeadersTestCase(utils.TestCase): + def setUp(self): + super(CheckHeadersTestCase, self).setUp() + mock_log_patch = mock.patch("novaclient.api_versions.LOG") + self.mock_log = mock_log_patch.start() + self.addCleanup(mock_log_patch.stop) + + def test_legacy_microversion_is_specified(self): + response = mock.MagicMock( + headers={api_versions.LEGACY_HEADER_NAME: ""}) + api_versions.check_headers(response, api_versions.APIVersion("2.2")) + self.assertFalse(self.mock_log.warning.called) + + response = mock.MagicMock(headers={}) + api_versions.check_headers(response, api_versions.APIVersion("2.2")) + self.assertTrue(self.mock_log.warning.called) + + def test_generic_microversion_is_specified(self): + response = mock.MagicMock( + headers={api_versions.HEADER_NAME: ""}) + api_versions.check_headers(response, api_versions.APIVersion("2.27")) + self.assertFalse(self.mock_log.warning.called) + + response = mock.MagicMock(headers={}) + api_versions.check_headers(response, api_versions.APIVersion("2.27")) + self.assertTrue(self.mock_log.warning.called) + + def test_microversion_is_not_specified(self): + response = mock.MagicMock( + headers={api_versions.LEGACY_HEADER_NAME: ""}) + api_versions.check_headers(response, api_versions.APIVersion("2.2")) + self.assertFalse(self.mock_log.warning.called) + + response = mock.MagicMock(headers={}) + api_versions.check_headers(response, api_versions.APIVersion("2.0")) + self.assertFalse(self.mock_log.warning.called) + + +class GetAPIVersionTestCase(utils.TestCase): + def test_get_available_client_versions(self): + output = api_versions.get_available_major_versions() + self.assertNotEqual([], output) + + def test_wrong_format(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.get_api_version, "something_wrong") + + def test_wrong_major_version(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.get_api_version, "1") + + @mock.patch("novaclient.api_versions.APIVersion") + def test_only_major_part_is_presented(self, mock_apiversion): + version = 7 + self.assertEqual(mock_apiversion.return_value, + api_versions.get_api_version(version)) + mock_apiversion.assert_called_once_with("%s.0" % str(version)) + + @mock.patch("novaclient.api_versions.APIVersion") + def test_major_and_minor_parts_is_presented(self, mock_apiversion): + version = "2.7" + self.assertEqual(mock_apiversion.return_value, + api_versions.get_api_version(version)) + mock_apiversion.assert_called_once_with(version) + + +class WrapsTestCase(utils.TestCase): + + def _get_obj_with_vers(self, vers): + return mock.MagicMock(api_version=api_versions.APIVersion(vers)) + + def _side_effect_of_vers_method(self, *args, **kwargs): + m = mock.MagicMock(start_version=args[1], end_version=args[2]) + m.name = args[0] + return m + + @mock.patch("novaclient.api_versions._get_function_name") + @mock.patch("novaclient.api_versions.VersionedMethod") + def test_end_version_is_none(self, mock_versioned_method, mock_name): + mock_versioned_method.side_effect = self._side_effect_of_vers_method + + @api_versions.wraps("2.2") + def foo(*args, **kwargs): + pass + + foo(self._get_obj_with_vers("2.4")) + + mock_versioned_method.assert_called_once_with( + mock_name.return_value, api_versions.APIVersion("2.2"), + api_versions.APIVersion("2.latest"), mock.ANY) + + @mock.patch("novaclient.api_versions._get_function_name") + @mock.patch("novaclient.api_versions.VersionedMethod") + def test_start_and_end_version_are_presented(self, mock_versioned_method, + mock_name): + mock_versioned_method.side_effect = self._side_effect_of_vers_method + + @api_versions.wraps("2.2", "2.6") + def foo(*args, **kwargs): + pass + + foo(self._get_obj_with_vers("2.4")) + + mock_versioned_method.assert_called_once_with( + mock_name.return_value, api_versions.APIVersion("2.2"), + api_versions.APIVersion("2.6"), mock.ANY) + + @mock.patch("novaclient.api_versions._get_function_name") + @mock.patch("novaclient.api_versions.VersionedMethod") + def test_api_version_doesnt_match(self, mock_versioned_method, mock_name): + mock_versioned_method.side_effect = self._side_effect_of_vers_method + + @api_versions.wraps("2.2", "2.6") + def foo(*args, **kwargs): + pass + + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + foo, self._get_obj_with_vers("2.1")) + + mock_versioned_method.assert_called_once_with( + mock_name.return_value, api_versions.APIVersion("2.2"), + api_versions.APIVersion("2.6"), mock.ANY) + + def test_define_method_is_actually_called(self): + checker = mock.MagicMock() + + @api_versions.wraps("2.2", "2.6") + def some_func(*args, **kwargs): + checker(*args, **kwargs) + + obj = self._get_obj_with_vers("2.4") + some_args = ("arg_1", "arg_2") + some_kwargs = {"key1": "value1", "key2": "value2"} + + some_func(obj, *some_args, **some_kwargs) + + checker.assert_called_once_with(*((obj,) + some_args), **some_kwargs) + + @mock.patch("novaclient.api_versions._get_function_name") + def test_arguments_property_is_copied(self, mock_name): + @nutils.arg("argument_1") + @api_versions.wraps("2.666", "2.777") + @nutils.arg("argument_2") + def some_func(): + pass + + versioned_method = api_versions.get_substitutions( + mock_name.return_value, api_versions.APIVersion("2.700"))[0] + + self.assertEqual(some_func.arguments, + versioned_method.func.arguments) + self.assertIn((("argument_1",), {}), versioned_method.func.arguments) + self.assertIn((("argument_2",), {}), versioned_method.func.arguments) + + def test_several_methods_with_same_name_in_one_module(self): + + class A(object): + api_version = api_versions.APIVersion("777.777") + + @api_versions.wraps("777.777") + def f(self): + return 1 + + class B(object): + api_version = api_versions.APIVersion("777.777") + + @api_versions.wraps("777.777") + def f(self): + return 2 + + self.assertEqual(1, A().f()) + self.assertEqual(2, B().f()) + + def test_generate_function_name(self): + expected_name = "novaclient.tests.unit.test_api_versions.fake_func" + + self.assertNotIn(expected_name, api_versions._SUBSTITUTIONS) + + @api_versions.wraps("7777777.7777777") + def fake_func(): + pass + + self.assertIn(expected_name, api_versions._SUBSTITUTIONS) + 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() + self.orig_max = novaclient.API_MAX_VERSION + self.orig_min = novaclient.API_MIN_VERSION + self.addCleanup(self._clear_fake_version) + + def _clear_fake_version(self): + novaclient.API_MAX_VERSION = self.orig_max + novaclient.API_MIN_VERSION = self.orig_min + + def test_server_is_too_new(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = mock.MagicMock( + version="2.7", min_version="2.4") + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.3") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.discover_version, fake_client, + api_versions.APIVersion('2.latest')) + + def test_server_is_too_old(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = mock.MagicMock( + version="2.7", min_version="2.4") + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.10") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.9") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.discover_version, fake_client, + api_versions.APIVersion('2.latest')) + + def test_server_end_version_is_the_latest_one(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = mock.MagicMock( + version="2.7", min_version="2.4") + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + + self.assertEqual( + "2.7", + api_versions.discover_version( + fake_client, + api_versions.APIVersion('2.latest')).get_string()) + + def test_client_end_version_is_the_latest_one(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = mock.MagicMock( + version="2.16", min_version="2.4") + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + + self.assertEqual( + "2.11", + api_versions.discover_version( + fake_client, + api_versions.APIVersion('2.latest')).get_string()) + + def test_server_without_microversion(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = mock.MagicMock( + version='', min_version='') + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + + self.assertEqual( + "2.0", + api_versions.discover_version( + fake_client, + api_versions.APIVersion('2.latest')).get_string()) + + def test_server_without_microversion_and_no_version_field(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = versions.Version( + None, {}) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + + self.assertEqual( + "2.0", + api_versions.discover_version( + fake_client, + api_versions.APIVersion('2.latest')).get_string()) + + def test_server_without_microversion_rax_workaround(self): + fake_client = mock.MagicMock() + fake_client.versions.get_current.return_value = None + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.11") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + + self.assertEqual( + "2.0", + api_versions.discover_version( + fake_client, + api_versions.APIVersion('2.latest')).get_string()) + + +class DecoratedAfterTestCase(utils.TestCase): + def test_decorated_after(self): + + class Fake(object): + api_version = api_versions.APIVersion('2.123') + + @api_versions.deprecated_after('2.123') + def foo(self): + pass + + with mock.patch('warnings.warn') as mock_warn: + Fake().foo() + msg = ('The novaclient.tests.unit.test_api_versions module ' + 'is deprecated and will be removed.') + mock_warn.assert_called_once_with(msg, mock.ANY) diff --git a/novaclient/tests/unit/test_base.py b/novaclient/tests/unit/test_base.py new file mode 100644 index 000000000..634cc93b4 --- /dev/null +++ b/novaclient/tests/unit/test_base.py @@ -0,0 +1,149 @@ +# +# 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 requests + +from novaclient import api_versions +from novaclient import base +from novaclient import exceptions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import flavors + + +def create_response_obj_with_header(): + resp = requests.Response() + resp.headers['x-openstack-request-id'] = fakes.FAKE_REQUEST_ID + return resp + + +def create_response_obj_with_compute_header(): + resp = requests.Response() + resp.headers['x-compute-request-id'] = fakes.FAKE_REQUEST_ID + return resp + + +class BaseTest(utils.TestCase): + def test_resource_repr(self): + r = base.Resource(None, dict(foo="bar", baz="spam")) + self.assertEqual("", repr(r)) + + def test_getid(self): + self.assertEqual(4, base.getid(4)) + + class TmpObject(object): + id = 4 + self.assertEqual(4, base.getid(TmpObject)) + + def test_resource_lazy_getattr(self): + cs = fakes.FakeClient(api_versions.APIVersion("2.0")) + f = flavors.Flavor(cs.flavors, {'id': 1}) + self.assertEqual('256 MiB Server', f.name) + cs.assert_called('GET', '/flavors/1') + + # Missing stuff still fails after a second get + self.assertRaises(AttributeError, getattr, f, 'blahblah') + + def test_eq(self): + # Two resources of the same type with the same id: equal + r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) + r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) + self.assertEqual(r1, r2) + + # Two resources of different types: never equal + r1 = base.Resource(None, {'id': 1}) + r2 = flavors.Flavor(None, {'id': 1}) + self.assertNotEqual(r1, r2) + + # Two resources with no ID: equal if their info is equal + r1 = base.Resource(None, {'name': 'joe', 'age': 12}) + r2 = base.Resource(None, {'name': 'joe', 'age': 12}) + self.assertEqual(r1, r2) + + def test_ne(self): + # Two resources of different types: never equal + r1 = base.Resource(None, {'id': 1, 'name': 'test'}) + r2 = object() + self.assertNotEqual(r1, r2) + + def test_findall_invalid_attribute(self): + cs = fakes.FakeClient(api_versions.APIVersion("2.0")) + # Make sure findall with an invalid attribute doesn't cause errors. + # The following should not raise an exception. + cs.flavors.findall(vegetable='carrot') + + # However, find() should raise an error + self.assertRaises(exceptions.NotFound, + cs.flavors.find, + vegetable='carrot') + + def test_resource_object_with_request_ids(self): + resp_obj = create_response_obj_with_header() + r = base.Resource(None, {"name": "1"}, resp=resp_obj) + self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, r.request_ids) + + def test_resource_object_with_compute_request_ids(self): + resp_obj = create_response_obj_with_compute_header() + r = base.Resource(None, {"name": "1"}, resp=resp_obj) + self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, r.request_ids) + + +class ListWithMetaTest(utils.TestCase): + def test_list_with_meta(self): + resp = create_response_obj_with_header() + obj = base.ListWithMeta([], resp) + self.assertEqual([], 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) + + +class DictWithMetaTest(utils.TestCase): + def test_dict_with_meta(self): + resp = create_response_obj_with_header() + obj = base.DictWithMeta({}, resp) + self.assertEqual({}, 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) + + +class TupleWithMetaTest(utils.TestCase): + def test_tuple_with_meta(self): + resp = create_response_obj_with_header() + expected_tuple = (1, 2) + obj = base.TupleWithMeta(expected_tuple, resp) + self.assertEqual(expected_tuple, 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) + + +class StrWithMetaTest(utils.TestCase): + def test_str_with_meta(self): + resp = create_response_obj_with_header() + obj = base.StrWithMeta("test-str", resp) + self.assertEqual("test-str", 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) + + +class BytesWithMetaTest(utils.TestCase): + def test_bytes_with_meta(self): + resp = create_response_obj_with_header() + obj = base.BytesWithMeta(b'test-bytes', resp) + self.assertEqual(b'test-bytes', 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 new file mode 100644 index 000000000..f0154ab78 --- /dev/null +++ b/novaclient/tests/unit/test_client.py @@ -0,0 +1,138 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +from unittest import mock + +from keystoneauth1 import session +from oslo_utils import uuidutils + +import novaclient.api_versions +import novaclient.client +import novaclient.extension +from novaclient.tests.unit import utils +import novaclient.v2.client + + +class SessionClientTest(utils.TestCase): + + def test_timings(self): + self.requests_mock.get('http://no.where') + + client = novaclient.client.SessionClient(session=session.Session()) + client.request("http://no.where", 'GET') + self.assertEqual(0, len(client.times)) + + client = novaclient.client.SessionClient(session=session.Session(), + timings=True) + client.request("http://no.where", 'GET') + self.assertEqual(1, len(client.times)) + self.assertEqual('GET http://no.where', client.times[0][0]) + + def test_client_get_reset_timings_v2(self): + cs = novaclient.client.SessionClient(session=session.Session()) + self.assertEqual(0, len(cs.get_timings())) + cs.times.append("somevalue") + self.assertEqual(1, len(cs.get_timings())) + self.assertEqual("somevalue", cs.get_timings()[0]) + + cs.reset_timings() + self.assertEqual(0, len(cs.get_timings())) + + def test_global_id(self): + global_id = "req-%s" % uuidutils.generate_uuid() + self.requests_mock.get('http://no.where') + + client = novaclient.client.SessionClient(session=session.Session(), + global_request_id=global_id) + client.request("http://no.where", 'GET') + headers = self.requests_mock.last_request.headers + self.assertEqual(headers['X-OpenStack-Request-ID'], global_id) + + +class ClientsUtilsTest(utils.TestCase): + + @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_all(self, mock_extension, + mock_discover_via_python_path, + mock_discover_via_entry_points): + def make_gen(start, end): + def f(*args, **kwargs): + for i in range(start, end): + yield "name-%s" % i, i + return f + + mock_discover_via_python_path.side_effect = make_gen(0, 3) + mock_discover_via_entry_points.side_effect = make_gen(3, 4) + + version = novaclient.api_versions.APIVersion("2.0") + + result = novaclient.client.discover_extensions(version) + + self.assertEqual([mock.call("name-%s" % i, i) for i in range(0, 4)], + mock_extension.call_args_list) + mock_discover_via_python_path.assert_called_once_with() + mock_discover_via_entry_points.assert_called_once_with() + self.assertEqual([mock_extension()] * 4, result) + + @mock.patch("novaclient.client.warnings") + def test__check_arguments(self, mock_warnings): + release = "Coolest" + + # no reference + novaclient.client._check_arguments({}, release=release, + deprecated_name="foo") + self.assertFalse(mock_warnings.warn.called) + novaclient.client._check_arguments({}, release=release, + deprecated_name="foo", + right_name="bar") + self.assertFalse(mock_warnings.warn.called) + + # with alternative + original_kwargs = {"foo": "text"} + actual_kwargs = copy.copy(original_kwargs) + self.assertEqual(original_kwargs, actual_kwargs) + novaclient.client._check_arguments(actual_kwargs, release=release, + deprecated_name="foo", + right_name="bar") + self.assertNotEqual(original_kwargs, actual_kwargs) + self.assertEqual({"bar": original_kwargs["foo"]}, actual_kwargs) + self.assertTrue(mock_warnings.warn.called) + + mock_warnings.warn.reset_mock() + + # without alternative + original_kwargs = {"foo": "text"} + actual_kwargs = copy.copy(original_kwargs) + self.assertEqual(original_kwargs, actual_kwargs) + novaclient.client._check_arguments(actual_kwargs, release=release, + deprecated_name="foo") + 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 new file mode 100644 index 000000000..f294e2e81 --- /dev/null +++ b/novaclient/tests/unit/test_discover.py @@ -0,0 +1,82 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import importlib +import inspect +from unittest import mock + +import stevedore +from stevedore import extension + +from novaclient import client +from novaclient.tests.unit import utils + + +class DiscoverTest(utils.TestCase): + + def test_discover_via_entry_points(self): + + 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(client, '_make_discovery_manager', mock_mgr) + def test(): + for name, module in client._discover_via_entry_points(): + self.assertEqual('foo', name) + self.assertTrue(inspect.ismodule(module)) + + test() + + def test_discover_extensions(self): + + def mock_discover_via_python_path(): + module_spec = importlib.machinery.ModuleSpec('foo', None) + module = importlib.util.module_from_spec(module_spec) + yield 'foo', module + + def mock_discover_via_entry_points(): + 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', + mock_discover_via_python_path) + @mock.patch.object(client, + '_discover_via_entry_points', + mock_discover_via_entry_points) + def test(): + extensions = client.discover_extensions('1.1') + self.assertEqual(2, len(extensions)) + names = sorted(['foo', 'baz']) + sorted_extensions = sorted(extensions, key=lambda ext: ext.name) + for i in range(len(names)): + ext = sorted_extensions[i] + name = names[i] + self.assertEqual(ext.name, name) + self.assertTrue(inspect.ismodule(ext.module)) + + test() diff --git a/novaclient/tests/unit/test_exceptions.py b/novaclient/tests/unit/test_exceptions.py new file mode 100644 index 000000000..34ce11d16 --- /dev/null +++ b/novaclient/tests/unit/test_exceptions.py @@ -0,0 +1,48 @@ +# Copyright 2016 IBM Corp. +# +# 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 exceptions +from novaclient.tests.unit import utils as test_utils + + +class ExceptionsTestCase(test_utils.TestCase): + + def _test_from_response(self, body, expected_message): + data = { + 'status_code': 404, + 'headers': { + 'content-type': 'application/json', + 'x-openstack-request-id': ( + 'req-d9df03b0-4150-4b53-8157-7560ccf39f75'), + } + } + response = test_utils.TestResponse(data) + fake_url = 'http://localhost:8774/v2.1/fake/flavors/test' + error = exceptions.from_response(response, body, fake_url, 'GET') + self.assertIsInstance(error, exceptions.NotFound) + self.assertEqual(expected_message, error.message) + + def test_from_response_webob_pre_1_6_0(self): + # Tests error responses before webob 1.6.0 where the error details + # are nested in the response body. + message = "Flavor test could not be found." + self._test_from_response( + {"itemNotFound": {"message": message, "code": 404}}, + message) + + def test_from_response_webob_post_1_6_0(self): + # Tests error responses from webob 1.6.0 where the error details + # are in the response body. + message = "Flavor test could not be found." + self._test_from_response({"message": message, "code": 404}, message) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py new file mode 100644 index 000000000..efad2ac9f --- /dev/null +++ b/novaclient/tests/unit/test_shell.py @@ -0,0 +1,938 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import io +import re +import sys +from unittest import mock + +import fixtures +from keystoneauth1 import fixture +import requests_mock +from testtools import matchers + +from novaclient import api_versions +import novaclient.client +from novaclient import exceptions +import novaclient.shell +from novaclient.tests.unit import fake_actions_module +from novaclient.tests.unit import utils + +FAKE_ENV = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_NAME': 'tenant_name', + '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/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/v3', + 'NOVA_ENDPOINT_TYPE': 'novaURL', + 'OS_ENDPOINT_TYPE': 'osURL', + 'OS_COMPUTE_API_VERSION': '2'} + +FAKE_ENV4 = {'OS_USER_ID': 'user_id', + 'OS_PASSWORD': 'password', + 'OS_TENANT_ID': 'tenant_id', + 'OS_AUTH_URL': 'http://no.where/v3', + 'NOVA_ENDPOINT_TYPE': 'internal', + 'OS_ENDPOINT_TYPE': 'osURL', + 'OS_COMPUTE_API_VERSION': '2'} + +FAKE_ENV5 = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_NAME': 'tenant_name', + 'OS_AUTH_URL': 'http://no.where/v3'} + + +def _create_ver_list(versions): + return {'versions': {'values': versions}} + + +class DeprecatedActionTest(utils.TestCase): + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_emptyhelp_nouse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertIsNone(result.use) + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', 'Deprecated', + {'a': 1, 'b': 2, 'c': 3})) + self.assertIsNone(result.real_action) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) + + @mock.patch.object(novaclient.shell.argparse.Action, '__init__', + return_value=None) + def test_init_emptyhelp_withuse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', use='use this instead', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, 'use this instead') + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', + 'Deprecated; use this instead', + {'a': 1, 'b': 2, 'c': 3})) + self.assertIsNone(result.real_action) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated; use this instead', + a=1, b=2, c=3) + + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_withhelp_nouse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', help='some help', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertIsNone(result.use) + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', + 'some help (Deprecated)', + {'a': 1, 'b': 2, 'c': 3})) + self.assertIsNone(result.real_action) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='some help (Deprecated)', + a=1, b=2, c=3) + + @mock.patch.object(novaclient.shell.argparse.Action, '__init__', + return_value=None) + def test_init_withhelp_withuse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', help='some help', + use='use this instead', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, 'use this instead') + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', + 'some help (Deprecated; use this instead)', + {'a': 1, 'b': 2, 'c': 3})) + self.assertIsNone(result.real_action) + mock_init.assert_called_once_with( + 'option_strings', 'dest', + help='some help (Deprecated; use this instead)', + a=1, b=2, c=3) + + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_suppresshelp_nouse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', help=argparse.SUPPRESS, a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertIsNone(result.use) + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', argparse.SUPPRESS, + {'a': 1, 'b': 2, 'c': 3})) + self.assertIsNone(result.real_action) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help=argparse.SUPPRESS, a=1, b=2, c=3) + + @mock.patch.object(novaclient.shell.argparse.Action, '__init__', + return_value=None) + def test_init_suppresshelp_withuse(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', help=argparse.SUPPRESS, + use='use this instead', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertEqual(result.use, 'use this instead') + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', argparse.SUPPRESS, + {'a': 1, 'b': 2, 'c': 3})) + self.assertIsNone(result.real_action) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help=argparse.SUPPRESS, a=1, b=2, c=3) + + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_action_nothing(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action='nothing', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertIsNone(result.use) + self.assertIs(result.real_action_args, False) + self.assertIsNone(result.real_action) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) + + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_action_string(self, mock_init): + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action='store', a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertIsNone(result.use) + self.assertEqual(result.real_action_args, + ('option_strings', 'dest', 'Deprecated', + {'a': 1, 'b': 2, 'c': 3})) + self.assertEqual(result.real_action, 'store') + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) + + @mock.patch.object(argparse.Action, '__init__', return_value=None) + def test_init_action_other(self, mock_init): + action = mock.Mock() + result = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action=action, a=1, b=2, c=3) + + self.assertEqual(result.emitted, set()) + self.assertIsNone(result.use) + self.assertIs(result.real_action_args, False) + self.assertEqual(result.real_action, action.return_value) + mock_init.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) + action.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', a=1, b=2, c=3) + + @mock.patch.object(sys, 'stderr', io.StringIO()) + def test_get_action_nolookup(self): + action_class = mock.Mock() + parser = mock.Mock(**{ + '_registry_get.return_value': action_class, + }) + obj = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action='nothing', const=1) + obj.real_action = 'action' + + result = obj._get_action(parser) + + self.assertEqual(result, 'action') + self.assertEqual(obj.real_action, 'action') + self.assertFalse(parser._registry_get.called) + self.assertFalse(action_class.called) + self.assertEqual(sys.stderr.getvalue(), '') + + @mock.patch.object(sys, 'stderr', io.StringIO()) + def test_get_action_lookup_noresult(self): + parser = mock.Mock(**{ + '_registry_get.return_value': None, + }) + obj = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action='store', const=1) + + result = obj._get_action(parser) + + self.assertIsNone(result) + self.assertIsNone(obj.real_action) + parser._registry_get.assert_called_once_with( + 'action', 'store') + self.assertEqual(sys.stderr.getvalue(), + 'WARNING: Programming error: Unknown real action ' + '"store"\n') + + @mock.patch.object(sys, 'stderr', io.StringIO()) + def test_get_action_lookup_withresult(self): + action_class = mock.Mock() + parser = mock.Mock(**{ + '_registry_get.return_value': action_class, + }) + obj = novaclient.shell.DeprecatedAction( + 'option_strings', 'dest', real_action='store', const=1) + + result = obj._get_action(parser) + + self.assertEqual(result, action_class.return_value) + self.assertEqual(obj.real_action, action_class.return_value) + parser._registry_get.assert_called_once_with( + 'action', 'store') + action_class.assert_called_once_with( + 'option_strings', 'dest', help='Deprecated', const=1) + self.assertEqual(sys.stderr.getvalue(), '') + + @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( + 'option_strings', 'dest') + + obj('parser', 'namespace', 'values', 'option_string') + + self.assertEqual(obj.emitted, set(['option_string'])) + mock_get_action.assert_called_once_with('parser') + mock_get_action.return_value.assert_called_once_with( + 'parser', 'namespace', 'values', 'option_string') + self.assertEqual(sys.stderr.getvalue(), + 'WARNING: Option "option_string" is deprecated\n') + + @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( + 'option_strings', 'dest', use='use this instead') + + obj('parser', 'namespace', 'values', 'option_string') + + self.assertEqual(obj.emitted, set(['option_string'])) + mock_get_action.assert_called_once_with('parser') + mock_get_action.return_value.assert_called_once_with( + 'parser', 'namespace', 'values', 'option_string') + self.assertEqual(sys.stderr.getvalue(), + 'WARNING: Option "option_string" is deprecated; ' + 'use this instead\n') + + @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( + 'option_strings', 'dest') + obj.emitted.add('option_string') + + obj('parser', 'namespace', 'values', 'option_string') + + self.assertEqual(obj.emitted, set(['option_string'])) + mock_get_action.assert_called_once_with('parser') + mock_get_action.return_value.assert_called_once_with( + 'parser', 'namespace', 'values', 'option_string') + self.assertEqual(sys.stderr.getvalue(), '') + + @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( + 'option_strings', 'dest', use='use this instead') + obj.emitted.add('option_string') + + obj('parser', 'namespace', 'values', 'option_string') + + self.assertEqual(obj.emitted, set(['option_string'])) + mock_get_action.assert_called_once_with('parser') + mock_get_action.return_value.assert_called_once_with( + 'parser', 'namespace', 'values', 'option_string') + self.assertEqual(sys.stderr.getvalue(), '') + + +class ParserTest(utils.TestCase): + + def setUp(self): + super(ParserTest, self).setUp() + self.parser = novaclient.shell.NovaClientArgumentParser() + + def test_ambiguous_option(self): + self.parser.add_argument('--tic') + self.parser.add_argument('--tac') + + try: + self.parser.parse_args(['--t']) + except SystemExit as err: + self.assertEqual(2, err.code) + else: + self.fail('SystemExit not raised') + + def test_not_really_ambiguous_option(self): + # current/deprecated forms of the same option + self.parser.add_argument('--tic-tac', action="store_true") + self.parser.add_argument('--tic_tac', action="store_true") + args = self.parser.parse_args(['--tic']) + self.assertTrue(args.tic_tac) + + +class ShellTest(utils.TestCase): + + _msg_no_tenant_project = ("You must provide a project name or project" + " ID via --os-project-name, --os-project-id," + " env[OS_PROJECT_ID] or env[OS_PROJECT_NAME]." + " You may use os-project and os-tenant" + " interchangeably.") + + def make_env(self, exclude=None, fake_env=FAKE_ENV): + env = dict((k, v) for k, v in fake_env.items() if k != exclude) + self.useFixture(fixtures.MonkeyPatch('os.environ', env)) + + def setUp(self): + super(ShellTest, self).setUp() + self.mock_client = mock.MagicMock() + self.mock_client.return_value.api_version = novaclient.API_MIN_VERSION + self.useFixture(fixtures.MonkeyPatch('novaclient.client.Client', + self.mock_client)) + self.nc_util = mock.patch('novaclient.utils.isunauthenticated').start() + self.nc_util.return_value = False + self.mock_server_version_range = mock.patch( + 'novaclient.api_versions._get_server_version_range').start() + self.mock_server_version_range.return_value = ( + novaclient.API_MIN_VERSION, + novaclient.API_MIN_VERSION) + self.orig_max_ver = novaclient.API_MAX_VERSION + self.orig_min_ver = novaclient.API_MIN_VERSION + self.addCleanup(self._clear_fake_version) + self.addCleanup(mock.patch.stopall) + + def _clear_fake_version(self): + novaclient.API_MAX_VERSION = self.orig_max_ver + novaclient.API_MIN_VERSION = self.orig_min_ver + + def shell(self, argstr, exitcodes=(0,)): + orig = sys.stdout + orig_stderr = sys.stderr + try: + sys.stdout = io.StringIO() + sys.stderr = io.StringIO() + _shell = novaclient.shell.OpenStackComputeShell() + _shell.main(argstr.split()) + except SystemExit: + exc_type, exc_value, exc_traceback = sys.exc_info() + self.assertIn(exc_value.code, exitcodes) + finally: + stdout = sys.stdout.getvalue() + sys.stdout.close() + sys.stdout = orig + stderr = sys.stderr.getvalue() + sys.stderr.close() + sys.stderr = orig_stderr + return (stdout, stderr) + + def register_keystone_discovery_fixture(self, mreq): + v3_url = "http://no.where/v3" + v3_version = fixture.V3Discovery(v3_url) + mreq.register_uri( + 'GET', v3_url, json=_create_ver_list([v3_version]), + status_code=200) + + def test_help_unknown_command(self): + self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo') + + def test_invalid_timeout(self): + for f in [0, -1, -10]: + cmd_text = '--timeout %s' % (f) + stdout, stderr = self.shell(cmd_text, exitcodes=[0, 2]) + required = [ + 'argument --timeout: %s must be greater than 0' % (f), + ] + for r in required: + self.assertIn(r, stderr) + + def _test_help(self, command, required=None): + if required is None: + required = [ + '.*?^usage: ', + '.*?^\\s+set-password\\s+Change the admin password', + '.*?^See "nova help COMMAND" for help on a specific command', + ] + stdout, stderr = self.shell(command) + for r in required: + self.assertThat((stdout + stderr), + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + + def test_help(self): + self._test_help('help') + + def test_help_option(self): + self._test_help('--help') + self._test_help('-h') + + 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', + '.*?^Change the admin password', + '.*?^Positional arguments:', + ] + self._test_help('help set-password', required=required) + + def test_bash_completion(self): + stdout, stderr = self.shell('bash-completion') + # just check we have some output + required = [ + '.*--matching', + '.*--wrap', + '.*help', + '.*server-group-delete', + '.*--image-with'] + for r in required: + self.assertThat((stdout + stderr), + matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) + + def test_no_username(self): + 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') + except exceptions.CommandError as message: + self.assertEqual(required, message.args[0]) + else: + self.fail('CommandError not raised') + + def test_no_user_id(self): + 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') + except exceptions.CommandError as message: + self.assertEqual(required, message.args[0]) + else: + self.fail('CommandError not raised') + + def test_no_tenant_name(self): + required = self._msg_no_tenant_project + self.make_env(exclude='OS_TENANT_NAME') + try: + self.shell('list') + except exceptions.CommandError as message: + self.assertEqual(required, message.args[0]) + else: + self.fail('CommandError not raised') + + def test_no_tenant_id(self): + required = self._msg_no_tenant_project + self.make_env(exclude='OS_TENANT_ID', fake_env=FAKE_ENV2) + try: + self.shell('list') + except exceptions.CommandError as message: + self.assertEqual(required, message.args[0]) + else: + self.fail('CommandError not raised') + + def test_no_auth_url(self): + required = ('You must provide an auth url' + ' via either --os-auth-url or env[OS_AUTH_URL].',) + self.make_env(exclude='OS_AUTH_URL') + try: + self.shell('list') + except exceptions.CommandError as message: + self.assertEqual(required, message.args) + 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) + self.register_keystone_discovery_fixture(m_requests) + self.shell('list') + client_kwargs = self.mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'novaURL') + + @requests_mock.Mocker() + def test_endpoint_type_like_other_clients(self, m_requests): + self.make_env(fake_env=FAKE_ENV4) + self.register_keystone_discovery_fixture(m_requests) + self.shell('list') + client_kwargs = self.mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'internalURL') + + @requests_mock.Mocker() + def test_os_endpoint_type(self, m_requests): + self.make_env(exclude='NOVA_ENDPOINT_TYPE', fake_env=FAKE_ENV3) + self.register_keystone_discovery_fixture(m_requests) + self.shell('list') + client_kwargs = self.mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'osURL') + + def test_default_endpoint_type(self): + self.make_env() + self.shell('list') + client_kwargs = self.mock_client.call_args_list[0][1] + self.assertEqual(client_kwargs['endpoint_type'], 'publicURL') + + @mock.patch('sys.stdin', side_effect=mock.MagicMock) + @mock.patch('getpass.getpass', return_value='password') + @requests_mock.Mocker() + def test_password(self, mock_getpass, mock_stdin, m_requests): + mock_stdin.encoding = "utf-8" + + 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) + + def _test_service_type(self, version, service_type, mock_client): + if version is None: + cmd = 'list' + else: + cmd = ('--service-type %s --os-compute-api-version %s list' % + (service_type, version)) + self.make_env() + self.shell(cmd) + _client_args, client_kwargs = mock_client.call_args_list[0] + self.assertEqual(service_type, client_kwargs['service_type']) + + def test_default_service_type(self): + self._test_service_type(None, 'compute', self.mock_client) + + def test_v2_service_type(self): + self._test_service_type('2', 'compute', self.mock_client) + + def test_v_unknown_service_type(self): + self.assertRaises(exceptions.UnsupportedVersion, + self._test_service_type, + 'unknown', 'compute', self.mock_client) + + @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([]) + 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(), + ) + # 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([]) + except SystemExit as ex: + self.assertEqual(ex.code, 130) + + @mock.patch.object(novaclient.shell.OpenStackComputeShell, 'times') + @requests_mock.Mocker() + def test_timing(self, m_times, m_requests): + m_times.append.side_effect = RuntimeError('Boom!') + self.make_env() + self.register_keystone_discovery_fixture(m_requests) + self.shell('list') + exc = self.assertRaises(RuntimeError, self.shell, '--timings list') + self.assertEqual('Boom!', str(exc)) + + @requests_mock.Mocker() + def test_osprofiler(self, m_requests): + self.make_env() + + def client(*args, **kwargs): + self.assertEqual('swordfish', kwargs['profile']) + with mock.patch('novaclient.client.Client', client): + # we are only interested in the fact Client is initialized properly + self.shell('list --profile swordfish', (0, 2)) + + @requests_mock.Mocker() + def test_osprofiler_not_installed(self, m_requests): + self.make_env() + + # NOTE(rpodolyaka): osprofiler is in test-requirements, so we have to + # simulate its absence here + with mock.patch('novaclient.shell.osprofiler_profiler', None): + _, stderr = self.shell('list --profile swordfish', (0, 2)) + self.assertIn('unrecognized arguments: --profile swordfish', + stderr) + + def test_microversion_with_default_behaviour(self): + self.make_env(fake_env=FAKE_ENV5) + self.mock_server_version_range.return_value = ( + api_versions.APIVersion("2.1"), api_versions.APIVersion("2.3")) + self.shell('list') + client_args = self.mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.3"), client_args[0]) + + def test_microversion_with_default_behaviour_with_legacy_server(self): + self.make_env(fake_env=FAKE_ENV5) + self.mock_server_version_range.return_value = ( + api_versions.APIVersion(), api_versions.APIVersion()) + self.shell('list') + client_args = self.mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.0"), client_args[0]) + + def test_microversion_with_latest(self): + self.make_env() + novaclient.API_MAX_VERSION = api_versions.APIVersion('2.3') + self.mock_server_version_range.return_value = ( + api_versions.APIVersion("2.1"), api_versions.APIVersion("2.3")) + self.shell('--os-compute-api-version 2.latest list') + client_args = self.mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.3"), client_args[0]) + + def test_microversion_with_specified_version(self): + self.make_env() + self.mock_server_version_range.return_value = ( + api_versions.APIVersion("2.10"), api_versions.APIVersion("2.100")) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.90") + self.shell('--os-compute-api-version 2.99 list') + client_args = self.mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.99"), client_args[0]) + + def test_microversion_with_specified_version_out_of_range(self): + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.90") + self.assertRaises(exceptions.CommandError, + self.shell, '--os-compute-api-version 2.199 list') + + def test_microversion_with_v2_and_v2_1_server(self): + self.make_env() + self.mock_server_version_range.return_value = ( + api_versions.APIVersion('2.1'), api_versions.APIVersion('2.3')) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + self.shell('--os-compute-api-version 2 list') + client_args = self.mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.0"), client_args[0]) + + def test_microversion_with_v2_and_v2_server(self): + self.make_env() + self.mock_server_version_range.return_value = ( + api_versions.APIVersion(), api_versions.APIVersion()) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + self.shell('--os-compute-api-version 2 list') + client_args = self.mock_client.call_args_list[1][0] + self.assertEqual(api_versions.APIVersion("2.0"), client_args[0]) + + def test_microversion_with_v2_without_server_compatible(self): + self.make_env() + self.mock_server_version_range.return_value = ( + api_versions.APIVersion('2.2'), api_versions.APIVersion('2.3')) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + self.assertRaises( + exceptions.UnsupportedVersion, + self.shell, '--os-compute-api-version 2 list') + + def test_microversion_with_specific_version_without_microversions(self): + self.make_env() + self.mock_server_version_range.return_value = ( + api_versions.APIVersion(), api_versions.APIVersion()) + novaclient.API_MAX_VERSION = api_versions.APIVersion("2.100") + novaclient.API_MIN_VERSION = api_versions.APIVersion("2.1") + self.assertRaises( + exceptions.UnsupportedVersion, + self.shell, + '--os-compute-api-version 2.3 list') + + @mock.patch.object(novaclient.shell.OpenStackComputeShell, 'main') + def test_main_error_handling(self, mock_compute_shell): + class MyException(Exception): + pass + with mock.patch('sys.stderr', io.StringIO()): + mock_compute_shell.side_effect = MyException('message') + self.assertRaises(SystemExit, novaclient.shell.main, []) + err = sys.stderr.getvalue() + # 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() + shell.subcommands = {} + shell._find_actions(subparsers, fake_actions_module, + api_versions.APIVersion("2.15"), False) + self.assertIn('fake-action', shell.subcommands.keys()) + 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) + self.assertIn('fake-action', shell.subcommands.keys()) + self.assertEqual( + 2, shell.subcommands['fake-action'].get_default('func')()) + + def test_load_versioned_actions_not_in_version_range(self): + 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.10000"), False) + self.assertNotIn('fake-action', shell.subcommands.keys()) + self.assertIn('fake-action2', shell.subcommands.keys()) + + def test_load_versioned_actions_with_help(self): + 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.15"), True) + self.assertIn('fake-action', shell.subcommands.keys()) + expected_desc = (" (Supported by API versions '%(start)s' - " + "'%(end)s')") % {'start': '2.10', 'end': '2.30'} + self.assertEqual(expected_desc, + shell.subcommands['fake-action'].description) + + def test_load_versioned_actions_with_help_on_latest(self): + 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.latest"), True) + self.assertIn('another-fake-action', shell.subcommands.keys()) + expected_desc = (" (Supported by API versions '%(start)s' - " + "'%(end)s')%(hint)s") % { + 'start': '2.0', 'end': '2.latest', + 'hint': novaclient.shell.HINT_HELP_MSG} + self.assertEqual(expected_desc, + shell.subcommands['another-fake-action'].description) + + @mock.patch.object(novaclient.shell.NovaClientArgumentParser, + 'add_argument') + def test_load_versioned_actions_with_args(self, mock_add_arg): + 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.1"), False) + self.assertIn('fake-action2', shell.subcommands.keys()) + mock_add_arg.assert_has_calls([ + mock.call('-h', '--help', action='help', help='==SUPPRESS=='), + mock.call('--foo')]) + + @mock.patch.object(novaclient.shell.NovaClientArgumentParser, + 'add_argument') + def test_load_versioned_actions_with_args2(self, mock_add_arg): + 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.4"), False) + self.assertIn('fake-action2', shell.subcommands.keys()) + mock_add_arg.assert_has_calls([ + mock.call('-h', '--help', action='help', help='==SUPPRESS=='), + mock.call('--bar')]) + + @mock.patch.object(novaclient.shell.NovaClientArgumentParser, + 'add_argument') + def test_load_versioned_actions_with_args_not_in_version_range( + self, mock_add_arg): + 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.10000"), False) + self.assertIn('fake-action2', shell.subcommands.keys()) + mock_add_arg.assert_has_calls([ + mock.call('-h', '--help', action='help', help='==SUPPRESS==')]) + + @mock.patch.object(novaclient.shell.NovaClientArgumentParser, + 'add_argument') + def test_load_versioned_actions_with_args_and_help(self, mock_add_arg): + 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.4"), True) + mock_add_arg.assert_has_calls([ + mock.call('-h', '--help', action='help', help='==SUPPRESS=='), + mock.call('--bar', + help=" (Supported by API versions '2.3' - '2.4')")]) + + @mock.patch.object(novaclient.shell.NovaClientArgumentParser, + 'add_argument') + def test_load_actions_with_versioned_args(self, mock_add_arg): + 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.20"), False) + self.assertIn(mock.call('--foo', help="first foo"), + mock_add_arg.call_args_list) + self.assertNotIn(mock.call('--foo', help="second foo"), + mock_add_arg.call_args_list) + + 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"), + mock_add_arg.call_args_list) + self.assertIn(mock.call('--foo', help="second foo"), + mock_add_arg.call_args_list) + + +class ShellTestKeystoneV3(ShellTest): + def make_env(self, exclude=None, fake_env=FAKE_ENV): + if 'OS_AUTH_URL' in fake_env: + fake_env.update({'OS_AUTH_URL': 'http://no.where/v3'}) + env = dict((k, v) for k, v in fake_env.items() if k != exclude) + self.useFixture(fixtures.MonkeyPatch('os.environ', env)) + + def register_keystone_discovery_fixture(self, mreq): + v3_url = "http://no.where/v3" + v3_version = fixture.V3Discovery(v3_url) + mreq.register_uri( + 'GET', v3_url, json=_create_ver_list([v3_version]), + status_code=200) diff --git a/novaclient/tests/unit/test_utils.py b/novaclient/tests/unit/test_utils.py new file mode 100644 index 000000000..8411f3a40 --- /dev/null +++ b/novaclient/tests/unit/test_utils.py @@ -0,0 +1,469 @@ +# +# 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 io +import sys +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' + + +class FakeResource(object): + NAME_ATTR = 'name' + + request_ids = fakes.FAKE_REQUEST_ID_LIST + + def __init__(self, _id, properties): + self.id = _id + try: + self.name = properties['name'] + except KeyError: + pass + + def append_request_ids(self, resp): + pass + + +class FakeManager(base.ManagerWithFind): + + resource_class = FakeResource + + resources = [ + FakeResource('1234', {'name': 'entity_one'}), + FakeResource('12345', {'name': 'UPPER'}), + FakeResource('123456', {'name': 'lower'}), + FakeResource('1234567', {'name': 'Mixed'}), + FakeResource('12345678', {'name': 'mixed'}), + FakeResource(UUID, {'name': 'entity_two'}), + FakeResource('5678', {'name': '9876'}), + FakeResource('01234', {'name': 'entity_three'}) + ] + + is_alphanum_id_allowed = None + + def __init__(self, alphanum_id_allowed=False): + self.is_alphanum_id_allowed = alphanum_id_allowed + + def get(self, resource_id): + for resource in self.resources: + if resource.id == str(resource_id): + return resource + raise exceptions.NotFound(resource_id) + + def list(self): + return base.ListWithMeta(self.resources, fakes.FAKE_REQUEST_ID_LIST) + + +class FakeDisplayResource(object): + NAME_ATTR = 'display_name' + + def __init__(self, _id, properties): + self.id = _id + try: + self.display_name = properties['display_name'] + except KeyError: + pass + + def append_request_ids(self, resp): + pass + + +class FakeDisplayManager(FakeManager): + + resource_class = FakeDisplayResource + + resources = [ + FakeDisplayResource('4242', {'display_name': 'entity_three'}), + ] + + +class FindResourceTestCase(test_utils.TestCase): + + def setUp(self): + super(FindResourceTestCase, self).setUp() + self.manager = FakeManager(None) + + def test_find_none(self): + """Test a few non-valid inputs.""" + self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + 'asdf') + self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + None) + self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + {}) + + def test_find_by_integer_id(self): + output = utils.find_resource(self.manager, 1234) + self.assertEqual(output, self.manager.get('1234')) + + def test_find_by_str_id(self): + output = utils.find_resource(self.manager, '1234') + self.assertEqual(output, self.manager.get('1234')) + + def test_find_by_uuid(self): + output = utils.find_resource(self.manager, UUID) + self.assertEqual(output, self.manager.get(UUID)) + + def test_find_by_str_name(self): + output = utils.find_resource(self.manager, 'entity_one') + self.assertEqual(output, self.manager.get('1234')) + + def test_find_by_str_upper_name(self): + output = utils.find_resource(self.manager, 'UPPER') + self.assertEqual(output, self.manager.get('12345')) + + def test_find_by_str_lower_name(self): + output = utils.find_resource(self.manager, 'lower') + self.assertEqual(output, self.manager.get('123456')) + + def test_find_by_str_mix_name(self): + output = utils.find_resource(self.manager, 'Mixed') + self.assertEqual(output, self.manager.get('1234567')) + + def test_find_by_str_lower_name_mixed(self): + output = utils.find_resource(self.manager, 'mixed') + self.assertEqual(output, self.manager.get('12345678')) + + def test_find_by_str_display_name(self): + display_manager = FakeDisplayManager(None) + output = utils.find_resource(display_manager, 'entity_three') + self.assertEqual(output, display_manager.get('4242')) + + def test_find_in_alphanum_allowed_manager_by_str_id_(self): + alphanum_manager = FakeManager(True) + output = utils.find_resource(alphanum_manager, '01234') + self.assertEqual(output, alphanum_manager.get('01234')) + + def test_find_without_wrapping_exception(self): + alphanum_manager = FakeManager(True) + self.assertRaises(exceptions.NotFound, utils.find_resource, + alphanum_manager, 'not_exist', wrap_exception=False) + res = alphanum_manager.resources[0] + alphanum_manager.resources.append(res) + self.assertRaises(exceptions.NoUniqueMatch, utils.find_resource, + alphanum_manager, res.name, wrap_exception=False) + + +class _FakeResult(object): + def __init__(self, name, value): + self.name = name + self.value = value + + +class PrintResultTestCase(test_utils.TestCase): + @mock.patch('sys.stdout', io.StringIO()) + def test_print_dict(self): + dict = {'key': 'value'} + utils.print_dict(dict) + self.assertEqual('+----------+-------+\n' + '| Property | Value |\n' + '+----------+-------+\n' + '| key | value |\n' + '+----------+-------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', io.StringIO()) + def test_print_dict_wrap(self): + dict = {'key1': 'not wrapped', + 'key2': 'this will be wrapped'} + utils.print_dict(dict, wrap=16) + self.assertEqual('+----------+--------------+\n' + '| Property | Value |\n' + '+----------+--------------+\n' + '| key1 | not wrapped |\n' + '| key2 | this will be |\n' + '| | wrapped |\n' + '+----------+--------------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', io.StringIO()) + def test_print_list_sort_by_str(self): + objs = [_FakeResult("k1", 1), + _FakeResult("k3", 2), + _FakeResult("k2", 3)] + + utils.print_list(objs, ["Name", "Value"], sortby_index=0) + + self.assertEqual('+------+-------+\n' + '| Name | Value |\n' + '+------+-------+\n' + '| k1 | 1 |\n' + '| k2 | 3 |\n' + '| k3 | 2 |\n' + '+------+-------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', io.StringIO()) + def test_print_list_sort_by_integer(self): + objs = [_FakeResult("k1", 1), + _FakeResult("k3", 2), + _FakeResult("k2", 3)] + + utils.print_list(objs, ["Name", "Value"], sortby_index=1) + + self.assertEqual('+------+-------+\n' + '| Name | Value |\n' + '+------+-------+\n' + '| k1 | 1 |\n' + '| k3 | 2 |\n' + '| k2 | 3 |\n' + '+------+-------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', io.StringIO()) + def test_print_unicode_list(self): + objs = [_FakeResult("k", '\u2026')] + utils.print_list(objs, ["Name", "Value"]) + s = '\u2026' + self.assertEqual('+------+-------+\n' + '| Name | Value |\n' + '+------+-------+\n' + '| k | %s |\n' + '+------+-------+\n' % s, + sys.stdout.getvalue()) + + # without sorting + @mock.patch('sys.stdout', io.StringIO()) + def test_print_list_sort_by_none(self): + objs = [_FakeResult("k1", 1), + _FakeResult("k3", 3), + _FakeResult("k2", 2)] + + utils.print_list(objs, ["Name", "Value"], sortby_index=None) + + self.assertEqual('+------+-------+\n' + '| Name | Value |\n' + '+------+-------+\n' + '| k1 | 1 |\n' + '| k3 | 3 |\n' + '| k2 | 2 |\n' + '+------+-------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', io.StringIO()) + def test_print_dict_dictionary(self): + dict = {'k': {'foo': 'bar'}} + utils.print_dict(dict) + self.assertEqual('+----------+----------------+\n' + '| Property | Value |\n' + '+----------+----------------+\n' + '| k | {"foo": "bar"} |\n' + '+----------+----------------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', io.StringIO()) + def test_print_dict_list_dictionary(self): + dict = {'k': [{'foo': 'bar'}]} + utils.print_dict(dict) + self.assertEqual('+----------+------------------+\n' + '| Property | Value |\n' + '+----------+------------------+\n' + '| k | [{"foo": "bar"}] |\n' + '+----------+------------------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', io.StringIO()) + def test_print_dict_list(self): + dict = {'k': ['foo', 'bar']} + utils.print_dict(dict) + self.assertEqual('+----------+----------------+\n' + '| Property | Value |\n' + '+----------+----------------+\n' + '| k | ["foo", "bar"] |\n' + '+----------+----------------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', io.StringIO()) + def test_print_large_dict_list(self): + dict = {'k': ['foo1', 'bar1', 'foo2', 'bar2', + 'foo3', 'bar3', 'foo4', 'bar4']} + utils.print_dict(dict, wrap=40) + self.assertEqual( + '+----------+------------------------------------------+\n' + '| Property | Value |\n' + '+----------+------------------------------------------+\n' + '| k | ["foo1", "bar1", "foo2", "bar2", "foo3", |\n' + '| | "bar3", "foo4", "bar4"] |\n' + '+----------+------------------------------------------+\n', + sys.stdout.getvalue()) + + @mock.patch('sys.stdout', io.StringIO()) + def test_print_unicode_dict(self): + dict = {'k': '\u2026'} + utils.print_dict(dict) + s = '\u2026' + self.assertEqual('+----------+-------+\n' + '| Property | Value |\n' + '+----------+-------+\n' + '| k | %s |\n' + '+----------+-------+\n' % s, + sys.stdout.getvalue()) + + +class FlattenTestCase(test_utils.TestCase): + def test_flattening(self): + squashed = utils.flatten_dict( + {'a1': {'b1': 1234, + 'b2': 'string', + 'b3': set((1, 2, 3)), + 'b4': {'c1': ['l', 'l', ['l']], + 'c2': 'string'}}, + 'a2': ['l'], + 'a3': ('t',), + 'a4': {}}) + + self.assertEqual({'a1_b1': 1234, + 'a1_b2': 'string', + 'a1_b3': set([1, 2, 3]), + 'a1_b4_c1': ['l', 'l', ['l']], + 'a1_b4_c2': 'string', + 'a2': ['l'], + 'a3': ('t',), + 'a4': {}}, + squashed) + + def test_pretty_choice_dict(self): + d = {} + r = utils.pretty_choice_dict(d) + self.assertEqual("", r) + + d = {"k1": "v1", + "k2": "v2", + "k3": "v3"} + r = utils.pretty_choice_dict(d) + self.assertEqual("'k1=v1', 'k2=v2', 'k3=v3'", r) + + +class ValidationsTestCase(test_utils.TestCase): + def test_validate_flavor_metadata_keys_with_valid_keys(self): + valid_keys = ['key1', 'month.price', 'I-Am:AK-ey.01-', 'spaces and _'] + utils.validate_flavor_metadata_keys(valid_keys) + + def test_validate_flavor_metadata_keys_with_invalid_keys(self): + invalid_keys = ['/1', '?1', '%1', '<', '>', '\1'] + for key in invalid_keys: + try: + utils.validate_flavor_metadata_keys([key]) + self.fail("Invalid key passed validation: %s" % key) + except exceptions.CommandError as ce: + self.assertIn(key, str(ce)) + + +class DoActionOnManyTestCase(test_utils.TestCase): + + def _test_do_action_on_many(self, side_effect, fail): + action = mock.Mock(side_effect=side_effect) + + if fail: + self.assertRaises(exceptions.CommandError, + utils.do_action_on_many, + action, [1, 2], 'success with %s', 'error') + else: + utils.do_action_on_many(action, [1, 2], 'success with %s', 'error') + action.assert_has_calls([mock.call(1), mock.call(2)]) + + def test_do_action_on_many_success(self): + self._test_do_action_on_many([None, None], fail=False) + + def test_do_action_on_many_first_fails(self): + self._test_do_action_on_many([Exception(), None], fail=True) + + 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): + + def test_record_time(self): + times = [] + + with utils.record_time(times, True, 'a', 'b'): + pass + self.assertEqual(1, len(times)) + self.assertEqual(3, len(times[0])) + self.assertEqual('a b', times[0][0]) + self.assertIsInstance(times[0][1], float) + self.assertIsInstance(times[0][2], float) + + times = [] + with utils.record_time(times, False, 'x'): + pass + self.assertEqual(0, len(times)) + + +class PrepareQueryStringTestCase(test_utils.TestCase): + + 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}, self.ustr), + ({(1, 2): '1', (3, 4): '2'}, '?(1, 2)=1&(3, 4)=2') + ) + + 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 new file mode 100644 index 000000000..c3c77b39f --- /dev/null +++ b/novaclient/tests/unit/utils.py @@ -0,0 +1,128 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +from unittest import mock + +import fixtures +from oslo_serialization import jsonutils +import requests +from requests_mock.contrib import fixture as requests_mock_fixture +import testscenarios +import testtools + + +def _patch_mock_to_raise_for_invalid_assert_calls(): + def raise_for_invalid_assert_calls(wrapped): + def wrapper(_self, name): + valid_asserts = [ + 'assert_called_with', + 'assert_called_once_with', + 'assert_has_calls', + 'assert_any_calls'] + + if name.startswith('assert') and name not in valid_asserts: + raise AttributeError('%s is not a valid mock assert method' + % name) + + return wrapped(_self, name) + return wrapper + 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() + + +class TestCase(testtools.TestCase): + TEST_REQUEST_BASE = { + 'verify': True, + } + + def setUp(self): + super(TestCase, self).setUp() + if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or + os.environ.get('OS_STDOUT_CAPTURE') == '1'): + stdout = self.useFixture(fixtures.StringStream('stdout')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) + if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or + os.environ.get('OS_STDERR_CAPTURE') == '1'): + stderr = self.useFixture(fixtures.StringStream('stderr')).stream + self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) + + self.requests_mock = self.useFixture(requests_mock_fixture.Fixture()) + + def assert_request_id(self, request_id_mixin, request_id_list): + self.assertEqual(request_id_list, request_id_mixin.request_ids) + + +class FixturedTestCase(testscenarios.TestWithScenarios, TestCase): + + client_fixture_class = None + data_fixture_class = None + + def setUp(self): + super(FixturedTestCase, self).setUp() + + self.data_fixture = None + self.client_fixture = None + self.cs = None + + if self.client_fixture_class: + fix = self.client_fixture_class(self.requests_mock) + self.client_fixture = self.useFixture(fix) + self.cs = self.client_fixture.client + + if self.data_fixture_class: + fix = self.data_fixture_class(self.requests_mock) + self.data_fixture = self.useFixture(fix) + + def assert_called(self, method, path, body=None): + self.assertEqual(self.requests_mock.last_request.method, method) + self.assertEqual(self.requests_mock.last_request.path_url, path) + + if body: + req_data = self.requests_mock.last_request.body + if isinstance(req_data, bytes): + req_data = req_data.decode('utf-8') + 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(body, req_data) + + +class TestResponse(requests.Response): + """Class used to wrap requests.Response. + + Provide some convenience to initialize with a dict. + """ + + def __init__(self, data): + super(TestResponse, self).__init__() + self._text = None + if isinstance(data, dict): + self.status_code = data.get('status_code') + self.headers = data.get('headers') + # Fake the text attribute to streamline Response creation + self._text = data.get('text') + else: + self.status_code = data + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + @property + def text(self): + return self._text diff --git a/novaclient/tests/unit/v2/__init__.py b/novaclient/tests/unit/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py new file mode 100644 index 000000000..f3e6d9908 --- /dev/null +++ b/novaclient/tests/unit/v2/fakes.py @@ -0,0 +1,2464 @@ +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy +import datetime +import re +from unittest import mock +from urllib import parse + +from oslo_utils import strutils + +import novaclient +from novaclient import api_versions +from novaclient import client as base_client +from novaclient import exceptions +from novaclient.tests.unit import fakes +from novaclient.tests.unit import utils +from novaclient.v2 import client + +# regex to compare callback to result of get_endpoint() +# checks version number (vX or vX.X where X is a number) +# and also checks if the id is on the end +ENDPOINT_RE = re.compile( + r"^get_http:__nova_api:8774_v\d(_\d)?_\w{32}$") + +# accepts formats like v2 or v2.1 +ENDPOINT_TYPE_RE = re.compile(r"^v\d(\.\d)?$") + +# accepts formats like v2 or v2_1 +CALLBACK_RE = re.compile(r"^get_http:__nova_api:8774_v\d(_\d)?$") + +# fake image uuids +FAKE_IMAGE_UUID_1 = 'c99d7632-bd66-4be9-aed5-3dd14b223a76' +FAKE_IMAGE_UUID_2 = 'f27f479a-ddda-419a-9bbc-d6b56b210161' +FAKE_IMAGE_UUID_SNAPSHOT = '555cae93-fb41-4145-9c52-f5b923538a26' +FAKE_IMAGE_UUID_SNAP_DEL = '55bb23af-97a4-4068-bdf8-f10c62880ddf' +FAKE_IMAGE_UUID_BACKUP = '2f87e889-41a4-4778-8553-83f5eea68c5d' + +# fake request id +FAKE_REQUEST_ID = fakes.FAKE_REQUEST_ID +FAKE_REQUEST_ID_LIST = fakes.FAKE_REQUEST_ID_LIST +FAKE_RESPONSE_HEADERS = {'x-openstack-request-id': FAKE_REQUEST_ID} + +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): + + def __init__(self, api_version, *args, **kwargs): + client.Client.__init__(self, username='username', password='password', + project_id='project_id', auth_url='auth_url', + extensions=kwargs.get('extensions'), + direct_use=False, api_version=api_version) + self.client = FakeSessionClient(api_version=api_version, **kwargs) + + +class FakeSessionClient(base_client.SessionClient): + + def __init__(self, *args, **kwargs): + + self.callstack = [] + self.visited = [] + self.auth = mock.Mock() + self.session = mock.Mock() + self.service_type = 'service_type' + self.service_name = None + self.endpoint_override = None + self.interface = None + self.region_name = None + self.version = None + self.api_version = kwargs.get('api_version') + self.auth.get_auth_ref.return_value.project_id = 'tenant_id' + # determines which endpoint to return in get_endpoint() + # NOTE(augustina): this is a hacky workaround, ultimately + # we need to fix our whole mocking architecture (fixtures?) + if 'endpoint_type' in kwargs: + self.endpoint_type = kwargs['endpoint_type'] + else: + self.endpoint_type = 'endpoint_type' + self.logger = mock.MagicMock() + + def get_endpoint(self, **kwargs): + # check if endpoint matches expected format (eg, v2.1) + if (hasattr(self, 'endpoint_type') and + ENDPOINT_TYPE_RE.search(self.endpoint_type)): + return "http://nova-api:8774/%s/" % self.endpoint_type + else: + return ( + "http://nova-api:8774/v2.1/190a755eef2e4aac9f06aa6be9786385") + + def request(self, url, method, **kwargs): + return self._cs_request(url, method, **kwargs) + + def _cs_request(self, url, method, **kwargs): + # Check that certain things are called correctly + if method in ['GET', 'DELETE']: + assert 'body' not in kwargs + elif method == 'PUT': + assert 'body' in kwargs + + if url is not None: + # Call the method + args = parse.parse_qsl(parse.urlparse(url)[4]) + kwargs.update(args) + munged_url = url.rsplit('?', 1)[0] + munged_url = munged_url.strip('/').replace('/', '_') + munged_url = munged_url.replace('.', '_') + munged_url = munged_url.replace('-', '_') + 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": + # To get API version information, it is necessary to GET + # a nova endpoint directly without "v2/". + callback = "get_versions" + elif CALLBACK_RE.search(callback): + callback = "get_current_version" + elif ENDPOINT_RE.search(callback): + # compare callback to result of get_endpoint() + # NOTE(sdague): if we try to call a thing that doesn't + # exist, just return a 404. This allows the stack to act + # more like we'd expect when making REST calls. + raise exceptions.NotFound('404') + + # Handle fake glance v2 requests + v2_image = False + if callback.startswith('get_v2_images'): + v2_image = True + callback = callback.replace('get_v2_', 'get_') + + simulate_pagination_next_links = [ + 'get_os_simple_tenant_usage', + 'get_os_simple_tenant_usage_tenant_id', + ] + if callback in simulate_pagination_next_links: + while callback in self.visited: + callback += '_next' + + if not hasattr(self, callback): + raise AssertionError('Called unknown API method: %s %s, ' + 'expected fakes method name: %s' % + (method, url, callback)) + + # Note the call + self.visited.append(callback) + self.callstack.append((method, url, kwargs.get('body'))) + + status, headers, body = getattr(self, callback)(**kwargs) + + # If we're dealing with a glance v2 image response, the response + # isn't wrapped like the compute images API proxy is, so handle that. + if body and v2_image and 'image' in body: + body = body['image'] + + r = utils.TestResponse({ + "status_code": status, + "text": body, + "headers": headers, + }) + + if status >= 400: + raise exceptions.from_response(r, body, url, method) + + return r, body + + def get_versions(self): + return (200, FAKE_RESPONSE_HEADERS, { + "versions": [ + {"status": "SUPPORTED", "updated": "2011-01-21T11:33:21Z", + "links": [{"href": "http://nova-api:8774/v2/", + "rel": "self"}], + "min_version": "", + "version": "", + "id": "v2.0"}, + {"status": "CURRENT", "updated": "2013-07-23T11:33:21Z", + "links": [{"href": "http://nova-api:8774/v2.1/", + "rel": "self"}], + "min_version": novaclient.API_MIN_VERSION.get_string(), + "version": novaclient.API_MAX_VERSION.get_string(), + "id": "v2.1"} + ]}) + + def get_current_version(self): + versions = { + # v2 doesn't contain version or min_version fields + "v2": { + "version": { + "status": "SUPPORTED", + "updated": "2011-01-21T11:33:21Z", + "links": [{ + "href": "http://nova-api:8774/v2/", + "rel": "self" + }], + "id": "v2.0" + } + }, + "v2.1": { + "version": { + "status": "CURRENT", + "updated": "2013-07-23T11:33:21Z", + "links": [{ + "href": "http://nova-api:8774/v2.1/", + "rel": "self" + }], + "min_version": novaclient.API_MIN_VERSION.get_string(), + "version": novaclient.API_MAX_VERSION.get_string(), + "id": "v2.1" + } + } + } + + # if an endpoint_type that matches a version wasn't initialized, + # default to v2.1 + endpoint = 'v2.1' + + if hasattr(self, 'endpoint_type'): + if ENDPOINT_TYPE_RE.search(self.endpoint_type): + if self.endpoint_type in versions: + endpoint = self.endpoint_type + else: + raise AssertionError( + "Unknown endpoint_type:%s" % self.endpoint_type) + + return (200, FAKE_RESPONSE_HEADERS, versions[endpoint]) + + # + # agents + # + + def get_os_agents(self, **kw): + hypervisor = kw.get('hypervisor', 'kvm') + return (200, {}, { + 'agents': + [{'hypervisor': hypervisor, + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'xxx://xxxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f545', + 'id': 1}, + {'hypervisor': hypervisor, + 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'xxx://xxxx/xxx/xxx1', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'id': 2}]}) + + def post_os_agents(self, body): + return (200, {}, {'agent': { + 'url': '/xxx/xxx/xxx', + 'hypervisor': body['agent']['hypervisor'], + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'version': '7.0', + 'architecture': 'x86', + 'os': 'win', + 'id': 1}}) + + def delete_os_agents_1(self, **kw): + return (202, {}, None) + + def put_os_agents_1(self, body, **kw): + return (200, {}, { + "agent": {"url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546", + 'id': 1}}) + + # + # 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": [ + { + "uri": "*", + "regex": ".*", + "limit": [ + { + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + }, + { + "value": 10, + "verb": "PUT", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + }, + { + "value": 100, + "verb": "DELETE", + "remaining": 100, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z" + } + ] + }, + { + "uri": "*/servers", + "regex": "^/servers", + "limit": [ + { + "verb": "POST", + "value": 25, + "remaining": 24, + "unit": "DAY", + "next-available": "2011-12-15T22:42:45Z" + } + ] + } + ], + "absolute": absolute, + }}) + + # + # Servers + # + + def get_servers(self, **kw): + 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): + servers = {"servers": [ + { + "id": '1234', + "name": "sample-server", + "image": { + "id": FAKE_IMAGE_UUID_2, + "name": "sample image", + }, + "flavor": { + "id": 1, + "name": "256 MiB Server", + }, + "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", + "status": "BUILD", + "progress": 60, + "addresses": { + "public": [ + { + "version": 4, + "addr": "1.2.3.4", + }, + { + "version": 4, + "addr": "5.6.7.8", + }], + "private": [{ + "version": 4, + "addr": "10.11.12.13", + }], + }, + "metadata": { + "Server Label": "Web Head 1", + "Image Version": "2.1" + }, + "OS-EXT-SRV-ATTR:host": "computenode1", + "security_groups": [{ + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], + "OS-EXT-MOD:some_thing": "mod_some_thing_value", + }, + { + "id": '5678', + "name": "sample-server2", + "image": { + "id": FAKE_IMAGE_UUID_1, + "name": "sample image", + }, + "flavor": { + "id": 1, + "name": "256 MiB Server", + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], + "private": [{ + "version": 4, + "addr": "10.13.12.13", + }], + }, + "metadata": { + "Server Label": "DB 1" + }, + "OS-EXT-SRV-ATTR:host": "computenode2", + "security_groups": [ + { + 'id': 1, 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }, + { + 'id': 2, 'name': 'securitygroup2', + 'description': 'ANOTHER_FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7' + }], + }, + { + "id": '9012', + "name": "sample-server3", + "image": "", + "flavor": { + "id": 1, + "name": "256 MiB Server", + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + "addresses": { + "public": [ + { + "version": 4, + "addr": "4.5.6.7", + }, + { + "version": 4, + "addr": "5.6.9.8", + }], + "private": [{ + "version": 4, + "addr": "10.13.12.13", + }], + }, + "metadata": { + "Server Label": "DB 1" + } + }, + { + "id": '9013', + "name": "sample-server4", + "flavor": { + "id": '80645cf4-6ad3-410a-bbc8-6f3e1e291f51', + }, + "image": { + "id": '3e861307-73a6-4d1f-8d68-f68b03223032', + }, + "hostId": "9e107d9d372bb6826bd81d3542a419d6", + "status": "ACTIVE", + }, + { + "id": '9014', + "name": "help", + "flavor": { + "id": '80645cf4-6ad3-410a-bbc8-6f3e1e291f51', + }, + "image": { + "id": '3e861307-73a6-4d1f-8d68-f68b03223032', + }, + "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']) + fakes.assert_has_keys( + body['server'], + required=['name', 'imageRef', 'flavorRef'], + optional=['metadata', 'personality']) + if 'personality' in body['server']: + for pfile in body['server']['personality']: + fakes.assert_has_keys(pfile, required=['path', 'contents']) + if body['server']['name'] == 'some-bad-server': + return (202, {}, self.get_servers_1235()[2]) + else: + return (202, {}, self.get_servers_1234()[2]) + + def get_servers_1234(self, **kw): + 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]} + r['server']['id'] = '1235' + r['server']['status'] = 'error' + r['server']['fault'] = {'message': 'something went wrong!'} + return (200, {}, r) + + def get_servers_5678(self, **kw): + r = {'server': self.get_servers_detail()[2]['servers'][1]} + return (200, {}, r) + + def get_servers_9012(self, **kw): + r = {'server': self.get_servers_detail()[2]['servers'][2]} + return (200, {}, r) + + def get_servers_9013(self, **kw): + r = {'server': self.get_servers_detail()[2]['servers'][3]} + return (200, {}, r) + + 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) + + def delete_os_server_groups_56789(self, **kw): + return (202, {}, None) + + def delete_servers_1234(self, **kw): + return (202, {}, None) + + def delete_servers_5678(self, **kw): + return (202, {}, None) + + def delete_servers_1234_metadata_key1(self, **kw): + return (204, {}, None) + + def delete_servers_1234_metadata_key2(self, **kw): + return (204, {}, None) + + def post_servers_1234_metadata(self, **kw): + return (204, {}, {'metadata': {'test_key': 'test_value'}}) + + def get_servers_1234_diagnostics(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def post_servers_uuid1_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def post_servers_uuid2_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + def post_servers_uuid3_metadata(self, **kw): + return (204, {}, {'metadata': {'key1': 'val1'}}) + + 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'}) + + def delete_servers_uuid2_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + def delete_servers_uuid3_metadata_key1(self, **kw): + return (200, {}, {'data': 'Fake diagnostics'}) + + 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": [{ + 'id': 1, + 'name': 'securitygroup1', + 'description': 'FAKE_SECURITY_GROUP', + 'tenant_id': '4ffc664c198e435e9853f2538fbcd7a7', + 'rules': []}] + }) + + def get_servers_1234_topology(self, **kw): + return 200, {}, SERVER_TOPOLOGY + + # + # Server password + # + + # Testing with the following password and key + # + # Clear password: FooBar123 + # + # RSA Private Key: novaclient/tests/unit/idfake.pem + # + # Encrypted password + # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r + # qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho + # QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw + # /y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N + # tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk + # Hi/fmZZNQQqj1Ijq0caOIw== + def get_servers_1234_os_server_password(self, **kw): + return (200, {}, { + 'password': + 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' + 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' + 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' + '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' + 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' + 'Hi/fmZZNQQqj1Ijq0caOIw=='}) + + def delete_servers_1234_os_server_password(self, **kw): + return (202, {}, None) + + # + # Server actions + # + + none_actions = ['revertResize', 'os-stop', 'os-start', + 'forceDelete', 'restore', 'pause', 'unpause', 'unlock', + 'unrescue', 'resume', 'suspend', 'shelve', + 'shelveOffload', 'resetNetwork'] + type_actions = ['os-getVNCConsole', 'os-getSPICEConsole', + 'os-getRDPConsole'] + + @classmethod + def check_server_actions(cls, body): + action = list(body)[0] + if action == 'reboot': + assert list(body[action]) == ['type'] + assert body[action]['type'] in ['HARD', 'SOFT'] + elif action == 'resize': + assert 'flavorRef' in body[action] + elif action in cls.none_actions: + assert body[action] is None + elif action == 'changePassword': + assert list(body[action]) == ['adminPass'] + elif action in cls.type_actions: + assert list(body[action]) == ['type'] + elif action == 'os-resetState': + assert list(body[action]) == ['state'] + elif action == 'resetNetwork': + assert body[action] is None + elif action in ['addSecurityGroup', 'removeSecurityGroup']: + assert list(body[action]) == ['name'] + elif action == 'trigger_crash_dump': + assert body[action] is None + else: + return False + return True + + def post_servers_1234_action(self, body, **kw): + _headers = dict() + _body = None + resp = 202 + assert len(body.keys()) == 1 + action = list(body)[0] + + if self.check_server_actions(body): + # NOTE(snikitin): No need to do any operations here. This 'pass' + # is needed to avoid AssertionError in the last 'else' statement + # if we found 'action' in method check_server_actions and + # raise AssertionError if we didn't find 'action' at all. + pass + elif action == 'os-migrateLive': + expected = set(['host', 'block_migration']) + if self.api_version >= api_versions.APIVersion("2.30"): + if 'force' in body[action].keys(): + # force can be optional + expected.add('force') + 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') + assert 'imageRef' in body + _body = self.get_servers_1234()[2] + _body['server']['adminPass'] = adminPass + elif action == 'confirmResize': + assert body[action] is None + # This one method returns a different response code + return (204, {}, None) + elif action == 'rescue': + if body[action]: + keys = set(body[action].keys()) + assert not (keys - set(['adminPass', 'rescue_image_ref'])) + else: + assert body[action] is None + _body = {'adminPass': 'RescuePassword'} + elif action == 'createImage': + assert set(body[action].keys()) == set(['name', 'metadata']) + if self.api_version < api_versions.APIVersion('2.45'): + _headers = dict(location="http://blah/images/%s" % + FAKE_IMAGE_UUID_SNAPSHOT) + else: + _body = {'image_id': FAKE_IMAGE_UUID_SNAPSHOT} + if body[action]['name'] == 'mysnapshot_deleted': + _headers = dict(location="http://blah/images/%s" % + FAKE_IMAGE_UUID_SNAP_DEL) + elif action == 'createBackup': + assert set(body[action].keys()) == set(['name', 'backup_type', + 'rotation']) + if self.api_version < api_versions.APIVersion('2.45'): + _headers = dict(location="http://blah/images/%s" % + FAKE_IMAGE_UUID_BACKUP) + else: + _body = {'image_id': FAKE_IMAGE_UUID_BACKUP} + elif action == 'os-getConsoleOutput': + assert list(body[action]) == ['length'] + return (202, {}, {'output': 'foo'}) + elif action == 'evacuate': + keys = list(body[action]) + if 'adminPass' in keys: + keys.remove('adminPass') + if 'host' in keys: + keys.remove('host') + if 'onSharedStorage' in keys: + keys.remove('onSharedStorage') + if 'force' in keys: + keys.remove('force') + assert set(keys) == set() + else: + raise AssertionError("Unexpected server action: %s" % action) + _headers.update(FAKE_RESPONSE_HEADERS) + return (resp, _headers, _body) + + def post_servers_5678_action(self, body, **kw): + return self.post_servers_1234_action(body, **kw) + + # + # 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 included_fields: + del flavor[k] + + return (200, FAKE_RESPONSE_HEADERS, flavors) + + def get_flavors_detail(self, **kw): + flavors = {'flavors': [ + {'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 MiB Server', 'ram': 512, 'disk': 20, + 'OS-FLV-EXT-DATA:ephemeral': 20, + 'os-flavor-access:is_public': False, + 'links': {}}, + {'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 MiB Server', 'ram': 128, 'disk': 0, + 'OS-FLV-EXT-DATA:ephemeral': 0, + 'os-flavor-access:is_public': True, + 'links': {}} + ]} + + if 'is_public' not in kw: + filter_is_public = True + else: + if kw['is_public'].lower() == 'none': + filter_is_public = None + else: + filter_is_public = strutils.bool_from_string(kw['is_public'], + True) + + if filter_is_public is not None: + if filter_is_public: + flavors['flavors'] = [ + v for v in flavors['flavors'] + if v['os-flavor-access:is_public'] + ] + else: + flavors['flavors'] = [ + v for v in flavors['flavors'] + 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): + return ( + 200, + FAKE_RESPONSE_HEADERS, + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][0]} + ) + + def get_flavors_2(self, **kw): + return ( + 200, + {}, + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][1]} + ) + + def get_flavors_3(self, **kw): + # Diablo has no ephemeral + return ( + 200, + FAKE_RESPONSE_HEADERS, + {'flavor': { + 'id': 3, + 'name': '256 MiB Server', + 'ram': 256, + 'disk': 10, + }}, + ) + + def get_flavors_512_MiB_Server(self, **kw): + raise exceptions.NotFound('404') + + def get_flavors_128_MiB_Server(self, **kw): + raise exceptions.NotFound('404') + + def get_flavors_80645cf4_6ad3_410a_bbc8_6f3e1e291f51(self, **kw): + raise exceptions.NotFound('404') + + def get_flavors_aa1(self, **kw): + # Alphanumeric flavor id are allowed. + return ( + 200, + FAKE_RESPONSE_HEADERS, + {'flavor': + self.get_flavors_detail(is_public='None')[2]['flavors'][3]} + ) + + def get_flavors_4(self, **kw): + return ( + 200, + {}, + {'flavor': + 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) + + def delete_flavors_2(self, **kw): + return (202, FAKE_RESPONSE_HEADERS, None) + + def post_flavors(self, body, **kw): + return ( + 202, + FAKE_RESPONSE_HEADERS, + {'flavor': + 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, + {}, + {'extra_specs': {"k1": "v1"}}) + + def get_flavors_2_os_extra_specs(self, **kw): + return ( + 200, + {}, + {'extra_specs': {"k2": "v2"}}) + + def get_flavors_aa1_os_extra_specs(self, **kw): + return ( + 200, {}, + {'extra_specs': {"k3": "v3"}}) + + def get_flavors_4_os_extra_specs(self, **kw): + return ( + 200, + {}, + {'extra_specs': {"k4": "v4"}}) + + def post_flavors_1_os_extra_specs(self, body, **kw): + assert list(body) == ['extra_specs'] + fakes.assert_has_keys(body['extra_specs'], + required=['k1']) + return ( + 200, + FAKE_RESPONSE_HEADERS, + {'extra_specs': {"k1": "v1"}}) + + def post_flavors_4_os_extra_specs(self, body, **kw): + assert list(body) == ['extra_specs'] + + return ( + 200, + FAKE_RESPONSE_HEADERS, + body) + + def delete_flavors_1_os_extra_specs_k1(self, **kw): + return (204, {}, None) + + # + # Flavor access + # + + def get_flavors_2_os_flavor_access(self, **kw): + return ( + 200, FAKE_RESPONSE_HEADERS, + {'flavor_access': [{'flavor_id': '2', 'tenant_id': 'proj1'}, + {'flavor_id': '2', 'tenant_id': 'proj2'}]}) + + def post_flavors_2_action(self, body, **kw): + return (202, FAKE_RESPONSE_HEADERS, + self.get_flavors_2_os_flavor_access()[2]) + + # + # Images + # + def get_images(self, **kw): + images = [ + { + "id": FAKE_IMAGE_UUID_SNAPSHOT, + "name": "My Server Backup", + "serverId": '1234', + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "SAVING", + "progress": 80, + "links": {}, + }, + { + "id": FAKE_IMAGE_UUID_SNAP_DEL, + "name": "My Server Backup Deleted", + "serverId": '1234', + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "DELETED", + "fault": {'message': 'Image has been deleted.'}, + "links": {}, + }, + { + 'id': FAKE_IMAGE_UUID_1, + 'name': 'CentOS 5.2', + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "ACTIVE", + "test_key": "test_value", + "links": {}, + }, + { + "id": FAKE_IMAGE_UUID_2, + "name": "My Server Backup", + "serverId": '1234', + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "SAVING", + "progress": 80, + "links": {}, + }, + { + "id": FAKE_IMAGE_UUID_BACKUP, + "name": "back1", + "serverId": '1234', + "updated": "2010-10-10T12:00:00Z", + "created": "2010-08-10T12:00:00Z", + "status": "SAVING", + "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]}) + + def get_images_55bb23af_97a4_4068_bdf8_f10c62880ddf(self, **kw): + return (200, {}, {'image': self.get_images()[2]['images'][1]}) + + def get_images_c99d7632_bd66_4be9_aed5_3dd14b223a76(self, **kw): + return (200, {}, {'image': self.get_images()[2]['images'][2]}) + + def get_images_f27f479a_ddda_419a_9bbc_d6b56b210161(self, **kw): + return (200, {}, {'image': self.get_images()[2]['images'][3]}) + + def get_images_2f87e889_41a4_4778_8553_83f5eea68c5d(self, **kw): + return (200, {}, {'image': self.get_images()[2]['images'][4]}) + + def get_images_3e861307_73a6_4d1f_8d68_f68b03223032(self): + raise exceptions.NotFound('404') + + # + # Keypairs + # + def get_os_keypairs_test(self, *kw): + return (200, {}, {'keypair': + self.get_os_keypairs()[2]['keypairs'][0]['keypair']}) + + def get_os_keypairs(self, user_id=None, limit=None, marker=None, *kw): + return (200, {}, { + "keypairs": [{"keypair": { + "public_key": "FAKE_SSH_RSA", + "private_key": "FAKE_PRIVATE_KEY", + "user_id": "81e373b596d6466e99c4896826abaa46", + "name": "test", + "deleted": False, + "created_at": "2014-04-19T02:16:44.000000", + "updated_at": "2014-04-19T10:12:3.000000", + "fingerprint": "FAKE_KEYPAIR", + "deleted_at": None, + "id": 4}} + ]}) + + def delete_os_keypairs_test(self, **kw): + return (202, {}, None) + + def post_os_keypairs(self, body, **kw): + assert list(body) == ['keypair'] + fakes.assert_has_keys(body['keypair'], + required=['name']) + r = {'keypair': self.get_os_keypairs()[2]['keypairs'][0]['keypair']} + return (202, {}, r) + + # + # Quotas + # + + def get_os_quota_sets_tenant_id(self, **kw): + return (200, {}, { + 'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def get_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221bff44578b0300df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def get_os_quota_sets_97f4c221bff44578b0300df4ef119353_detail(self, **kw): + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221bff44578b0300df4ef119353', + 'cores': { + 'in_use': 0, + 'limit': 20, + 'reserved': 0 + }, + 'fixed_ips': { + 'in_use': 0, + 'limit': -1, + 'reserved': 0 + }, + 'floating_ips': { + 'in_use': 0, + 'limit': 10, + 'reserved': 0 + }, + 'injected_file_content_bytes': { + 'in_use': 0, + 'limit': 10240, + 'reserved': 0 + }, + 'injected_file_path_bytes': { + 'in_use': 0, + 'limit': 255, + 'reserved': 0 + }, + 'injected_files': { + 'in_use': 0, + 'limit': 5, + 'reserved': 0 + }, + 'instances': { + 'in_use': 0, + 'limit': 10, + 'reserved': 0 + }, + 'key_pairs': { + 'in_use': 0, + 'limit': 100, + 'reserved': 0 + }, + 'metadata_items': { + 'in_use': 0, + 'limit': 128, + 'reserved': 0 + }, + 'ram': { + 'in_use': 0, + 'limit': 51200, + 'reserved': 0 + }, + 'security_group_rules': { + 'in_use': 0, + 'limit': 20, + 'reserved': 0 + }, + 'security_groups': { + 'in_use': 0, + 'limit': 10, + 'reserved': 0 + }, + 'server_group_members': { + 'in_use': 0, + 'limit': 10, + 'reserved': 0 + }, + 'server_groups': { + 'in_use': 0, + 'limit': 10, + 'reserved': 0 + } + }}) + + def get_os_quota_sets_97f4c221bff44578b0300df4ef119353_defaults(self): + return (200, {}, { + 'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def get_os_quota_sets_tenant_id_defaults(self): + return (200, {}, { + 'quota_set': { + 'tenant_id': 'test', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def put_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, body, **kw): + assert list(body) == ['quota_set'] + fakes.assert_has_keys(body['quota_set']) + return (200, {}, { + 'quota_set': { + 'tenant_id': '97f4c221bff44578b0300df4ef119353', + 'metadata_items': [], + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'keypairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def delete_os_quota_sets_97f4c221bff44578b0300df4ef119353(self, **kw): + return (202, {}, {}) + + # + # Quota Classes + # + + 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': { + 'id': 'test', + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'server_groups': 1, + 'server_group_members': 1}}) + return (200, FAKE_RESPONSE_HEADERS, { + 'quota_class_set': { + 'id': 'test', + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'fixed_ips': -1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + 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': { + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'server_groups': 1, + 'server_group_members': 1}}) + return (200, {}, { + 'quota_class_set': { + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'fixed_ips': -1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + def put_os_quota_class_sets_97f4c221bff44578b0300df4ef119353(self, + body, **kw): + assert list(body) == ['quota_class_set'] + return (200, {}, { + 'quota_class_set': { + 'metadata_items': 1, + 'injected_file_content_bytes': 1, + 'injected_file_path_bytes': 1, + 'ram': 1, + 'floating_ips': 1, + 'instances': 1, + 'injected_files': 1, + 'cores': 1, + 'key_pairs': 1, + 'security_groups': 1, + 'security_group_rules': 1}}) + + # + # Tenant Usage + # + def get_os_simple_tenant_usage(self, **kw): + 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, {'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, {'tenant_usages': []}) + + def get_os_simple_tenant_usage_tenantfoo(self, **kw): + 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, {}, {'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, {}, {'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, {}, {'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, {}, {'tenant_usage': {}}) + + # + # Aggregates + # + + def get_os_aggregates(self, *kw): + response = (200, {}, {"aggregates": [ + {'id': '1', + 'name': 'test', + 'availability_zone': 'nova1'}, + {'id': '2', + 'name': 'test2', + 'availability_zone': 'nova1'}, + {'id': '3', + 'name': 'test3', + 'metadata': {'test': "dup", "none_key": "Nine"}}, + ]}) + # microversion >= 2.41 returns the uuid in the response + if self.api_version >= api_versions.APIVersion('2.41'): + aggregates = response[2]['aggregates'] + aggregates[0]['uuid'] = '80785864-087b-45a5-a433-b20eac9b58aa' + aggregates[1]['uuid'] = '30827713-5957-4b68-8fd3-ccaddb568c24' + aggregates[2]['uuid'] = '9a651b22-ce3f-4a87-acd7-98446ef591c4' + return response + + def _return_aggregate(self): + r = {'aggregate': self.get_os_aggregates()[2]['aggregates'][0]} + return (200, {}, r) + + def _return_aggregate_3(self): + r = {'aggregate': self.get_os_aggregates()[2]['aggregates'][2]} + return (200, {}, r) + + def get_os_aggregates_1(self, **kw): + return self._return_aggregate() + + def get_os_aggregates_3(self, **kw): + return self._return_aggregate_3() + + def post_os_aggregates(self, body, **kw): + return self._return_aggregate() + + def put_os_aggregates_1(self, body, **kw): + return self._return_aggregate() + + def post_os_aggregates_1_action(self, body, **kw): + return self._return_aggregate() + + def post_os_aggregates_3_action(self, body, **kw): + return self._return_aggregate_3() + + def delete_os_aggregates_1(self, **kw): + return (202, {}, None) + + def post_os_aggregates_1_images(self, body, **kw): + return (202, {}, None) + + # + # Services + # + def get_os_services(self, **kw): + host = kw.get('host', 'host1') + binary = kw.get('binary', 'nova-compute') + if self.api_version >= api_versions.APIVersion('2.53'): + service_id_1 = FAKE_SERVICE_UUID_1 + service_id_2 = FAKE_SERVICE_UUID_2 + else: + service_id_1 = 1 + service_id_2 = 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, + {'service': {'host': body['host'], + 'binary': body['binary'], + 'status': 'enabled'}}) + + def put_os_services_disable(self, body, **kw): + return (200, FAKE_RESPONSE_HEADERS, + {'service': {'host': body['host'], + 'binary': body['binary'], + 'status': 'disabled'}}) + + def put_os_services_disable_log_reason(self, body, **kw): + return (200, FAKE_RESPONSE_HEADERS, {'service': { + 'host': body['host'], + 'binary': body['binary'], + 'status': 'disabled', + 'disabled_reason': body['disabled_reason']}}) + + def put_os_services_75e9eabc_ed3b_4f11_8bba_add1e7e7e2de( + self, body, **kw): + """This should only be called with microversion >= 2.53.""" + return (200, FAKE_RESPONSE_HEADERS, {'service': { + 'host': 'host1', + 'binary': 'nova-compute', + 'status': body.get('status', 'enabled'), + 'disabled_reason': body.get('disabled_reason'), + 'forced_down': body.get('forced_down', False)}}) + + def delete_os_services_1(self, **kw): + return (204, FAKE_RESPONSE_HEADERS, None) + + def delete_os_services_75e9eabc_ed3b_4f11_8bba_add1e7e7e2de(self, **kwarg): + return (204, FAKE_RESPONSE_HEADERS, None) + + def put_os_services_force_down(self, body, **kw): + return (200, FAKE_RESPONSE_HEADERS, {'service': { + 'host': body['host'], + 'binary': body['binary'], + 'forced_down': False}}) + + # + # Hypervisors + # + + def get_os_hypervisors(self, **kw): + return (200, {}, { + "hypervisors": [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) + + def get_os_hypervisors_statistics(self, **kw): + return (200, {}, { + "hypervisor_statistics": { + 'count': 2, + 'vcpus': 8, + 'memory_mb': 20 * 1024, + 'local_gb': 500, + 'vcpus_used': 4, + 'memory_mb_used': 10 * 1024, + 'local_gb_used': 250, + 'free_ram_mb': 10 * 1024, + 'free_disk_gb': 250, + 'current_workload': 4, + 'running_vms': 4, + 'disk_available_least': 200} + }) + + def get_os_hypervisors_hyper1(self, **kw): + return (200, {}, { + 'hypervisor': + {'id': 1234, + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': "xen", + 'hypervisor_version': 3, + 'hypervisor_hostname': "hyper1", + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100}}) + + def get_os_hypervisors_region_child_1(self, **kw): + return (200, {}, { + 'hypervisor': + {'id': 'region!child@1', + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': "xen", + 'hypervisor_version': 3, + 'hypervisor_hostname': "hyper1", + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100}}) + + def get_os_hypervisors_hyper_search(self, **kw): + return (200, {}, { + 'hypervisors': [ + {'id': 1234, 'hypervisor_hostname': 'hyper1'}, + {'id': 5678, 'hypervisor_hostname': 'hyper2'}]}) + + def get_os_hypervisors_hyper_servers(self, **kw): + return (200, {}, { + 'hypervisors': [ + {'id': 1234, + 'hypervisor_hostname': 'hyper1', + 'servers': [ + {'name': 'inst1', 'uuid': 'uuid1'}, + {'name': 'inst2', 'uuid': 'uuid2'}]}, + {'id': 5678, + 'hypervisor_hostname': 'hyper2', + 'servers': [ + {'name': 'inst3', 'uuid': 'uuid3'}, + {'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'}]}) + + def get_os_hypervisors_1234(self, **kw): + return (200, {}, { + 'hypervisor': + {'id': 1234, + 'service': {'id': 1, 'host': 'compute1'}, + 'vcpus': 4, + 'memory_mb': 10 * 1024, + 'local_gb': 250, + 'vcpus_used': 2, + 'memory_mb_used': 5 * 1024, + 'local_gb_used': 125, + 'hypervisor_type': "xen", + 'hypervisor_version': 3, + 'hypervisor_hostname': "hyper1", + 'free_ram_mb': 5 * 1024, + 'free_disk_gb': 125, + 'current_workload': 2, + 'running_vms': 2, + 'cpu_info': 'cpu_info', + 'disk_available_least': 100}}) + + def get_os_hypervisors_1234_uptime(self, **kw): + return (200, {}, { + 'hypervisor': {'id': 1234, + 'hypervisor_hostname': "hyper1", + 'uptime': "fake uptime"}}) + + def get_os_hypervisors_region_child_1_uptime(self, **kw): + return (200, {}, { + 'hypervisor': {'id': 'region!child@1', + 'hypervisor_hostname': "hyper1", + 'uptime': "fake uptime"}}) + + def get_v2_0_networks(self, **kw): + """Return neutron proxied networks. + + We establish a few different possible networks that we can get + by name, which we can then call in tests. The only usage of + this API should be for name -> id translation, however a full + valid neutron block is provided for the private network to see + the kinds of things that will be in that payload. + """ + + name = kw.get('name', "blank") + + networks_by_name = { + 'private': [ + {"status": "ACTIVE", + "router:external": False, + "availability_zone_hints": [], + "availability_zones": ["nova"], + "description": "", + "name": "private", + "subnets": ["64706c26-336c-4048-ab3c-23e3283bca2c", + "18512740-c760-4d5f-921f-668105c9ee44"], + "shared": False, + "tenant_id": "abd42f270bca43ea80fe4a166bc3536c", + "created_at": "2016-08-15T17:34:49", + "tags": [], + "ipv6_address_scope": None, + "updated_at": "2016-08-15T17:34:49", + "admin_state_up": True, + "ipv4_address_scope": None, + "port_security_enabled": True, + "mtu": 1450, + "id": "e43a56c7-11d4-45c9-8681-ddc8171b5850", + "revision": 2}], + 'duplicate': [ + {"status": "ACTIVE", + "id": "e43a56c7-11d4-45c9-8681-ddc8171b5850"}, + {"status": "ACTIVE", + "id": "f43a56c7-11d4-45c9-8681-ddc8171b5850"}], + 'blank': [] + } + + return (200, {}, {"networks": networks_by_name[name]}) + + def get_os_availability_zone_detail(self, **kw): + return (200, {}, { + "availabilityZoneInfo": [ + {"zoneName": "zone-1", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "nova-compute": { + "active": True, + "available": True, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0)}}}}, + {"zoneName": "internal", + "zoneState": {"available": True}, + "hosts": { + "fake_host-1": { + "nova-sched": { + "active": True, + "available": True, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 25, 0)}}, + "fake_host-2": { + "nova-network": { + "active": True, + "available": False, + "updated_at": datetime.datetime( + 2012, 12, 26, 14, 45, 24, 0)}}}}, + {"zoneName": "zone-2", + "zoneState": {"available": False}, + "hosts": None}]}) + + def get_servers_1234_os_interface(self, **kw): + attachments = { + "interfaceAttachments": [ + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "port_id": "port-id-1", + "mac_address": "aa:bb:cc:dd:ee:ff", + "fixed_ips": [{"ip_address": "1.2.3.4"}], + }, + {"port_state": "ACTIVE", + "net_id": "net-id-1", + "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): + 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): + 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): + attachments = { + "volumeAttachments": [ + {"display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [{"id": "3333", "links": ''}], + "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, { + "volumeAttachment": + {"display_name": "Work", + "display_description": "volume for work", + "status": "ATTACHED", + "id": "15e59938-07d5-11e1-90e3-e3dffe0c5983", + "created_at": "2011-09-09T00:00:00Z", + "attached": "2011-11-11T00:00:00Z", + "size": 1024, + "attachments": [{"id": "3333", "links": ''}], + "metadata": {}}}) + + def delete_servers_1234_os_volume_attachments_Work(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, {}) + + def get_servers_1234_os_instance_actions(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'): + # 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": 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, {}, {} + + def post_servers_uuid2_action(self, **kw): + return 202, {}, {} + + def post_servers_uuid3_action(self, **kw): + return 202, {}, {} + + def post_servers_uuid4_action(self, **kw): + return 202, {}, {} + + def post_servers_uuid5_action(self, **kw): + return 202, {}, {} + + def post_servers_uuid6_action(self, **kw): + return 202, {}, {} + + def get_os_migrations(self, **kw): + migration1 = { + "created_at": "2012-10-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": '1234', + "instance_uuid": "instance_id_123", + "new_instance_type_id": 2, + "old_instance_type_id": 1, + "source_compute": "compute1", + "source_node": "node1", + "status": "Done", + "updated_at": "2012-10-29T13:42:02.000000" + } + + migration2 = { + "created_at": "2012-10-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": '1234', + "instance_uuid": "instance_id_456", + "new_instance_type_id": 2, + "old_instance_type_id": 1, + "source_compute": "compute1", + "source_node": "node1", + "status": "Done", + "updated_at": "2013-11-50T13:42:02.000000" + } + + if self.api_version >= api_versions.APIVersion("2.23"): + 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']: + migration_list.append(migration1) + elif instance_uuid == migration2['instance_uuid']: + migration_list.append(migration2) + elif instance_uuid is None: + migration_list.extend([migration1, migration2]) + + migrations = {'migrations': migration_list} + + return (200, FAKE_RESPONSE_HEADERS, migrations) + + # + # Server Groups + # + + def get_os_server_groups(self, **kw): + server_groups = [ + {"members": [], "metadata": {}, + "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b", + "policies": [], "name": "ig1"}, + {"members": [], "metadata": {}, + "id": "4473bb03-4370-4bfb-80d3-dc8cffc47d94", + "policies": ["anti-affinity"], "name": "ig2"}, + {"members": [], "metadata": {"key": "value"}, + "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", + "policies": [], "name": "ig3"}, + {"members": ["2dccb4a1-02b9-482a-aa23-5799490d6f5d"], + "metadata": {}, + "id": "4890bb03-7070-45fb-8453-d34556c87d94", + "policies": ["anti-affinity"], "name": "ig2"}] + + other_project_server_groups = [ + {"members": [], "metadata": {}, + "id": "11111111-1111-1111-1111-111111111111", + "policies": [], "name": "ig4"}, + {"members": [], "metadata": {}, + "id": "22222222-2222-2222-2222-222222222222", + "policies": ["anti-affinity"], "name": "ig5"}, + {"members": [], "metadata": {"key": "value"}, + "id": "31ab9bdb-55e1-4ac3-b094-97eeb1b65cc4", + "policies": [], "name": "ig6"}, + {"members": ["33333333-3333-3333-3333-333333333333"], + "metadata": {}, + "id": "44444444-4444-4444-4444-444444444444", + "policies": ["anti-affinity"], "name": "ig5"}] + + if kw.get("all_projects", False): + server_groups.extend(other_project_server_groups) + limit = int(kw.get("limit", 1000)) + offset = int(kw.get("offset", 0)) + server_groups = server_groups[offset:limit + 1] + return (200, {}, {"server_groups": server_groups}) + + def _return_server_group(self): + 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): + return self._return_server_group() + + def post_servers_1234_migrations_1_action(self, body): + return (202, {}, None) + + @api_versions.wraps(start_version="2.23") + def get_servers_1234_migrations_1(self, **kw): + migration = {"migration": { + "created_at": "2016-01-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1, + "server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe", + "source_compute": "compute1", + "source_node": "node1", + "status": "running", + "memory_total_bytes": 123456, + "memory_processed_bytes": 12345, + "memory_remaining_bytes": 120000, + "disk_total_bytes": 234567, + "disk_processed_bytes": 23456, + "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") + def get_servers_1234_migrations(self, **kw): + migrations = {'migrations': [ + { + "created_at": "2016-01-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1, + "server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe", + "source_compute": "compute1", + "source_node": "node1", + "status": "running", + "memory_total_bytes": 123456, + "memory_processed_bytes": 12345, + "memory_remaining_bytes": 120000, + "disk_total_bytes": 234567, + "disk_processed_bytes": 23456, + "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) + + def put_servers_1234_tags_tag1(self, **kw): + return (201, {}, None) + + def put_servers_1234_tags_tag2(self, **kw): + return (201, {}, None) + + def put_servers_1234_tags_tag3(self, **kw): + return (201, {}, None) + + def put_servers_1234_tags(self, **kw): + return (201, {}, None) + + def get_servers_1234_tags(self, **kw): + return (200, {}, {'tags': ['tag1', 'tag2']}) + + def delete_servers_1234_tags_tag(self, **kw): + return (204, {}, None) + + def delete_servers_1234_tags_tag1(self, **kw): + return (204, {}, None) + + def delete_servers_1234_tags_tag2(self, **kw): + return (204, {}, None) + + def delete_servers_1234_tags_tag3(self, **kw): + return (204, {}, None) + + def delete_servers_1234_tags(self, **kw): + return (204, {}, None) + + def post_os_assisted_volume_snapshots(self, **kw): + return (202, FAKE_RESPONSE_HEADERS, + {'snapshot': {'id': 'blah', 'volumeId': '1'}}) + + def delete_os_assisted_volume_snapshots_x(self, **kw): + return (202, FAKE_RESPONSE_HEADERS, {}) + + def post_os_server_external_events(self, **kw): + return (200, FAKE_RESPONSE_HEADERS, { + 'events': [ + {'name': 'test-event', + 'status': 'completed', + 'tag': 'tag', + 'server_uuid': 'fake-uuid1'}, + {'name': 'test-event', + 'status': 'completed', + 'tag': 'tag', + 'server_uuid': 'fake-uuid2'}]}) diff --git a/novaclient/tests/unit/v2/test_agents.py b/novaclient/tests/unit/v2/test_agents.py new file mode 100644 index 000000000..2e749e07a --- /dev/null +++ b/novaclient/tests/unit/v2/test_agents.py @@ -0,0 +1,111 @@ +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import agents 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 agents + + +class AgentsTest(utils.FixturedTestCase): + + data_fixture_class = data.Fixture + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def stub_hypervisors(self, hypervisor='kvm'): + get_os_agents = { + 'agents': [ + { + 'hypervisor': hypervisor, + 'os': 'win', + 'architecture': 'x86', + 'version': '7.0', + 'url': 'xxx://xxxx/xxx/xxx', + 'md5hash': 'add6bb58e139be103324d04d82d8f545', + 'id': 1 + }, + { + 'hypervisor': hypervisor, + 'os': 'linux', + 'architecture': 'x86', + 'version': '16.0', + 'url': 'xxx://xxxx/xxx/xxx1', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'id': 2 + }, + ] + } + + headers = {'Content-Type': 'application/json', + 'x-openstack-request-id': fakes.FAKE_REQUEST_ID} + self.requests_mock.get(self.data_fixture.url(), + json=get_os_agents, + headers=headers) + + def test_list_agents(self): + self.stub_hypervisors() + ags = self.cs.agents.list() + self.assert_called('GET', '/os-agents') + self.assert_request_id(ags, fakes.FAKE_REQUEST_ID_LIST) + for a in ags: + self.assertIsInstance(a, agents.Agent) + self.assertEqual('kvm', a.hypervisor) + + def test_list_agents_with_hypervisor(self): + self.stub_hypervisors('xen') + ags = self.cs.agents.list('xen') + self.assert_called('GET', '/os-agents?hypervisor=xen') + self.assert_request_id(ags, fakes.FAKE_REQUEST_ID_LIST) + for a in ags: + self.assertIsInstance(a, agents.Agent) + self.assertEqual('xen', a.hypervisor) + + def test_agents_create(self): + ag = self.cs.agents.create('win', 'x86', '7.0', + '/xxx/xxx/xxx', + 'add6bb58e139be103324d04d82d8f546', + 'xen') + self.assert_request_id(ag, fakes.FAKE_REQUEST_ID_LIST) + body = {'agent': {'url': '/xxx/xxx/xxx', + 'hypervisor': 'xen', + 'md5hash': 'add6bb58e139be103324d04d82d8f546', + 'version': '7.0', + 'architecture': 'x86', + 'os': 'win'}} + self.assert_called('POST', '/os-agents', body) + self.assertEqual(1, ag._info.copy()['id']) + + def test_agents_delete(self): + ret = self.cs.agents.delete('1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/os-agents/1') + + def _build_example_update_body(self): + return {"para": { + "url": "/yyy/yyyy/yyyy", + "version": "8.0", + "md5hash": "add6bb58e139be103324d04d82d8f546"}} + + def test_agents_modify(self): + ag = self.cs.agents.update('1', '8.0', + '/yyy/yyyy/yyyy', + 'add6bb58e139be103324d04d82d8f546') + self.assert_request_id(ag, fakes.FAKE_REQUEST_ID_LIST) + body = self._build_example_update_body() + self.assert_called('PUT', '/os-agents/1', body) + self.assertEqual(1, ag.id) diff --git a/novaclient/tests/unit/v2/test_aggregates.py b/novaclient/tests/unit/v2/test_aggregates.py new file mode 100644 index 000000000..4f3eecdf5 --- /dev/null +++ b/novaclient/tests/unit/v2/test_aggregates.py @@ -0,0 +1,203 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from 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): + + data_fixture_class = data.Fixture + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def test_list_aggregates(self): + result = self.cs.aggregates.list() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-aggregates') + for aggregate in result: + self.assertIsInstance(aggregate, aggregates.Aggregate) + + def test_create_aggregate(self): + body = {"aggregate": {"name": "test", "availability_zone": "nova1"}} + aggregate = self.cs.aggregates.create("test", "nova1") + self.assert_request_id(aggregate, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/os-aggregates', body) + self.assertIsInstance(aggregate, aggregates.Aggregate) + + def test_get(self): + aggregate = self.cs.aggregates.get("1") + self.assert_request_id(aggregate, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-aggregates/1') + self.assertIsInstance(aggregate, aggregates.Aggregate) + + aggregate2 = self.cs.aggregates.get(aggregate) + self.assert_request_id(aggregate2, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-aggregates/1') + self.assertIsInstance(aggregate2, aggregates.Aggregate) + + def test_get_details(self): + aggregate = self.cs.aggregates.get_details("1") + self.assert_request_id(aggregate, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-aggregates/1') + self.assertIsInstance(aggregate, aggregates.Aggregate) + + aggregate2 = self.cs.aggregates.get_details(aggregate) + self.assert_request_id(aggregate2, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-aggregates/1') + self.assertIsInstance(aggregate2, aggregates.Aggregate) + + def test_update(self): + aggregate = self.cs.aggregates.get("1") + values = {"name": "foo"} + body = {"aggregate": values} + + result1 = aggregate.update(values) + self.assert_request_id(result1, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/os-aggregates/1', body) + self.assertIsInstance(result1, aggregates.Aggregate) + + result2 = self.cs.aggregates.update(2, values) + self.assert_request_id(result2, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/os-aggregates/2', body) + self.assertIsInstance(result2, aggregates.Aggregate) + + def test_update_with_availability_zone(self): + aggregate = self.cs.aggregates.get("1") + values = {"name": "foo", "availability_zone": "new_zone"} + body = {"aggregate": values} + + result3 = self.cs.aggregates.update(aggregate, values) + self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/os-aggregates/1', body) + self.assertIsInstance(result3, aggregates.Aggregate) + + def test_add_host(self): + aggregate = self.cs.aggregates.get("1") + host = "host1" + body = {"add_host": {"host": "host1"}} + + result1 = aggregate.add_host(host) + self.assert_request_id(result1, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/os-aggregates/1/action', body) + self.assertIsInstance(result1, aggregates.Aggregate) + + result2 = self.cs.aggregates.add_host("2", host) + self.assert_request_id(result2, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/os-aggregates/2/action', body) + self.assertIsInstance(result2, aggregates.Aggregate) + + result3 = self.cs.aggregates.add_host(aggregate, host) + self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/os-aggregates/1/action', body) + self.assertIsInstance(result3, aggregates.Aggregate) + + def test_remove_host(self): + aggregate = self.cs.aggregates.get("1") + host = "host1" + body = {"remove_host": {"host": "host1"}} + + result1 = aggregate.remove_host(host) + self.assert_request_id(result1, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/os-aggregates/1/action', body) + self.assertIsInstance(result1, aggregates.Aggregate) + + result2 = self.cs.aggregates.remove_host("2", host) + self.assert_request_id(result2, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/os-aggregates/2/action', body) + self.assertIsInstance(result2, aggregates.Aggregate) + + result3 = self.cs.aggregates.remove_host(aggregate, host) + self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/os-aggregates/1/action', body) + self.assertIsInstance(result3, aggregates.Aggregate) + + def test_set_metadata(self): + aggregate = self.cs.aggregates.get("1") + metadata = {"foo": "bar"} + body = {"set_metadata": {"metadata": metadata}} + + result1 = aggregate.set_metadata(metadata) + self.assert_request_id(result1, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/os-aggregates/1/action', body) + self.assertIsInstance(result1, aggregates.Aggregate) + + result2 = self.cs.aggregates.set_metadata(2, metadata) + self.assert_request_id(result2, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/os-aggregates/2/action', body) + self.assertIsInstance(result2, aggregates.Aggregate) + + result3 = self.cs.aggregates.set_metadata(aggregate, metadata) + self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/os-aggregates/1/action', body) + self.assertIsInstance(result3, aggregates.Aggregate) + + def test_delete_aggregate(self): + aggregate = self.cs.aggregates.list()[0] + result1 = aggregate.delete() + self.assert_request_id(result1, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/os-aggregates/1') + + result2 = self.cs.aggregates.delete('1') + self.assert_request_id(result2, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/os-aggregates/1') + + 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_assisted_volume_snapshots.py b/novaclient/tests/unit/v2/test_assisted_volume_snapshots.py new file mode 100644 index 000000000..8fa4cb598 --- /dev/null +++ b/novaclient/tests/unit/v2/test_assisted_volume_snapshots.py @@ -0,0 +1,39 @@ +# 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 import api_versions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes + + +class AssistedVolumeSnapshotsTestCase(utils.TestCase): + def setUp(self): + super(AssistedVolumeSnapshotsTestCase, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) + + def test_create_snap(self): + vs = self.cs.assisted_volume_snapshots.create('1', {}) + self.assert_request_id(vs, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('POST', '/os-assisted-volume-snapshots') + + def test_delete_snap(self): + vs = self.cs.assisted_volume_snapshots.delete('x', {}) + self.assert_request_id(vs, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'DELETE', + '/os-assisted-volume-snapshots/x?delete_info={}') diff --git a/novaclient/tests/unit/v2/test_availability_zone.py b/novaclient/tests/unit/v2/test_availability_zone.py new file mode 100644 index 000000000..c3ac6f07e --- /dev/null +++ b/novaclient/tests/unit/v2/test_availability_zone.py @@ -0,0 +1,100 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient.tests.unit.fixture_data import availability_zones 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 availability_zones + + +class AvailabilityZoneTest(utils.FixturedTestCase): + # NOTE(cyeoh): import shell here so the V3 version of + # this class can inherit off the v3 version of shell + from novaclient.v2 import shell # noqa + + data_fixture_class = data.V1 + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def setUp(self): + super(AvailabilityZoneTest, self).setUp() + self.availability_zone_type = self._get_availability_zone_type() + + def _get_availability_zone_type(self): + return availability_zones.AvailabilityZone + + def _assertZone(self, zone, name, status): + self.assertEqual(zone.zoneName, name) + self.assertEqual(zone.zoneState, status) + + def test_list_availability_zone(self): + zones = self.cs.availability_zones.list(detailed=False) + self.assert_request_id(zones, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-availability-zone') + + for zone in zones: + self.assertIsInstance(zone, self.availability_zone_type) + + self.assertEqual(2, len(zones)) + + l0 = ['zone-1', 'available'] + l1 = ['zone-2', 'not available'] + + z0 = self.shell._treeizeAvailabilityZone(zones[0]) + z1 = self.shell._treeizeAvailabilityZone(zones[1]) + + self.assertEqual((1, 1), (len(z0), len(z1))) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z1[0], l1[0], l1[1]) + + def test_detail_availability_zone(self): + zones = self.cs.availability_zones.list(detailed=True) + self.assert_request_id(zones, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-availability-zone/detail') + + for zone in zones: + self.assertIsInstance(zone, self.availability_zone_type) + + self.assertEqual(3, len(zones)) + + 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]) + z2 = self.shell._treeizeAvailabilityZone(zones[2]) + + self.assertEqual((3, 5, 1), (len(z0), len(z1), len(z2))) + + self._assertZone(z0[0], l0[0], l0[1]) + self._assertZone(z0[1], l1[0], l1[1]) + self._assertZone(z0[2], l2[0], l2[1]) + self._assertZone(z1[0], l3[0], l3[1]) + self._assertZone(z1[1], l4[0], l4[1]) + self._assertZone(z1[2], l5[0], l5[1]) + self._assertZone(z1[3], l6[0], l6[1]) + self._assertZone(z1[4], l7[0], l7[1]) + self._assertZone(z2[0], l8[0], l8[1]) diff --git a/novaclient/tests/unit/v2/test_client.py b/novaclient/tests/unit/v2/test_client.py new file mode 100644 index 000000000..65de2d564 --- /dev/null +++ b/novaclient/tests/unit/v2/test_client.py @@ -0,0 +1,46 @@ +# 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 keystoneauth1 import session +from oslo_utils import uuidutils + +from novaclient import api_versions +from novaclient.tests.unit import utils +from novaclient.v2 import client + + +class ClientTest(utils.TestCase): + + def test_adapter_properties(self): + # sample of properties, there are many more + user_agent = uuidutils.generate_uuid(dashed=False) + endpoint_override = uuidutils.generate_uuid(dashed=False) + + s = session.Session() + c = client.Client(session=s, + api_version=api_versions.APIVersion("2.0"), + user_agent=user_agent, + endpoint_override=endpoint_override, + direct_use=False) + + self.assertEqual(user_agent, c.client.user_agent) + self.assertEqual(endpoint_override, c.client.endpoint_override) + + def test_passing_endpoint_type(self): + endpoint_type = uuidutils.generate_uuid(dashed=False) + + s = session.Session() + c = client.Client(session=s, + endpoint_type=endpoint_type, + direct_use=False) + + self.assertEqual(endpoint_type, c.client.interface) diff --git a/novaclient/tests/unit/v2/test_flavor_access.py b/novaclient/tests/unit/v2/test_flavor_access.py new file mode 100644 index 000000000..1c604f91e --- /dev/null +++ b/novaclient/tests/unit/v2/test_flavor_access.py @@ -0,0 +1,77 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import api_versions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import flavor_access + + +class FlavorAccessTest(utils.TestCase): + def setUp(self): + super(FlavorAccessTest, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) + + def test_list_access_by_flavor_private(self): + kwargs = {'flavor': self.cs.flavors.get(2)} + r = self.cs.flavor_access.list(**kwargs) + self.assert_request_id(r, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/2/os-flavor-access') + for a in r: + self.assertIsInstance(a, flavor_access.FlavorAccess) + + def test_add_tenant_access(self): + flavor = self.cs.flavors.get(2) + tenant = 'proj2' + r = self.cs.flavor_access.add_tenant_access(flavor, tenant) + self.assert_request_id(r, fakes.FAKE_REQUEST_ID_LIST) + + body = { + "addTenantAccess": { + "tenant": "proj2" + } + } + + self.cs.assert_called('POST', '/flavors/2/action', body) + for a in r: + self.assertIsInstance(a, flavor_access.FlavorAccess) + + def test_remove_tenant_access(self): + flavor = self.cs.flavors.get(2) + tenant = 'proj2' + r = self.cs.flavor_access.remove_tenant_access(flavor, tenant) + self.assert_request_id(r, fakes.FAKE_REQUEST_ID_LIST) + + body = { + "removeTenantAccess": { + "tenant": "proj2" + } + } + + self.cs.assert_called('POST', '/flavors/2/action', body) + for a in r: + self.assertIsInstance(a, flavor_access.FlavorAccess) + + def test_repr_flavor_access(self): + flavor = self.cs.flavors.get(2) + tenant = 'proj3' + r = self.cs.flavor_access.add_tenant_access(flavor, tenant) + + def get_expected(flavor_access): + return ("" % + (flavor_access.flavor_id, flavor_access.tenant_id)) + + for a in r: + self.assertEqual(get_expected(a), repr(a)) diff --git a/novaclient/tests/unit/v2/test_flavors.py b/novaclient/tests/unit/v2/test_flavors.py new file mode 100644 index 000000000..fccfc8fdb --- /dev/null +++ b/novaclient/tests/unit/v2/test_flavors.py @@ -0,0 +1,323 @@ +# Copyright (c) 2013, OpenStack +# 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 unittest import mock + +from novaclient import api_versions +from novaclient import base +from novaclient import exceptions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import flavors + + +class FlavorsTest(utils.TestCase): + def setUp(self): + super(FlavorsTest, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) + self.flavor_type = self._get_flavor_type() + + def _get_flavor_type(self): + return flavors.Flavor + + def test_list_flavors(self): + fl = self.cs.flavors.list() + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/detail') + for flavor in fl: + self.assertIsInstance(flavor, self.flavor_type) + + def test_list_flavors_undetailed(self): + fl = self.cs.flavors.list(detailed=False) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors') + for flavor in fl: + self.assertIsInstance(flavor, self.flavor_type) + + def test_list_flavors_with_marker_limit(self): + fl = self.cs.flavors.list(marker=1234, limit=4) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/detail?limit=4&marker=1234') + + def test_list_flavors_with_min_disk(self): + fl = self.cs.flavors.list(min_disk=20) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/detail?minDisk=20') + + def test_list_flavors_with_min_ram(self): + fl = self.cs.flavors.list(min_ram=512) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/detail?minRam=512') + + def test_list_flavors_with_sort_key_dir(self): + fl = self.cs.flavors.list(sort_key='id', sort_dir='asc') + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', + '/flavors/detail?sort_dir=asc&sort_key=id') + + def test_list_flavors_is_public_none(self): + fl = self.cs.flavors.list(is_public=None) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/detail?is_public=None') + for flavor in fl: + self.assertIsInstance(flavor, self.flavor_type) + + def test_list_flavors_is_public_false(self): + fl = self.cs.flavors.list(is_public=False) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/detail?is_public=False') + for flavor in fl: + self.assertIsInstance(flavor, self.flavor_type) + + def test_list_flavors_is_public_true(self): + fl = self.cs.flavors.list(is_public=True) + self.assert_request_id(fl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/detail') + for flavor in fl: + self.assertIsInstance(flavor, self.flavor_type) + + def test_get_flavor_details(self): + f = self.cs.flavors.get(1) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/1') + self.assertIsInstance(f, self.flavor_type) + self.assertEqual(256, f.ram) + self.assertEqual(10, f.disk) + self.assertEqual(10, f.ephemeral) + self.assertTrue(f.is_public) + + def test_get_flavor_details_alphanum_id(self): + f = self.cs.flavors.get('aa1') + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/aa1') + self.assertIsInstance(f, self.flavor_type) + self.assertEqual(128, f.ram) + self.assertEqual(0, f.disk) + self.assertEqual(0, f.ephemeral) + self.assertTrue(f.is_public) + + def test_get_flavor_details_diablo(self): + f = self.cs.flavors.get(3) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/flavors/3') + self.assertIsInstance(f, self.flavor_type) + self.assertEqual(256, f.ram) + self.assertEqual(10, f.disk) + self.assertEqual('N/A', f.ephemeral) + self.assertEqual('N/A', f.is_public) + + 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 MiB Server', f.name) + + f = self.cs.flavors.find(disk=0) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) + self.assertEqual('128 MiB Server', f.name) + + self.assertRaises(exceptions.NotFound, self.cs.flavors.find, + disk=12345) + + @staticmethod + def _create_body(name, ram, vcpus, disk, ephemeral, id, swap, + rxtx_factor, is_public): + return { + "flavor": { + "name": name, + "ram": ram, + "vcpus": vcpus, + "disk": disk, + "OS-FLV-EXT-DATA:ephemeral": ephemeral, + "id": id, + "swap": swap, + "rxtx_factor": rxtx_factor, + "os-flavor-access:is_public": is_public, + } + } + + def test_create(self): + f = self.cs.flavors.create("flavorcreate", 512, 1, 10, 1234, + ephemeral=10, is_public=False) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) + + body = self._create_body("flavorcreate", 512, 1, 10, 10, 1234, 0, 1.0, + False) + + self.cs.assert_called('POST', '/flavors', body) + self.assertIsInstance(f, self.flavor_type) + + def test_create_with_id_as_string(self): + flavor_id = 'foobar' + f = self.cs.flavors.create("flavorcreate", 512, + 1, 10, flavor_id, ephemeral=10, + is_public=False) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) + + body = self._create_body("flavorcreate", 512, 1, 10, 10, flavor_id, 0, + 1.0, False) + + self.cs.assert_called('POST', '/flavors', body) + self.assertIsInstance(f, self.flavor_type) + + def test_create_ephemeral_ispublic_defaults(self): + f = self.cs.flavors.create("flavorcreate", 512, 1, 10, 1234) + self.assert_request_id(f, fakes.FAKE_REQUEST_ID_LIST) + + body = self._create_body("flavorcreate", 512, 1, 10, 0, 1234, 0, + 1.0, True) + + self.cs.assert_called('POST', '/flavors', body) + self.assertIsInstance(f, self.flavor_type) + + def test_invalid_parameters_create(self): + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", "invalid", 1, 10, 1234, swap=0, + ephemeral=0, rxtx_factor=1.0, is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, "invalid", 10, 1234, swap=0, + ephemeral=0, rxtx_factor=1.0, is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, 1, "invalid", 1234, swap=0, + ephemeral=0, rxtx_factor=1.0, is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, 1, 10, 1234, swap="invalid", + ephemeral=0, rxtx_factor=1.0, is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, 1, 10, 1234, swap=0, + ephemeral="invalid", rxtx_factor=1.0, is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, 1, 10, 1234, swap=0, + ephemeral=0, rxtx_factor="invalid", is_public=True) + self.assertRaises(exceptions.CommandError, self.cs.flavors.create, + "flavorcreate", 512, 1, 10, 1234, swap=0, + ephemeral=0, rxtx_factor=1.0, is_public='invalid') + + def test_delete(self): + ret = self.cs.flavors.delete("flavordelete") + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('DELETE', '/flavors/flavordelete') + + def test_delete_with_flavor_instance(self): + f = self.cs.flavors.get(2) + ret = self.cs.flavors.delete(f) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('DELETE', '/flavors/2') + + def test_delete_with_flavor_instance_method(self): + f = self.cs.flavors.get(2) + ret = f.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('DELETE', '/flavors/2') + + def test_set_keys(self): + f = self.cs.flavors.get(1) + fk = f.set_keys({'k1': 'v1'}) + self.assert_request_id(fk, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('POST', '/flavors/1/os-extra_specs', + {"extra_specs": {'k1': 'v1'}}) + + def test_set_with_valid_keys(self): + valid_keys = ['key4', 'month.price', 'I-Am:AK-ey.44-', + 'key with spaces and _'] + + f = self.cs.flavors.get(4) + for key in valid_keys: + fk = f.set_keys({key: 'v4'}) + self.assert_request_id(fk, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('POST', '/flavors/4/os-extra_specs', + {"extra_specs": {key: 'v4'}}) + + def test_set_with_invalid_keys(self): + invalid_keys = ['/1', '?1', '%1', '<', '>'] + + f = self.cs.flavors.get(1) + for key in invalid_keys: + self.assertRaises(exceptions.CommandError, f.set_keys, {key: 'v1'}) + + @mock.patch.object(flavors.FlavorManager, '_delete') + def test_unset_keys(self, mock_delete): + f = self.cs.flavors.get(1) + keys = ['k1', 'k2'] + mock_delete.return_value = base.TupleWithMeta( + (), fakes.FAKE_REQUEST_ID_LIST) + fu = f.unset_keys(keys) + self.assert_request_id(fu, fakes.FAKE_REQUEST_ID_LIST) + mock_delete.assert_has_calls([ + 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_hypervisors.py b/novaclient/tests/unit/v2/test_hypervisors.py new file mode 100644 index 000000000..be48914fc --- /dev/null +++ b/novaclient/tests/unit/v2/test_hypervisors.py @@ -0,0 +1,335 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from 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 +from novaclient.tests.unit.v2 import fakes + + +class HypervisorsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + + def compare_to_expected(self, expected, hyper): + for key, value in expected.items(): + self.assertEqual(getattr(hyper, key), value) + + def test_hypervisor_index(self): + expected = [ + dict(id=self.data_fixture.hyper_id_1, + hypervisor_hostname='hyper1'), + dict(id=self.data_fixture.hyper_id_2, + hypervisor_hostname='hyper2')] + + result = self.cs.hypervisors.list(False) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-hypervisors') + + for idx, hyper in enumerate(result): + self.compare_to_expected(expected[idx], hyper) + + def test_hypervisor_detail(self): + expected = [ + dict(id=self.data_fixture.hyper_id_1, + service=dict(id=self.data_fixture.service_id_1, + host='compute1'), + vcpus=4, + memory_mb=10 * 1024, + local_gb=250, + vcpus_used=2, + memory_mb_used=5 * 1024, + local_gb_used=125, + hypervisor_type="xen", + hypervisor_version=3, + hypervisor_hostname="hyper1", + free_ram_mb=5 * 1024, + free_disk_gb=125, + current_workload=2, + running_vms=2, + cpu_info='cpu_info', + 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"), + vcpus=4, + memory_mb=10 * 1024, + local_gb=250, + vcpus_used=2, + memory_mb_used=5 * 1024, + local_gb_used=125, + hypervisor_type="xen", + hypervisor_version=3, + hypervisor_hostname="hyper2", + free_ram_mb=5 * 1024, + free_disk_gb=125, + current_workload=2, + running_vms=2, + cpu_info='cpu_info', + 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) + self.assert_called('GET', '/os-hypervisors/detail') + + for idx, hyper in enumerate(result): + self.compare_to_expected(expected[idx], hyper) + + def test_hypervisor_search(self): + expected = [ + dict(id=self.data_fixture.hyper_id_1, + hypervisor_hostname='hyper1', + state='up', + status='enabled'), + dict(id=self.data_fixture.hyper_id_2, + hypervisor_hostname='hyper2', + state='up', + status='enabled')] + + result = self.cs.hypervisors.search('hyper') + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + if self.cs.api_version >= api_versions.APIVersion('2.53'): + self.assert_called( + 'GET', '/os-hypervisors?hypervisor_hostname_pattern=hyper') + else: + self.assert_called('GET', '/os-hypervisors/hyper/search') + + 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')]), + ] + + result = self.cs.hypervisors.search('hyper', True) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + if self.cs.api_version >= api_versions.APIVersion('2.53'): + self.assert_called( + 'GET', '/os-hypervisors?hypervisor_hostname_pattern=hyper&' + 'with_servers=True') + else: + self.assert_called('GET', '/os-hypervisors/hyper/servers') + + for idx, hyper in enumerate(result): + self.compare_to_expected(expected[idx], hyper) + + def test_hypervisor_get(self): + expected = dict( + id=self.data_fixture.hyper_id_1, + service=dict(id=self.data_fixture.service_id_1, host='compute1'), + vcpus=4, + memory_mb=10 * 1024, + local_gb=250, + vcpus_used=2, + memory_mb_used=5 * 1024, + local_gb_used=125, + hypervisor_type="xen", + hypervisor_version=3, + hypervisor_hostname="hyper1", + free_ram_mb=5 * 1024, + free_disk_gb=125, + current_workload=2, + running_vms=2, + cpu_info='cpu_info', + 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) + self.assert_called( + 'GET', '/os-hypervisors/%s' % self.data_fixture.hyper_id_1) + + self.compare_to_expected(expected, result) + + def test_hypervisor_uptime(self): + expected = dict( + 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/uptime' % self.data_fixture.hyper_id_1) + + self.compare_to_expected(expected, result) + + def test_hypervisor_statistics(self): + expected = dict( + count=2, + vcpus=8, + memory_mb=20 * 1024, + local_gb=500, + vcpus_used=4, + memory_mb_used=10 * 1024, + local_gb_used=250, + free_ram_mb=10 * 1024, + free_disk_gb=250, + current_workload=4, + running_vms=4, + disk_available_least=200, + ) + + result = self.cs.hypervisors.statistics() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-hypervisors/statistics') + + self.compare_to_expected(expected, result) + + # Test for Bug #1370415, the line below used to raise AttributeError + self.assertEqual("", + result.__repr__()) + + +class HypervisorsV233Test(HypervisorsTest): + def setUp(self): + super(HypervisorsV233Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.33") + + def test_use_limit_marker_params(self): + params = {'limit': '10', 'marker': 'fake-marker'} + self.cs.hypervisors.list(**params) + for k, v in params.items(): + self.assertEqual([v], self.requests_mock.last_request.qs[k]) + + +class HypervisorsV253Test(HypervisorsV233Test): + """Tests the os-hypervisors 2.53 API bindings.""" + data_fixture_class = data.V253 + + def setUp(self): + 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 new file mode 100644 index 000000000..5fa448e01 --- /dev/null +++ b/novaclient/tests/unit/v2/test_images.py @@ -0,0 +1,37 @@ +# +# 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 unittest import mock + +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import images as data +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import images + + +class ImagesTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + + @mock.patch('novaclient.base.Manager.alternate_service_type') + def test_list_images(self, mock_alternate_service_type): + il = self.cs.glance.list() + self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/v2/images') + for i in il: + self.assertIsInstance(i, images.Image) + self.assertEqual(2, len(il)) + mock_alternate_service_type.assert_called_once_with( + 'image', allowed_types=('image',)) diff --git a/novaclient/tests/unit/v2/test_instance_actions.py b/novaclient/tests/unit/v2/test_instance_actions.py new file mode 100644 index 000000000..e2da9d03a --- /dev/null +++ b/novaclient/tests/unit/v2/test_instance_actions.py @@ -0,0 +1,83 @@ +# 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 +from novaclient.v2 import instance_action + + +class InstanceActionExtensionTests(utils.TestCase): + def setUp(self): + super(InstanceActionExtensionTests, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) + + def test_list_instance_actions(self): + server_uuid = '1234' + ial = self.cs.instance_action.list(server_uuid) + self.assert_request_id(ial, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'GET', '/servers/%s/os-instance-actions' % + server_uuid) + + def test_get_instance_action(self): + server_uuid = '1234' + request_id = 'req-abcde12345' + ia = self.cs.instance_action.get(server_uuid, request_id) + self.assert_request_id(ia, fakes.FAKE_REQUEST_ID_LIST) + 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 new file mode 100644 index 000000000..7e16438d8 --- /dev/null +++ b/novaclient/tests/unit/v2/test_keypairs.py @@ -0,0 +1,154 @@ +# +# 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.fixture_data import client +from novaclient.tests.unit.fixture_data import keypairs as data +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import keypairs + + +class KeypairsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + + def setUp(self): + super(KeypairsTest, self).setUp() + self.keypair_type = self._get_keypair_type() + self.keypair_prefix = self._get_keypair_prefix() + + def _get_keypair_type(self): + return keypairs.Keypair + + def _get_keypair_prefix(self): + return keypairs.KeypairManager.keypair_prefix + + def test_get_keypair(self): + kp = self.cs.keypairs.get('test') + self.assert_request_id(kp, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/%s/test' % self.keypair_prefix) + self.assertIsInstance(kp, keypairs.Keypair) + self.assertEqual('test', kp.name) + + def test_list_keypairs(self): + kps = self.cs.keypairs.list() + self.assert_request_id(kps, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/%s' % self.keypair_prefix) + for kp in kps: + self.assertIsInstance(kp, keypairs.Keypair) + + def test_delete_keypair(self): + kp = self.cs.keypairs.list()[0] + ret = kp.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + ret = self.cs.keypairs.delete('test') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + ret = self.cs.keypairs.delete(kp) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/%s/test' % self.keypair_prefix) + + +class KeypairsV2TestCase(KeypairsTest): + def setUp(self): + super(KeypairsV2TestCase, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.0") + + def test_create_keypair(self): + name = "foo" + kp = self.cs.keypairs.create(name) + self.assert_request_id(kp, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name}}) + self.assertIsInstance(kp, keypairs.Keypair) + + def test_import_keypair(self): + name = "foo" + pub_key = "fake-public-key" + kp = self.cs.keypairs.create(name, pub_key) + self.assert_request_id(kp, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name, + 'public_key': pub_key}}) + self.assertIsInstance(kp, keypairs.Keypair) + + +class KeypairsV22TestCase(KeypairsTest): + def setUp(self): + super(KeypairsV22TestCase, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.2") + + def test_create_keypair(self): + name = "foo" + key_type = "some_type" + kp = self.cs.keypairs.create(name, 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, + 'type': key_type}}) + self.assertIsInstance(kp, keypairs.Keypair) + + def test_import_keypair(self): + name = "foo" + pub_key = "fake-public-key" + kp = self.cs.keypairs.create(name, pub_key) + self.assert_request_id(kp, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/%s' % self.keypair_prefix, + body={'keypair': {'name': name, + 'public_key': pub_key, + 'type': 'ssh'}}) + self.assertIsInstance(kp, keypairs.Keypair) + + +class KeypairsV35TestCase(KeypairsTest): + def setUp(self): + super(KeypairsV35TestCase, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.35") + + def test_list_keypairs(self): + kps = self.cs.keypairs.list(user_id='test_user', marker='test_kp', + limit=3) + self.assert_request_id(kps, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', + '/%s?limit=3&marker=test_kp&user_id=test_user' + % 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 new file mode 100644 index 000000000..1abcd1a5d --- /dev/null +++ b/novaclient/tests/unit/v2/test_limits.py @@ -0,0 +1,109 @@ +# +# 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.fixture_data import client +from novaclient.tests.unit.fixture_data import limits as data +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import limits + + +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() + self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/limits') + self.assertIsInstance(obj, limits.Limits) + + def test_get_limits_for_a_tenant(self): + obj = self.cs.limits.get(tenant_id=1234) + self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/limits?tenant_id=1234') + self.assertIsInstance(obj, limits.Limits) + + def test_absolute_limits_reserved(self): + obj = self.cs.limits.get(reserved=True) + self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST) + + expected = [ + limits.AbsoluteLimit("maxTotalRAMSize", 51200), + 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) + self.assertEqual(len(abs_limits), len(expected)) + + for limit in abs_limits: + self.assertIn(limit, expected) + + def test_rate_absolute_limits(self): + obj = self.cs.limits.get() + self.assert_request_id(obj, fakes.FAKE_REQUEST_ID_LIST) + + expected = ( + limits.RateLimit('POST', '*', '.*', 10, 2, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('PUT', '*', '.*', 10, 2, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('DELETE', '*', '.*', 100, 100, 'MINUTE', + '2011-12-15T22:42:45Z'), + limits.RateLimit('POST', '*/servers', '^/servers', 25, 24, 'DAY', + '2011-12-15T22:42:45Z'), + ) + + rate_limits = list(obj.rate) + self.assertEqual(len(rate_limits), len(expected)) + + for limit in rate_limits: + self.assertIn(limit, expected) + + expected = [ + limits.AbsoluteLimit("maxTotalRAMSize", 51200), + 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_migrations.py b/novaclient/tests/unit/v2/test_migrations.py new file mode 100644 index 000000000..0b5ebb84e --- /dev/null +++ b/novaclient/tests/unit/v2/test_migrations.py @@ -0,0 +1,167 @@ +# 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 +from novaclient.v2 import migrations + + +class MigrationsTest(utils.TestCase): + def setUp(self): + super(MigrationsTest, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) + + 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.assertRaises(AttributeError, getattr, m, "migration_type") + self.assertRaises(AttributeError, getattr, m, "uuid") + + def test_list_migrations_with_filters(self): + ml = self.cs.migrations.list('host1', 'finished') + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + '/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', 'instance_id_456') + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + ('/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 new file mode 100644 index 000000000..3becb6323 --- /dev/null +++ b/novaclient/tests/unit/v2/test_quota_classes.py @@ -0,0 +1,123 @@ +# 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 api_versions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes + + +class QuotaClassSetsTest(utils.TestCase): + def setUp(self): + super(QuotaClassSetsTest, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) + + def test_class_quotas_get(self): + class_name = 'test' + q = self.cs.quota_classes.get(class_name) + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) + return q + + def test_update_quota(self): + q = self.cs.quota_classes.get('test') + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) + q.update(cores=2) + self.cs.assert_called('PUT', '/os-quota-class-sets/test') + return q + + def test_refresh_quota(self): + q = self.cs.quota_classes.get('test') + q2 = self.cs.quota_classes.get('test') + self.assertEqual(q.cores, q2.cores) + q2.cores = 0 + self.assertNotEqual(q.cores, q2.cores) + q2.get() + self.assertEqual(q.cores, q2.cores) + + +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(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 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 + # response. + for valid_resource in ('server_groups', 'server_group_members'): + self.assertTrue(hasattr(q, valid_resource), + '%s should be in %s' % (invalid_resource, q)) + + def test_update_quota(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_update_quota() + 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 + # response. + for valid_resource in ('server_groups', 'server_group_members'): + self.assertTrue(hasattr(q, valid_resource), + '%s should be in %s' % (invalid_resource, q)) + + def test_update_quota_invalid_resources(self): + """Tests trying to update quota class values for invalid resources. + + This will fail with TypeError because the network-related resource + kwargs aren't defined. + """ + q = self.cs.quota_classes.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) + 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 new file mode 100644 index 000000000..67a0bc3df --- /dev/null +++ b/novaclient/tests/unit/v2/test_quotas.py @@ -0,0 +1,121 @@ +# 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 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 +from novaclient.tests.unit.v2 import fakes + + +class QuotaSetsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + + def test_tenant_quotas_get(self): + tenant_id = 'test' + 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' + user_id = 'fake_user' + q = self.cs.quotas.get(tenant_id, user_id=user_id) + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) + url = '/os-quota-sets/%s?user_id=%s' % (tenant_id, user_id) + self.assert_called('GET', url) + + def test_tenant_quotas_get_detail(self): + tenant_id = 'test' + q = self.cs.quotas.get(tenant_id, detail=True) + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-quota-sets/%s/detail' % tenant_id) + + def test_user_quotas_get_detail(self): + tenant_id = 'test' + user_id = 'fake_user' + q = self.cs.quotas.get(tenant_id, user_id=user_id, detail=True) + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) + url = '/os-quota-sets/%s/detail?user_id=%s' % (tenant_id, user_id) + self.assert_called('GET', url) + + def test_tenant_quotas_defaults(self): + tenant_id = '97f4c221bff44578b0300df4ef119353' + q = self.cs.quotas.defaults(tenant_id) + self.assert_request_id(q, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) + + def test_force_update_quota(self): + q = self.cs.quotas.get('97f4c221bff44578b0300df4ef119353') + qu = q.update(cores=2, force=True) + self.assert_request_id(qu, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'force': True, 'cores': 2}}) + return q + + def test_quotas_delete(self): + tenant_id = 'test' + ret = self.cs.quotas.delete(tenant_id) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/os-quota-sets/%s' % tenant_id) + + def test_user_quotas_delete(self): + tenant_id = 'test' + user_id = 'fake_user' + ret = self.cs.quotas.delete(tenant_id, user_id=user_id) + 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_external_events.py b/novaclient/tests/unit/v2/test_server_external_events.py new file mode 100644 index 000000000..4b640750c --- /dev/null +++ b/novaclient/tests/unit/v2/test_server_external_events.py @@ -0,0 +1,41 @@ +# 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 import api_versions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes + + +class ServerExternalEventsTestCase(utils.TestCase): + def setUp(self): + super(ServerExternalEventsTestCase, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.1")) + + def test_external_event(self): + events = [{'server_uuid': 'fake-uuid1', + 'name': 'test-event', + 'status': 'completed', + 'tag': 'tag'}, + {'server_uuid': 'fake-uuid2', + 'name': 'test-event', + 'status': 'completed', + 'tag': 'tag'}] + result = self.cs.server_external_events.create(events) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assertEqual(events, result) + self.cs.assert_called('POST', '/os-server-external-events') diff --git a/novaclient/tests/unit/v2/test_server_groups.py b/novaclient/tests/unit/v2/test_server_groups.py new file mode 100644 index 000000000..40af1a13e --- /dev/null +++ b/novaclient/tests/unit/v2/test_server_groups.py @@ -0,0 +1,142 @@ +# Copyright (c) 2014 VMware, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +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 +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import server_groups + + +class ServerGroupsTest(utils.FixturedTestCase): + + client_fixture_class = client.V1 + data_fixture_class = data.Fixture + + def test_list_server_groups(self): + result = self.cs.server_groups.list() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-server-groups') + self.assertEqual(4, len(result)) + for server_group in result: + self.assertIsInstance(server_group, + server_groups.ServerGroup) + + def test_list_server_groups_with_all_projects(self): + result = self.cs.server_groups.list(all_projects=True) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-server-groups?all_projects=True') + self.assertEqual(8, len(result)) + for server_group in result: + self.assertIsInstance(server_group, + server_groups.ServerGroup) + + def test_list_server_groups_with_limit_and_offset(self): + all_groups = self.cs.server_groups.list() + result = self.cs.server_groups.list(limit=2, offset=1) + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-server-groups?limit=2&offset=1') + self.assertEqual(2, len(result)) + for server_group in result: + self.assertIsInstance(server_group, + server_groups.ServerGroup) + self.assertEqual(all_groups[1:3], result) + + def test_create_server_group(self): + kwargs = {'name': 'ig1', + 'policies': ['anti-affinity']} + server_group = self.cs.server_groups.create(**kwargs) + self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST) + body = {'server_group': kwargs} + self.assert_called('POST', '/os-server-groups', body) + self.assertIsInstance(server_group, + server_groups.ServerGroup) + + def test_get_server_group(self): + id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' + server_group = self.cs.server_groups.get(id) + self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-server-groups/%s' % id) + self.assertIsInstance(server_group, + server_groups.ServerGroup) + + def test_delete_server_group(self): + id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' + ret = self.cs.server_groups.delete(id) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/os-server-groups/%s' % id) + + def test_delete_server_group_object(self): + id = '2cbd51f4-fafe-4cdb-801b-cf913a6f288b' + server_group = self.cs.server_groups.get(id) + ret = server_group.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/os-server-groups/%s' % id) + + def test_find_server_groups_by_name(self): + expected_name = 'ig1' + kwargs = {self.cs.server_groups.resource_class.NAME_ATTR: + expected_name} + server_group = self.cs.server_groups.find(**kwargs) + self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/os-server-groups') + self.assertIsInstance(server_group, server_groups.ServerGroup) + actual_name = getattr(server_group, + self.cs.server_groups.resource_class.NAME_ATTR) + self.assertEqual(expected_name, actual_name) + + def test_find_no_existing_server_groups_by_name(self): + expected_name = 'no-exist' + kwargs = {self.cs.server_groups.resource_class.NAME_ATTR: + expected_name} + self.assertRaises(exceptions.NotFound, + 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_server_migrations.py b/novaclient/tests/unit/v2/test_server_migrations.py new file mode 100644 index 000000000..d0d8cdcc7 --- /dev/null +++ b/novaclient/tests/unit/v2/test_server_migrations.py @@ -0,0 +1,92 @@ +# Copyright 2016 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 api_versions +from novaclient import base +from novaclient.tests.unit.fixture_data import client +from novaclient.tests.unit.fixture_data import server_migrations as data +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import server_migrations + + +class ServerMigrationsTest(utils.FixturedTestCase): + client_fixture_class = client.V1 + data_fixture_class = data.Fixture + + def setUp(self): + super(ServerMigrationsTest, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.22") + + def test_live_migration_force_complete(self): + body = {'force_complete': None} + self.cs.server_migrations.live_migrate_force_complete(1234, 1) + self.assert_called('POST', '/servers/1234/migrations/1/action', body) + + +class ServerMigrationsTestV223(ServerMigrationsTest): + + migration = { + "created_at": "2016-01-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1, + "server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe", + "source_compute": "compute1", + "source_node": "node1", + "status": "running", + "memory_total_bytes": 123456, + "memory_processed_bytes": 12345, + "memory_remaining_bytes": 120000, + "disk_total_bytes": 234567, + "disk_processed_bytes": 23456, + "disk_remaining_bytes": 230000, + "updated_at": "2016-01-29T13:42:02.000000" + } + + def setUp(self): + super(ServerMigrationsTestV223, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.23") + + def test_list_migrations(self): + ml = self.cs.server_migrations.list(1234) + + self.assertIsInstance(ml, base.ListWithMeta) + self.assert_request_id(ml, fakes.FAKE_REQUEST_ID_LIST) + for k in self.migration: + self.assertEqual(self.migration[k], getattr(ml[0], k)) + + self.assert_called('GET', '/servers/1234/migrations') + + def test_get_migration(self): + migration = self.cs.server_migrations.get(1234, 1) + + self.assertIsInstance(migration, server_migrations.ServerMigration) + for k in migration._info: + self.assertEqual(self.migration[k], migration._info[k]) + self.assert_request_id(migration, fakes.FAKE_REQUEST_ID_LIST) + + self.assert_called('GET', '/servers/1234/migrations/1') + + +class ServerMigrationsTestV224(ServerMigrationsTest): + def setUp(self): + super(ServerMigrationsTestV224, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.24") + + def test_live_migration_abort(self): + self.cs.server_migrations.live_migration_abort(1234, 1) + self.assert_called('DELETE', '/servers/1234/migrations/1') diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py new file mode 100644 index 000000000..36eded7a8 --- /dev/null +++ b/novaclient/tests/unit/v2/test_servers.py @@ -0,0 +1,2107 @@ +# -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import base64 +import io +import os +import tempfile +from unittest 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 floatingips +from novaclient.tests.unit.fixture_data import servers as data +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import servers + + +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) + + def _get_server_create_default_nics(self): + """Callback for default nics kwarg when creating a server. + """ + return None + + def test_list_servers(self): + sl = self.cs.servers.list() + 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_filter_servers_unicode(self): + 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: + self.assertIsInstance(s, servers.Server) + + def test_list_all_servers(self): + # use marker just to identify this call in fixtures + sl = self.cs.servers.list(limit=-1, marker=1234) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) + + self.assertEqual(2, len(sl)) + + self.assertEqual(self.requests_mock.request_history[-2].method, 'GET') + self.assertEqual(self.requests_mock.request_history[-2].path_url, + '/servers/detail?marker=1234') + self.assert_called('GET', '/servers/detail?marker=5678') + + 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) + self.assert_called('GET', '/servers') + for s in sl: + self.assertIsInstance(s, servers.Server) + + def test_list_servers_with_marker_limit(self): + sl = self.cs.servers.list(marker=1234, limit=2) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/detail?limit=2&marker=1234') + for s in sl: + self.assertIsInstance(s, servers.Server) + + def test_list_servers_sort_single(self): + sl = self.cs.servers.list(sort_keys=['display_name'], + sort_dirs=['asc']) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'GET', + '/servers/detail?sort_dir=asc&sort_key=display_name') + for s in sl: + self.assertIsInstance(s, servers.Server) + + def test_list_servers_sort_multiple(self): + sl = self.cs.servers.list(sort_keys=['display_name', 'id'], + sort_dirs=['asc', 'desc']) + self.assert_request_id(sl, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'GET', + ('/servers/detail?sort_dir=asc&sort_dir=desc&' + 'sort_key=display_name&sort_key=id')) + for s in sl: + self.assertIsInstance(s, servers.Server) + + def test_get_server_details(self): + s = self.cs.servers.get(1234) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234') + self.assertIsInstance(s, servers.Server) + self.assertEqual(1234, s.id) + self.assertEqual('BUILD', s.status) + + def test_get_server_promote_details(self): + s1 = self.cs.servers.list(detailed=False)[0] + s2 = self.cs.servers.list(detailed=True)[0] + self.assertNotEqual(s1._info, s2._info) + s1.get() + 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, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + 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_boot_from_volume_with_nics(self): + old_boot = self.cs.servers._boot + + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'v4-fixed-ip': '10.0.0.7'}] + bdm = {"volume_size": "1", + "volume_id": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": "0", + "device_name": "vda"} + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['block_device_mapping'], bdm) + self.assertEqual(boot_kwargs['nics'], nics) + return old_boot(url, key, *boot_args, **boot_kwargs) + + @mock.patch.object(self.cs.servers, '_boot', wrapped_boot) + def test_create_server_from_volume(): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + block_device_mapping=bdm, + nics=nics + ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + test_create_server_from_volume() + + def test_create_server_boot_from_volume_bdm_v2(self): + old_boot = self.cs.servers._boot + + bdm = [{"volume_size": "1", + "volume_id": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": "0", + "device_name": "vda"}] + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['block_device_mapping_v2'], bdm) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(self.cs.servers, '_boot', wrapped_boot): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + block_device_mapping_v2=bdm, + nics=self._get_server_create_default_nics() + ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + def test_create_server_boot_with_nics_ipv6(self): + old_boot = self.cs.servers._boot + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'v6-fixed-ip': '2001:db9:0:1::10'}] + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['nics'], nics) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(self.cs.servers, '_boot', wrapped_boot): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=nics + ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + def test_create_server_boot_with_address(self): + old_boot = self.cs.servers._boot + access_ip_v6 = '::1' + access_ip_v4 = '10.10.10.10' + + def wrapped_boot(url, key, *boot_args, **boot_kwargs): + self.assertEqual(boot_kwargs['access_ip_v6'], access_ip_v6) + self.assertEqual(boot_kwargs['access_ip_v4'], access_ip_v4) + return old_boot(url, key, *boot_args, **boot_kwargs) + + with mock.patch.object(self.cs.servers, '_boot', wrapped_boot): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + access_ip_v6=access_ip_v6, + access_ip_v4=access_ip_v4, + nics=self._get_server_create_default_nics() + ) + 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_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=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='こんにちは', + key_name="fakekey", + 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, + flavor=1, + meta={'foo': 'bar'}, + userdata='こんにちは', + key_name="fakekey", + 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_admin_pass(self): + test_password = "test-pass" + test_key = "fakekey" + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + admin_pass=test_password, + key_name=test_key, + nics=self._get_server_create_default_nics() + ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + body = self.requests_mock.last_request.json() + 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) + bin_file.flush() + bin_file.seek(0) + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata=bin_file, + key_name="fakekey", + 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) + # verify userdata matches original + body = self.requests_mock.last_request.json() + transferred_data = body['server']['user_data'] + transferred_data = base64.b64decode(transferred_data) + self.assertEqual(original_data, transferred_data) + + def _create_disk_config(self, disk_config): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + disk_config=disk_config, + nics=self._get_server_create_default_nics(), + ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + # verify disk config param was used in the request: + server = self.requests_mock.last_request.json()['server'] + self.assertIn('OS-DCF:diskConfig', server) + self.assertEqual(disk_config, server['OS-DCF:diskConfig']) + + def test_create_server_disk_config_auto(self): + self._create_disk_config('AUTO') + + def test_create_server_disk_config_manual(self): + self._create_disk_config('MANUAL') + + def test_create_server_return_reservation_id(self): + s = self.cs.servers.create( + name="My server", + image=1, + flavor=1, + reservation_id=True, + nics=self._get_server_create_default_nics() + ) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers') + + def test_update_server(self): + s = self.cs.servers.get(1234) + + # Update via instance + s.update(name='hi') + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/servers/1234') + s.update(name='hi') + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/servers/1234') + + # Silly, but not an error + s.update() + + # Update via manager + self.cs.servers.update(s, name='hi') + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/servers/1234') + + def test_delete_server(self): + s = self.cs.servers.get(1234) + ret = s.delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234') + ret = self.cs.servers.delete(1234) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234') + ret = self.cs.servers.delete(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234') + + def test_delete_server_meta(self): + ret = self.cs.servers.delete_meta(1234, ['test_key']) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234/metadata/test_key') + + def test_set_server_meta(self): + m = self.cs.servers.set_meta(1234, {'test_key': 'test_value'}) + self.assert_request_id(m, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/metadata', + {'metadata': {'test_key': 'test_value'}}) + + def test_set_server_meta_item(self): + m = self.cs.servers.set_meta_item(1234, 'test_key', 'test_value') + self.assert_request_id(m, fakes.FAKE_REQUEST_ID_LIST) + 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 MiB 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]) + + def test_reboot_server(self): + s = self.cs.servers.get(1234) + ret = s.reboot() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.reboot(s, reboot_type='HARD') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_rebuild_server(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image=1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.rebuild(s, image=1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = s.rebuild(image=1, password='5678') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.rebuild(s, image=1, password='5678') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def _rebuild_resize_disk_config(self, disk_config, operation="rebuild"): + s = self.cs.servers.get(1234) + + if operation == "rebuild": + ret = s.rebuild(image=1, disk_config=disk_config) + elif operation == "resize": + ret = s.resize(flavor=1, disk_config=disk_config) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + # verify disk config param was used in the request: + d = self.requests_mock.last_request.json()[operation] + self.assertIn('OS-DCF:diskConfig', d) + self.assertEqual(disk_config, d['OS-DCF:diskConfig']) + + def test_rebuild_server_disk_config_auto(self): + self._rebuild_resize_disk_config('AUTO') + + def test_rebuild_server_disk_config_manual(self): + self._rebuild_resize_disk_config('MANUAL') + + def test_rebuild_server_preserve_ephemeral(self): + s = self.cs.servers.get(1234) + ret = s.rebuild(image=1, preserve_ephemeral=True) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + d = self.requests_mock.last_request.json()['rebuild'] + self.assertIn('preserve_ephemeral', d) + self.assertTrue(d['preserve_ephemeral']) + + def test_rebuild_server_name_meta_files(self): + files = {'/etc/passwd': 'some data'} + s = self.cs.servers.get(1234) + ret = s.rebuild(image=1, name='new', meta={'foo': 'bar'}, files=files) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + d = self.requests_mock.last_request.json()['rebuild'] + self.assertEqual('new', d['name']) + self.assertEqual({'foo': 'bar'}, d['metadata']) + self.assertEqual('/etc/passwd', + d['personality'][0]['path']) + + def test_resize_server(self): + s = self.cs.servers.get(1234) + ret = s.resize(flavor=1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.resize(s, flavor=1) + self.assert_called('POST', '/servers/1234/action') + + def test_resize_server_disk_config_auto(self): + self._rebuild_resize_disk_config('AUTO', 'resize') + + def test_resize_server_disk_config_manual(self): + self._rebuild_resize_disk_config('MANUAL', 'resize') + + def test_confirm_resized_server(self): + s = self.cs.servers.get(1234) + ret = s.confirm_resize() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.confirm_resize(s) + self.assert_called('POST', '/servers/1234/action') + + def test_revert_resized_server(self): + s = self.cs.servers.get(1234) + ret = s.revert_resize() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.revert_resize(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + 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') + ret = self.cs.servers.migrate(s) + 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() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.stop(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_force_delete(self): + s = self.cs.servers.get(1234) + ret = s.force_delete() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.force_delete(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_restore(self): + s = self.cs.servers.get(1234) + ret = s.restore() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.restore(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_start(self): + s = self.cs.servers.get(1234) + ret = s.start() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.start(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_rescue(self): + s = self.cs.servers.get(1234) + ret = s.rescue() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.rescue(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_rescue_password(self): + s = self.cs.servers.get(1234) + ret = s.rescue(password='asdf') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'adminPass': 'asdf'}}) + ret = self.cs.servers.rescue(s, password='asdf') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'adminPass': 'asdf'}}) + + def test_rescue_image(self): + s = self.cs.servers.get(1234) + ret = s.rescue(image=1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'rescue_image_ref': 1}}) + ret = self.cs.servers.rescue(s, image=1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'rescue_image_ref': 1}}) + + def test_unrescue(self): + s = self.cs.servers.get(1234) + ret = s.unrescue() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.unrescue(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_lock(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') + ret = self.cs.servers.lock(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_unlock(self): + s = self.cs.servers.get(1234) + ret = s.unlock() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.unlock(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_backup(self): + s = self.cs.servers.get(1234) + sb = s.backup('back1', 'daily', 1) + self.assert_request_id(sb, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + sb = self.cs.servers.backup(s, 'back1', 'daily', 2) + self.assert_request_id(sb, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_get_console_output_without_length(self): + success = 'foo' + s = self.cs.servers.get(1234) + co = s.get_console_output() + self.assert_request_id(co, fakes.FAKE_REQUEST_ID_LIST) + self.assertEqual(success, s.get_console_output()) + self.assert_called('POST', '/servers/1234/action') + + co = self.cs.servers.get_console_output(s) + self.assert_request_id(co, fakes.FAKE_REQUEST_ID_LIST) + self.assertEqual(success, self.cs.servers.get_console_output(s)) + self.assert_called('POST', '/servers/1234/action') + + def test_get_console_output_with_length(self): + success = 'foo' + + s = self.cs.servers.get(1234) + co = s.get_console_output(length=50) + self.assert_request_id(co, fakes.FAKE_REQUEST_ID_LIST) + self.assertEqual(success, s.get_console_output(length=50)) + self.assert_called('POST', '/servers/1234/action') + + co = self.cs.servers.get_console_output(s, length=50) + self.assert_request_id(co, fakes.FAKE_REQUEST_ID_LIST) + self.assertEqual(success, + self.cs.servers.get_console_output(s, length=50)) + self.assert_called('POST', '/servers/1234/action') + + # Testing password methods with the following password and key + # + # Clear password: FooBar123 + # + # RSA Private Key: novaclient/tests/unit/idfake.pem + # + # Encrypted password + # OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r + # qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho + # QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw + # /y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N + # tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk + # Hi/fmZZNQQqj1Ijq0caOIw== + + 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('FooBar123', password) + self.assert_called('GET', '/servers/1234/os-server-password') + + def test_get_password_without_key(self): + s = self.cs.servers.get(1234) + password = s.get_password() + self.assert_request_id(password, fakes.FAKE_REQUEST_ID_LIST) + self.assertEqual( + 'OIuEuQttO8Rk93BcKlwHQsziDAnkAm/V6V8VPToA8ZeUaUBWwS0gwo2K6Y61Z96r' + 'qG447iRz0uTEEYq3RAYJk1mh3mMIRVl27t8MtIecR5ggVVbz1S9AwXJQypDKl0ho' + 'QFvhCBcMWPohyGewDJOhDbtuN1IoFI9G55ZvFwCm5y7m7B2aVcoLeIsJZE4PLsIw' + '/y5a6Z3/AoJZYGG7IH5WN88UROU3B9JZGFB2qtPLQTOvDMZLUhoPRIJeHiVSlo1N' + 'tI2/++UsXVg3ow6ItqCJGgdNuGG5JB+bslDHWPxROpesEIHdczk46HCpHQN8f1sk' + 'Hi/fmZZNQQqj1Ijq0caOIw==', password) + self.assert_called('GET', '/servers/1234/os-server-password') + + def test_clear_password(self): + s = self.cs.servers.get(1234) + ret = s.clear_password() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234/os-server-password') + + def test_get_server_diagnostics(self): + s = self.cs.servers.get(1234) + diagnostics = s.diagnostics() + self.assert_request_id(diagnostics, fakes.FAKE_REQUEST_ID_LIST) + self.assertIsNotNone(diagnostics) + self.assert_called('GET', '/servers/1234/diagnostics') + + diagnostics_from_manager = self.cs.servers.diagnostics(1234) + self.assert_request_id(diagnostics_from_manager, + fakes.FAKE_REQUEST_ID_LIST) + self.assertIsNotNone(diagnostics_from_manager) + self.assert_called('GET', '/servers/1234/diagnostics') + + self.assertEqual(diagnostics[1], diagnostics_from_manager[1]) + + def test_get_vnc_console(self): + s = self.cs.servers.get(1234) + vc = s.get_vnc_console('novnc') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + vc = self.cs.servers.get_vnc_console(s, 'novnc') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_get_spice_console(self): + s = self.cs.servers.get(1234) + sc = s.get_spice_console('spice-html5') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + sc = self.cs.servers.get_spice_console(s, 'spice-html5') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_get_serial_console(self): + s = self.cs.servers.get(1234) + sc = s.get_serial_console('serial') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + sc = self.cs.servers.get_serial_console(s, 'serial') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_get_rdp_console(self): + s = self.cs.servers.get(1234) + rc = s.get_rdp_console('rdp-html5') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + rc = self.cs.servers.get_rdp_console(s, 'rdp-html5') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_get_console_url(self): + s = self.cs.servers.get(1234) + rc = s.get_console_url('novnc') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + rc = self.cs.servers.get_console_url(s, 'novnc') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_console_url, + 'invalid') + + def test_create_image(self): + s = self.cs.servers.get(1234) + im = s.create_image('123') + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + im = s.create_image('123', {}) + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + im = self.cs.servers.create_image(s, '123') + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + im = self.cs.servers.create_image(s, '123', {}) + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_live_migrate_server(self): + s = self.cs.servers.get(1234) + ret = s.live_migrate(host='hostname', block_migration=False, + disk_over_commit=False) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}}) + ret = self.cs.servers.live_migrate(s, host='hostname', + block_migration=False, + disk_over_commit=False) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}}) + + def test_live_migrate_server_block_migration_none(self): + s = self.cs.servers.get(1234) + ret = s.live_migrate(host='hostname', block_migration=None, + disk_over_commit=None) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}}) + + def test_reset_state(self): + s = self.cs.servers.get(1234) + ret = s.reset_state('newstate') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.reset_state(s, 'newstate') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_reset_network(self): + s = self.cs.servers.get(1234) + ret = s.reset_network() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.reset_network(s) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_add_security_group(self): + s = self.cs.servers.get(1234) + sg = s.add_security_group('newsg') + self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + sg = self.cs.servers.add_security_group(s, 'newsg') + self.assert_request_id(sg, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_remove_security_group(self): + s = self.cs.servers.get(1234) + ret = s.remove_security_group('oldsg') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.remove_security_group(s, 'oldsg') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_list_security_group(self): + s = self.cs.servers.get(1234) + sgs = s.list_security_group() + self.assert_request_id(sgs, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234/os-security-groups') + + def test_evacuate(self): + s = self.cs.servers.get(1234) + ret = s.evacuate('fake_target_host', 'True') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + ret = self.cs.servers.evacuate(s, 'fake_target_host', + 'False', 'NewAdminPassword') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_interface_list(self): + s = self.cs.servers.get(1234) + il = s.interface_list() + self.assert_request_id(il, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234/os-interface') + + def test_interface_list_result_string_representable(self): + """Test for bugs.launchpad.net/python-novaclient/+bug/1280453.""" + # According to https://github.com/openstack/nova/blob/master/ + # nova/api/openstack/compute/contrib/attach_interfaces.py#L33, + # the attach_interface extension get method will return a json + # object partly like this: + interface_list = [{ + 'net_id': 'd7745cf5-63f9-4883-b0ae-983f061e4f23', + 'port_id': 'f35079da-36d5-4513-8ec1-0298d703f70e', + 'mac_addr': 'fa:16:3e:4c:37:c8', + 'port_state': 'ACTIVE', + 'fixed_ips': [ + { + 'subnet_id': 'f1ad93ad-2967-46ba-b403-e8cbbe65f7fa', + 'ip_address': '10.2.0.96' + }] + }] + # If server is not string representable, it will raise an exception, + # because attribute named 'name' cannot be found. + # Parameter 'loaded' must be True or it will try to get attribute + # 'id' then fails (lazy load detail), this is exactly same as + # novaclient.base.Manager._list() + s = servers.Server(servers.ServerManager, interface_list[0], + loaded=True) + # Trigger the __repr__ magic method + self.assertEqual('', '%r' % s) + + def test_interface_attach(self): + s = self.cs.servers.get(1234) + 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) + ret = s.interface_detach('port-id') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234/os-interface/port-id') + + def test_create_server_with_description(self): + self.assertRaises(exceptions.UnsupportedAttribute, + self.cs.servers.create, + name="My server", + description="descr", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey" + ) + + def test_create_server_with_nics_auto(self): + """Negative test for specifying nics='auto' before 2.37 + """ + self.assertRaises(ValueError, + self.cs.servers.create, + name='test', + image='d9d8d53c-4b4a-4144-a5e5-b30d9f1fe46a', + flavor='1', + nics='auto') + + def test__validate_create_nics(self): + if self.cs.api_version > api_versions.APIVersion('2.36'): + self.assertRaises(ValueError, + self.cs.servers._validate_create_nics, None) + else: + self.cs.servers._validate_create_nics(None) + self.assertRaises(ValueError, + self.cs.servers._validate_create_nics, + mock.Mock()) + self.cs.servers._validate_create_nics(["foo", "bar"]) + self.cs.servers._validate_create_nics(("foo", "bar")) + + +class ServersV26Test(ServersTest): + + api_version = "2.6" + + def test_get_vnc_console(self): + s = self.cs.servers.get(1234) + vc = s.get_vnc_console('novnc') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + vc = self.cs.servers.get_vnc_console(s, 'novnc') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_vnc_console, + 'invalid') + + def test_get_spice_console(self): + s = self.cs.servers.get(1234) + sc = s.get_spice_console('spice-html5') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + sc = self.cs.servers.get_spice_console(s, 'spice-html5') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_spice_console, + 'invalid') + + def test_get_serial_console(self): + s = self.cs.servers.get(1234) + sc = s.get_serial_console('serial') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + sc = self.cs.servers.get_serial_console(s, 'serial') + self.assert_request_id(sc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_serial_console, + 'invalid') + + def test_get_rdp_console(self): + s = self.cs.servers.get(1234) + rc = s.get_rdp_console('rdp-html5') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + rc = self.cs.servers.get_rdp_console(s, 'rdp-html5') + self.assert_request_id(rc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_rdp_console, + 'invalid') + + def test_get_console_url(self): + s = self.cs.servers.get(1234) + vc = s.get_console_url('novnc') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + vc = self.cs.servers.get_console_url(s, 'novnc') + self.assert_request_id(vc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_console_url, + 'invalid') + # console type webmks is supported since api version 2.8 + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_console_url, + 'webmks') + + +class ServersV28Test(ServersV26Test): + + api_version = "2.8" + + def test_get_mks_console(self): + s = self.cs.servers.get(1234) + mksc = s.get_mks_console() + self.assert_request_id(mksc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + mksc = self.cs.servers.get_mks_console(s) + self.assert_request_id(mksc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + def test_get_console_url(self): + s = self.cs.servers.get(1234) + mksc = s.get_console_url('novnc') + self.assert_request_id(mksc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + mksc = self.cs.servers.get_console_url(s, 'novnc') + self.assert_request_id(mksc, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/remote-consoles') + + # test the case with invalid console type + self.assertRaises(exceptions.UnsupportedConsoleType, + s.get_console_url, + 'invalid') + + +class ServersV214Test(ServersV28Test): + + api_version = "2.14" + + def test_evacuate(self): + s = self.cs.servers.get(1234) + s.evacuate('fake_target_host') + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.evacuate(s, 'fake_target_host', + password='NewAdminPassword') + self.assert_called('POST', '/servers/1234/action') + + +class ServersV217Test(ServersV214Test): + + api_version = "2.17" + + def test_trigger_crash_dump(self): + s = self.cs.servers.get(1234) + s.trigger_crash_dump() + self.assert_called('POST', '/servers/1234/action') + self.cs.servers.trigger_crash_dump(s) + self.assert_called('POST', '/servers/1234/action') + + +class ServersV219Test(ServersV217Test): + + api_version = "2.19" + + def test_create_server_with_description(self): + self.cs.servers.create( + name="My server", + description="descr", + image=1, + flavor=1, + meta={'foo': 'bar'}, + userdata="hello moto", + key_name="fakekey", + nics=self._get_server_create_default_nics() + ) + self.assert_called('POST', '/servers') + + def test_update_server_with_description(self): + s = self.cs.servers.get(1234) + + s.update(description='hi') + s.update(name='hi', description='hi') + self.assert_called('PUT', '/servers/1234') + + def test_rebuild_with_description(self): + s = self.cs.servers.get(1234) + + ret = s.rebuild(image="1", description="descr") + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + +class ServersV225Test(ServersV219Test): + + api_version = "2.25" + + 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'}}) + + def test_live_migrate_server_block_migration_true(self): + s = self.cs.servers.get(1234) + ret = s.live_migrate(host='hostname', block_migration=True) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': True}}) + + ret = self.cs.servers.live_migrate(s, host='hostname', + block_migration=True) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': True}}) + + def test_live_migrate_server_block_migration_none(self): + s = self.cs.servers.get(1234) + ret = s.live_migrate(host='hostname', block_migration=None) + 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'}}) + + +class ServersV226Test(ServersV225Test): + + api_version = "2.26" + + def test_tag_list(self): + s = self.cs.servers.get(1234) + ret = s.tag_list() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234/tags') + + def test_tag_delete(self): + s = self.cs.servers.get(1234) + ret = s.delete_tag('tag') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234/tags/tag') + + def test_tag_delete_all(self): + s = self.cs.servers.get(1234) + ret = s.delete_all_tags() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234/tags') + + def test_tag_add(self): + s = self.cs.servers.get(1234) + ret = s.add_tag('tag') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/servers/1234/tags/tag') + + def test_tags_set(self): + s = self.cs.servers.get(1234) + ret = s.set_tags(['tag1', 'tag2']) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/servers/1234/tags') + + +class ServersV229Test(ServersV226Test): + + api_version = "2.29" + + def test_evacuate(self): + s = self.cs.servers.get(1234) + s.evacuate('fake_target_host') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'fake_target_host'}}) + self.cs.servers.evacuate(s, 'fake_target_host', force=True) + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'fake_target_host', + 'force': True}}) + + +class ServersV230Test(ServersV229Test): + + api_version = "2.30" + + 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', + force=True) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto', + 'force': True}}) + + +class ServersV232Test(ServersV226Test): + + api_version = "2.32" + + def test_create_server_boot_with_tagged_nics(self): + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'tag': 'one'}, + {'net-id': '22222222-2222-2222-2222-222222222222', + 'tag': 'two'}] + self.cs.servers.create(name="Server with tagged nics", + image=1, + flavor=1, + nics=nics) + self.assert_called('POST', '/servers') + + def test_create_server_boot_with_tagged_nics_pre232(self): + self.cs.api_version = api_versions.APIVersion("2.31") + nics = [{'net-id': '11111111-1111-1111-1111-111111111111', + 'tag': 'one'}, + {'net-id': '22222222-2222-2222-2222-222222222222', + 'tag': 'two'}] + self.assertRaises(ValueError, self.cs.servers.create, + name="Server with tagged nics", image=1, flavor=1, + nics=nics) + + def test_create_server_boot_from_volume_tagged_bdm_v2(self): + bdm = [{"volume_size": "1", + "volume_id": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": "0", + "device_name": "vda", "tag": "foo"}] + s = self.cs.servers.create(name="My server", image=1, flavor=1, + meta={'foo': 'bar'}, userdata="hello moto", + key_name="fakekey", + block_device_mapping_v2=bdm) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + 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") + bdm = [{"volume_size": "1", + "volume_id": "11111111-1111-1111-1111-111111111111", + "delete_on_termination": "0", + "device_name": "vda", "tag": "foo"}] + self.assertRaises(ValueError, self.cs.servers.create, name="My server", + image=1, flavor=1, meta={'foo': 'bar'}, + userdata="hello moto", key_name="fakekey", + block_device_mapping_v2=bdm) + + +class ServersV2_37Test(ServersV226Test): + + api_version = "2.37" + + def _get_server_create_default_nics(self): + return 'auto' + + def test_create_server_no_nics(self): + """Tests that nics are required in microversion 2.37+ + """ + self.assertRaises(ValueError, self.cs.servers.create, + name='test', + image='d9d8d53c-4b4a-4144-a5e5-b30d9f1fe46a', + flavor='1') + + def test_create_server_with_nics_auto(self): + s = self.cs.servers.create( + name='test', image='d9d8d53c-4b4a-4144-a5e5-b30d9f1fe46a', + flavor='1', nics=self._get_server_create_default_nics()) + self.assert_request_id(s, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers') + self.assertIsInstance(s, servers.Server) + + +class ServersCreateImageBackupV2_45Test(utils.FixturedTestCase): + """Tests the 2.45 microversion for createImage and createBackup + server actions. + """ + + client_fixture_class = client.V1 + data_fixture_class = data.V1 + api_version = '2.45' + + def setUp(self): + super(ServersCreateImageBackupV2_45Test, self).setUp() + self.cs.api_version = api_versions.APIVersion(self.api_version) + + def test_create_image(self): + """Tests the createImage API with the 2.45 microversion which + does not return the Location header, it returns a json dict in the + response body with an image_id key. + """ + s = self.cs.servers.get(1234) + im = s.create_image('123') + self.assertEqual('456', im) + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + im = s.create_image('123', {}) + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + im = self.cs.servers.create_image(s, '123') + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + im = self.cs.servers.create_image(s, '123', {}) + self.assert_request_id(im, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + def test_backup(self): + s = self.cs.servers.get(1234) + # Test backup on the Server object. + sb = s.backup('back1', 'daily', 1) + self.assertIn('image_id', sb) + self.assertEqual('456', sb['image_id']) + self.assert_request_id(sb, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + # Test backup on the ServerManager. + sb = self.cs.servers.backup(s, 'back1', 'daily', 2) + self.assertEqual('456', sb['image_id']) + self.assert_request_id(sb, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('POST', '/servers/1234/action') + + +class ServersV249Test(ServersV2_37Test): + + api_version = "2.49" + + def test_interface_attach_with_tag(self): + s = self.cs.servers.get(1234) + ret = s.interface_attach('7f42712e-63fe-484c-a6df-30ae4867ff66', + None, None, 'test_tag') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called( + 'POST', '/servers/1234/os-interface', + {'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() + # is not available after 2.44 + pass + + def test_remove_fixed_ip(self): + # novaclient.v2.servers.Server.remove_fixed_ip() + # is not available after 2.44 + pass + + +class ServersV252Test(ServersV249Test): + + api_version = "2.52" + + def test_create_server_with_tags(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(), + tags=['tag1', 'tag2'] + ) + 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', + 'tags': ['tag1', 'tag2'], + 'user_data': 'aGVsbG8gbW90bw==' + }} + ) + + def test_create_server_with_tags_pre_252_fails(self): + self.cs.api_version = api_versions.APIVersion('2.51') + 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(), + 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_services.py b/novaclient/tests/unit/v2/test_services.py new file mode 100644 index 000000000..ddc75eeb1 --- /dev/null +++ b/novaclient/tests/unit/v2/test_services.py @@ -0,0 +1,191 @@ +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import api_versions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import services + + +class ServicesTest(utils.TestCase): + api_version = "2.0" + + def setUp(self): + super(ServicesTest, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion(self.api_version)) + self.service_type = self._get_service_type() + + def _get_service_type(self): + return services.Service + + def test_list_services(self): + svs = self.cs.services.list() + self.assert_request_id(svs, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-services') + for s in svs: + self.assertIsInstance(s, self._get_service_type()) + self.assertEqual('nova-compute', s.binary) + self.assertEqual('host1', s.host) + self.assertEqual('' % s.id, str(s)) + + def test_list_services_with_hostname(self): + svs = self.cs.services.list(host='host2') + self.assert_request_id(svs, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-services?host=host2') + for s in svs: + self.assertIsInstance(s, self._get_service_type()) + self.assertEqual('nova-compute', s.binary) + self.assertEqual('host2', s.host) + + def test_list_services_with_binary(self): + svs = self.cs.services.list(binary='nova-cert') + self.assert_request_id(svs, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-services?binary=nova-cert') + for s in svs: + self.assertIsInstance(s, self._get_service_type()) + self.assertEqual('nova-cert', s.binary) + self.assertEqual('host1', s.host) + + def test_list_services_with_host_binary(self): + svs = self.cs.services.list(host='host2', binary='nova-cert') + self.assert_request_id(svs, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', + '/os-services?host=host2&binary=nova-cert') + for s in svs: + self.assertIsInstance(s, self._get_service_type()) + self.assertEqual('nova-cert', s.binary) + self.assertEqual('host2', s.host) + + def _update_body(self, host, binary, disabled_reason=None): + body = {"host": host, + "binary": binary} + if disabled_reason is not None: + body["disabled_reason"] = disabled_reason + return body + + def test_services_enable(self): + service = self.cs.services.enable('host1', 'nova-cert') + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) + values = self._update_body("host1", "nova-cert") + self.cs.assert_called('PUT', '/os-services/enable', values) + self.assertIsInstance(service, self._get_service_type()) + self.assertEqual('enabled', service.status) + + def test_services_delete(self): + ret = self.cs.services.delete('1') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('DELETE', '/os-services/1') + + def test_services_disable(self): + service = self.cs.services.disable('host1', 'nova-cert') + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) + values = self._update_body("host1", "nova-cert") + self.cs.assert_called('PUT', '/os-services/disable', values) + self.assertIsInstance(service, self._get_service_type()) + self.assertEqual('disabled', service.status) + + def test_services_disable_log_reason(self): + service = self.cs.services.disable_log_reason( + 'compute1', 'nova-compute', 'disable bad host') + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) + values = self._update_body("compute1", "nova-compute", + "disable bad host") + self.cs.assert_called('PUT', '/os-services/disable-log-reason', values) + self.assertIsInstance(service, self._get_service_type()) + self.assertEqual('disabled', service.status) + + +class ServicesV211TestCase(ServicesTest): + api_version = "2.11" + + def _update_body(self, host, binary, disabled_reason=None, + force_down=None): + body = {"host": host, + "binary": binary} + if disabled_reason is not None: + body["disabled_reason"] = disabled_reason + if force_down is not None: + body["forced_down"] = force_down + return body + + def test_services_force_down(self): + service = self.cs.services.force_down( + 'compute1', 'nova-compute', False) + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) + values = self._update_body("compute1", "nova-compute", + force_down=False) + self.cs.assert_called('PUT', '/os-services/force-down', values) + self.assertIsInstance(service, self._get_service_type()) + self.assertFalse(service.forced_down) + + +class ServicesV2_53TestCase(ServicesV211TestCase): + api_version = "2.53" + + def _update_body(self, status=None, disabled_reason=None, force_down=None): + body = {} + if status is not None: + body['status'] = status + if disabled_reason is not None: + body['disabled_reason'] = disabled_reason + if force_down is not None: + body['forced_down'] = force_down + return body + + def test_services_enable(self): + service = self.cs.services.enable(fakes.FAKE_SERVICE_UUID_1) + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) + values = self._update_body(status='enabled') + self.cs.assert_called( + 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, values) + self.assertIsInstance(service, self._get_service_type()) + self.assertEqual('enabled', service.status) + + def test_services_delete(self): + ret = self.cs.services.delete(fakes.FAKE_SERVICE_UUID_1) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('DELETE', + '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1) + + def test_services_disable(self): + service = self.cs.services.disable(fakes.FAKE_SERVICE_UUID_1) + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) + values = self._update_body(status='disabled') + self.cs.assert_called( + 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, values) + self.assertIsInstance(service, self._get_service_type()) + self.assertEqual('disabled', service.status) + + def test_services_disable_log_reason(self): + service = self.cs.services.disable_log_reason( + fakes.FAKE_SERVICE_UUID_1, 'disable bad host') + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) + values = self._update_body(status='disabled', + disabled_reason='disable bad host') + self.cs.assert_called( + 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, values) + self.assertIsInstance(service, self._get_service_type()) + self.assertEqual('disabled', service.status) + self.assertEqual('disable bad host', service.disabled_reason) + + def test_services_force_down(self): + service = self.cs.services.force_down( + fakes.FAKE_SERVICE_UUID_1, False) + self.assert_request_id(service, fakes.FAKE_REQUEST_ID_LIST) + values = self._update_body(force_down=False) + self.cs.assert_called( + 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, values) + self.assertIsInstance(service, self._get_service_type()) + self.assertFalse(service.forced_down) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py new file mode 100644 index 000000000..846faf9e1 --- /dev/null +++ b/novaclient/tests/unit/v2/test_shell.py @@ -0,0 +1,4978 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack Foundation +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import base64 +import builtins +import collections +import datetime +import io +import os +from unittest import mock + +import fixtures +from oslo_utils import timeutils +import testtools + +import novaclient +from novaclient import api_versions +from novaclient import base +import novaclient.client +from novaclient import exceptions +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() + self.shell = novaclient.shell.OpenStackComputeShell() + + def tearDown(self): + # For some method like test_image_meta_bad_action we are + # testing a SystemExit to be thrown and object self.shell has + # no time to get instantiated which is OK in this case, so + # we make sure the method is there before launching it. + if hasattr(self.shell, 'cs'): + self.shell.cs.clear_callstack() + super(ShellFixture, self).tearDown() + + +class ShellTest(utils.TestCase): + FAKE_ENV = { + 'NOVA_USERNAME': 'username', + 'NOVA_PASSWORD': 'password', + 'NOVA_PROJECT_ID': 'project_id', + 'OS_COMPUTE_API_VERSION': '2', + 'NOVA_URL': 'http://no.where', + 'OS_AUTH_URL': 'http://no.where/v3', + } + + def setUp(self): + """Run before each test.""" + super(ShellTest, self).setUp() + + for var in self.FAKE_ENV: + self.useFixture(fixtures.EnvironmentVariable(var, + self.FAKE_ENV[var])) + self.shell = self.useFixture(ShellFixture()).shell + self.useFixture(fixtures.MonkeyPatch( + 'novaclient.client.Client', fakes.FakeClient)) + + # 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 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) + + return mock_stdout.getvalue(), mock_stderr.getvalue() + + def assert_called(self, method, url, body=None, **kwargs): + return self.shell.cs.assert_called(method, url, body, **kwargs) + + def assert_called_anytime(self, method, url, body=None): + return self.shell.cs.assert_called_anytime(method, url, body) + + def 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): + _, 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): + _, err = self.run_command('agent-create win x86 7.0 ' + '/xxx/xxx/xxx ' + 'add6bb58e139be103324d04d82d8f546 ' + 'kvm') + self.assert_called( + 'POST', '/os-agents', + {'agent': { + 'hypervisor': 'kvm', + 'os': 'win', + 'architecture': 'x86', + '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): + _, 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): + _, 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 ' + 'some-server' % FAKE_UUID_1) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_image_with(self): + self.run_command("boot --flavor 1" + " --image-with test_key=test_value some-server") + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + 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) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'key_name': '1', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_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 = base64.b64encode(data).decode('utf-8') + self.run_command( + 'boot --flavor 1 --image %s --user-data %s some-server' % + (FAKE_UUID_1, testfile)) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'user_data': expected_file_data + }}, + ) + + def test_boot_avzone(self): + self.run_command( + 'boot --flavor 1 --image %s --availability-zone avzone ' + 'some-server' % FAKE_UUID_1) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'availability_zone': 'avzone', + 'min_count': 1, + 'max_count': 1 + }}, + ) + + def test_boot_secgroup(self): + self.run_command( + 'boot --flavor 1 --image %s --security-groups secgroup1,' + 'secgroup2 some-server' % FAKE_UUID_1) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'security_groups': [{'name': 'secgroup1'}, + {'name': 'secgroup2'}], + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_access_ip(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) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'accessIPv4': '10.10.10.10', + 'accessIPv6': '::1', + 'max_count': 1, + 'min_count': 1 + }}, + ) + + def test_boot_config_drive(self): + self.run_command( + '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, + 'min_count': 1, + 'max_count': 1, + 'config_drive': True + }}, + ) + + def test_boot_config_drive_false(self): + self.run_command( + 'boot --flavor 1 --image %s --config-drive false some-server' % + FAKE_UUID_1) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + 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') + cmd = ('boot some-server --flavor 1 --image %s' + ' --user-data %s' % (FAKE_UUID_1, invalid_file)) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_no_image_no_bdms(self): + cmd = 'boot --flavor 1 some-server' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_no_flavor(self): + cmd = 'boot --image %s some-server' % FAKE_UUID_1 + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_not_key_value_bdm(self): + cmd = ('boot --flavor 1 --image %s --block-device %s,tag=foo ' + 'test-server' % (FAKE_UUID_1, FAKE_UUID_2)) + self.assertRaises(argparse.ArgumentTypeError, self.run_command, cmd) + + def test_boot_not_key_value_ephemeral(self): + cmd = ('boot --flavor 1 --image %s --ephemeral %s,tag=foo ' + 'test-server' % (FAKE_UUID_1, FAKE_UUID_2)) + self.assertRaises(argparse.ArgumentTypeError, self.run_command, cmd) + + def test_boot_no_image_bdms(self): + self.run_command( + 'boot --flavor 1 --block-device-mapping vda=blah:::0 some-server' + ) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping': [ + { + 'volume_id': 'blah', + 'delete_on_termination': '0', + 'device_name': 'vda' + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_image_bdms_v2(self): + self.run_command( + 'boot --flavor 1 --image %s --block-device id=fake-id,' + 'source=volume,dest=volume,device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 + ) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'local', + 'boot_index': 0, + 'delete_on_termination': True, + }, + { + 'uuid': 'fake-id', + 'source_type': 'volume', + 'destination_type': 'volume', + 'device_name': 'vda', + 'volume_size': '1', + 'guest_format': 'ext4', + 'device_type': 'disk', + 'delete_on_termination': False, + }, + ], + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_image_bdms_v2_wrong_source_type(self): + self.assertRaises( + exceptions.CommandError, self.run_command, + 'boot --flavor 1 --image %s --block-device id=fake-id,' + 'source=fake,device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1) + + def test_boot_image_bdms_v2_no_source_type_no_destination_type(self): + self.run_command( + 'boot --flavor 1 --image %s --block-device id=fake-id,' + 'device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 + ) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'local', + 'boot_index': 0, + 'delete_on_termination': True, + }, + { + 'uuid': 'fake-id', + 'source_type': 'blank', + 'destination_type': 'local', + 'device_name': 'vda', + 'volume_size': '1', + 'guest_format': 'ext4', + 'device_type': 'disk', + 'delete_on_termination': False, + }, + ], + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_image_bdms_v2_no_destination_type(self): + self.run_command( + 'boot --flavor 1 --image %s --block-device id=fake-id,' + 'source=volume,device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1 + ) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'local', + 'boot_index': 0, + 'delete_on_termination': True, + }, + { + 'uuid': 'fake-id', + 'source_type': 'volume', + 'destination_type': 'volume', + 'device_name': 'vda', + 'volume_size': '1', + 'guest_format': 'ext4', + 'device_type': 'disk', + 'delete_on_termination': False, + }, + ], + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_image_bdms_v2_wrong_destination_type(self): + self.assertRaises( + exceptions.CommandError, self.run_command, + 'boot --flavor 1 --image %s --block-device id=fake-id,' + 'source=volume,dest=dest1,device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve some-server' % FAKE_UUID_1) + + def test_boot_image_bdms_v2_with_tag(self): + self.run_command( + 'boot --flavor 1 --image %s --block-device id=fake-id,' + 'source=volume,dest=volume,device=vda,size=1,format=ext4,' + 'type=disk,shutdown=preserve,tag=foo some-server' % FAKE_UUID_1, + api_version='2.32' + ) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': FAKE_UUID_1, + 'source_type': 'image', + 'destination_type': 'local', + 'boot_index': 0, + 'delete_on_termination': True, + }, + { + 'uuid': 'fake-id', + 'source_type': 'volume', + 'destination_type': 'volume', + 'device_name': 'vda', + 'volume_size': '1', + 'guest_format': 'ext4', + 'device_type': 'disk', + 'delete_on_termination': False, + 'tag': 'foo', + }, + ], + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_no_image_bdms_v2(self): + self.run_command( + 'boot --flavor 1 --block-device id=fake-id,source=volume,' + 'dest=volume,bus=virtio,device=vda,size=1,format=ext4,bootindex=0,' + 'type=disk,shutdown=preserve some-server' + ) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': 'fake-id', + 'source_type': 'volume', + 'destination_type': 'volume', + 'disk_bus': 'virtio', + 'device_name': 'vda', + 'volume_size': '1', + 'guest_format': 'ext4', + 'boot_index': '0', + 'device_type': 'disk', + 'delete_on_termination': False, + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + cmd = 'boot --flavor 1 --boot-volume fake-id some-server' + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': 'fake-id', + 'source_type': 'volume', + 'destination_type': 'volume', + 'boot_index': 0, + 'delete_on_termination': False, + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + cmd = 'boot --flavor 1 --snapshot fake-id some-server' + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'uuid': 'fake-id', + 'source_type': 'snapshot', + 'destination_type': 'volume', + 'boot_index': 0, + 'delete_on_termination': False, + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + self.run_command('boot --flavor 1 --swap 1 some-server') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'source_type': 'blank', + 'destination_type': 'local', + 'boot_index': -1, + 'guest_format': 'swap', + 'volume_size': '1', + 'delete_on_termination': True, + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + self.run_command( + 'boot --flavor 1 --ephemeral size=1,format=ext4 some-server' + ) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'block_device_mapping_v2': [ + { + 'source_type': 'blank', + 'destination_type': 'local', + 'boot_index': -1, + 'guest_format': 'ext4', + 'volume_size': '1', + 'delete_on_termination': True, + } + ], + 'imageRef': '', + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_bdms_v2_invalid_shutdown_value(self): + self.assertRaises(exceptions.CommandError, self.run_command, + ('boot --flavor 1 --image %s --block-device ' + 'id=fake-id,source=volume,dest=volume,device=vda,' + '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) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'metadata': {'foo': 'bar=pants', 'spam': 'eggs'}, + 'min_count': 1, + 'max_count': 1, + }}, + ) + + def test_boot_with_incorrect_metadata(self): + cmd = ('boot --image %s --flavor 1 --meta foo ' + 'some-server ' % FAKE_UUID_1) + result = self.assertRaises(argparse.ArgumentTypeError, + self.run_command, cmd) + expected = "'['foo']' is not in the format of 'key=value'" + self.assertEqual(expected, result.args[0]) + + def test_boot_hints(self): + 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', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }, + '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' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nic_auto_not_alone_before(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic tag=foo,auto some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nic_none_not_alone_before(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic none,tag=foo some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nic_none_not_alone_after(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic tag=foo,none some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1 some-server' % + FAKE_UUID_1) + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': 'a=c', 'fixed_ip': '10.0.0.1'}, + ], + }, + }, + ) + + def test_boot_with_multiple_nics(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=net_a,v4-fixed-ip=10.0.0.1 ' + '--nic net-id=net_b some-server' % + FAKE_UUID_1) + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': 'net_a', 'fixed_ip': '10.0.0.1'}, + {'uuid': 'net_b'} + ], + }, + }, + ) + + def test_boot_nics_with_tag(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,tag=foo some-server' % + FAKE_UUID_1) + self.run_command(cmd, api_version='2.32') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': 'a=c', 'fixed_ip': '10.0.0.1', 'tag': 'foo'}, + ], + }, + }, + ) + + def test_boot_invalid_nics_pre_v2_32(self): + # This is a negative test to make sure we fail with the correct message + cmd = ('boot --image %s --flavor 1 ' + '--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', str(ex)) + + def test_boot_invalid_nics_v2_32(self): + # This is a negative test to make sure we fail with the correct message + cmd = ('boot --image %s --flavor 1 ' + '--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', str(ex)) + + def test_boot_invalid_nics_v2_36_auto(self): + """This is a negative test to make sure we fail with the correct + message. --nic auto isn't allowed before 2.37. + """ + 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', str(ex)) + + def test_boot_invalid_nics_v2_37(self): + """This is a negative test to make sure we fail with the correct + message. + """ + cmd = ('boot --image %s --flavor 1 ' + '--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', str(ex)) + + def test_boot_nics_auto_allocate_default(self): + """Tests that if microversion>=2.37 is specified and no --nics are + specified that a single --nic with net-id=auto is used. + """ + cmd = 'boot --image %s --flavor 1 some-server' % FAKE_UUID_1 + self.run_command(cmd, api_version='2.37') + 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_nics_auto_allocate_none(self): + """Tests specifying '--nic none' with microversion 2.37 + """ + cmd = 'boot --image %s --flavor 1 --nic none some-server' % FAKE_UUID_1 + self.run_command(cmd, api_version='2.37') + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'none', + }, + }, + ) + + def test_boot_nics_ipv6(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v6-fixed-ip=2001:db9:0:1::10 some-server' % + FAKE_UUID_1) + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': 'a=c', 'fixed_ip': '2001:db9:0:1::10'}, + ], + }, + }, + ) + + def test_boot_nics_both_ipv4_and_ipv6(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,' + 'v6-fixed-ip=2001:db9:0:1::10 some-server' % FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_no_value(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id some-server' % FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_random_key(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=10.0.0.1,foo=bar some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_no_netid_or_portid(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic v4-fixed-ip=10.0.0.1 some-server' % FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_netid_and_portid(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic port-id=some=port,net-id=some=net some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_invalid_ipv4(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v4-fixed-ip=2001:db9:0:1::10 some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_invalid_ipv6(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=a=c,v6-fixed-ip=10.0.0.1 some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_net_id_twice(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-id=net-id1,net-id=net-id2 some-server' % FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_nics_net_name_neutron(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=private some-server' % FAKE_UUID_1) + self.run_command(cmd) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': [ + {'uuid': 'e43a56c7-11d4-45c9-8681-ddc8171b5850'}, + ], + }, + }, + ) + + def test_boot_nics_net_name_neutron_dup(self): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=duplicate some-server' % FAKE_UUID_1) + # this should raise a multiple matches error + msg = ("Multiple network matches found for 'duplicate', " + "use an ID to be more specific.") + with testtools.ExpectedException(exceptions.CommandError, msg): + self.run_command(cmd) + + 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\\..*' + with testtools.ExpectedException(exceptions.CommandError, msg): + self.run_command(cmd) + + # TODO(sdague): the following tests should really avoid mocking + # out other tests, and they should check the string in the + # CommandError, because it's not really enough to distinguish + # between various errors. + @mock.patch('novaclient.v2.shell._find_network_id', return_value='net-id') + def test_boot_nics_net_name_and_net_id(self, mock_find_network_id): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=some-net,net-id=some-id some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + @mock.patch('novaclient.v2.shell._find_network_id', return_value='net-id') + def test_boot_nics_net_name_and_port_id(self, mock_find_network_id): + cmd = ('boot --image %s --flavor 1 ' + '--nic net-name=some-net,port-id=some-id some-server' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_files(self): + testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') + with open(testfile) as testfile_fd: + data = testfile_fd.read() + expected = base64.b64encode(data.encode('utf-8')).decode('utf-8') + + cmd = ('boot some-server --flavor 1 --image %s' + ' --file /tmp/foo=%s --file /tmp/bar=%s') + self.run_command(cmd % (FAKE_UUID_1, testfile, testfile)) + + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'personality': [ + {'path': '/tmp/bar', 'contents': expected}, + {'path': '/tmp/foo', 'contents': expected}, + ] + }}, + ) + + def test_boot_invalid_files(self): + invalid_file = os.path.join(os.path.dirname(__file__), + 'asdfasdfasdfasdf') + cmd = ('boot some-server --flavor 1 --image %s' + ' --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) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 3, + } + }) + + def test_boot_invalid_min_count(self): + cmd = 'boot --image %s --flavor 1 --min-count 0 server' % FAKE_UUID_1 + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_boot_min_max_count(self): + self.run_command('boot --image %s --flavor 1 --max-count 3 server' % + FAKE_UUID_1) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 3, + } + }) + self.run_command('boot --image %s --flavor 1 --min-count 3 server' % + FAKE_UUID_1) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': FAKE_UUID_1, + 'min_count': 3, + 'max_count': 3, + } + }) + self.run_command('boot --image %s --flavor 1 ' + '--min-count 3 --max-count 3 server' % FAKE_UUID_1) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': FAKE_UUID_1, + 'min_count': 3, + 'max_count': 3, + } + }) + self.run_command('boot --image %s --flavor 1 ' + '--min-count 3 --max-count 5 server' % FAKE_UUID_1) + self.assert_called_anytime( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '1', + 'name': 'server', + 'imageRef': FAKE_UUID_1, + 'min_count': 3, + 'max_count': 5, + } + }) + cmd = ('boot --image %s --flavor 1 --min-count 3 --max-count 1 serv' % + FAKE_UUID_1) + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + @mock.patch('novaclient.v2.shell._poll_for_status') + def test_boot_with_poll(self, poll_method): + self.run_command('boot --flavor 1 --image %s some-server --poll' % + FAKE_UUID_1) + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + }}, + ) + self.assertEqual(1, poll_method.call_count) + poll_method.assert_has_calls( + [mock.call(self.shell.cs.servers.get, '1234', 'building', + ['active'])]) + + def test_boot_with_poll_to_check_VM_state_error(self): + self.assertRaises(exceptions.ResourceInErrorState, self.run_command, + 'boot --flavor 1 --image %s some-bad-server --poll' % + FAKE_UUID_1) + + def test_boot_named_flavor(self): + self.run_command(["boot", "--image", FAKE_UUID_1, + "--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 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( + 'POST', '/servers', + { + 'server': { + 'flavorRef': '2', + 'name': 'server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 3, + } + }, pos=4) + + def test_boot_invalid_ephemeral_data_format(self): + cmd = ('boot --flavor 1 --image %s --ephemeral 1 some-server' % + FAKE_UUID_1) + self.assertRaises(argparse.ArgumentTypeError, self.run_command, cmd) + + def test_boot_with_tags(self): + self.run_command('boot --flavor 1 --image %s --nic auto ' + 'some-server --tags tag1,tag2' % FAKE_UUID_1, + api_version='2.52') + self.assert_called_anytime( + 'POST', '/servers', + {'server': { + 'flavorRef': '1', + 'name': 'some-server', + 'imageRef': FAKE_UUID_1, + 'min_count': 1, + 'max_count': 1, + 'networks': 'auto', + 'tags': ['tag1', 'tag2'] + }}, + ) + + def test_boot_without_tags_v252(self): + self.run_command('boot --flavor 1 --image %s --nic auto ' + 'some-server' % FAKE_UUID_1, + api_version='2.52') + 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_tags_pre_v2_52(self): + cmd = ('boot --flavor 1 --image %s some-server ' + '--tags tag1,tag2' % FAKE_UUID_1) + 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): + 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') + + def test_flavor_list_with_limit_and_marker(self): + self.run_command('flavor-list --marker 1 --limit 2') + self.assert_called('GET', '/flavors/detail?limit=2&marker=1') + + def test_flavor_list_with_min_disk(self): + self.run_command('flavor-list --min-disk 20') + self.assert_called('GET', '/flavors/detail?minDisk=20') + + def test_flavor_list_with_min_ram(self): + self.run_command('flavor-list --min-ram 512') + self.assert_called('GET', '/flavors/detail?minRam=512') + + def test_flavor_list_with_sort_key_dir(self): + self.run_command('flavor-list --sort-key id --sort-dir asc') + self.assert_called('GET', '/flavors/detail?sort_dir=asc&sort_key=id') + + def test_flavor_show(self): + 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 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 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) + + def test_flavor_key_set(self): + self.run_command('flavor-key 1 set k1=v1') + self.assert_called('POST', '/flavors/1/os-extra_specs', + {'extra_specs': {'k1': 'v1'}}) + + def test_flavor_key_unset(self): + self.run_command('flavor-key 1 unset k1') + self.assert_called('DELETE', '/flavors/1/os-extra_specs/k1') + + def test_flavor_access_list_flavor(self): + self.run_command('flavor-access-list --flavor 2') + self.assert_called('GET', '/flavors/2/os-flavor-access') + + def test_flavor_access_list_no_filter(self): + cmd = 'flavor-access-list' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_flavor_access_list_public(self): + cmd = 'flavor-access-list --flavor 1' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_flavor_access_add_by_id(self): + self.run_command('flavor-access-add 2 proj2') + self.assert_called('POST', '/flavors/2/action', + {'addTenantAccess': {'tenant': 'proj2'}}) + + def test_flavor_access_add_by_name(self): + self.run_command(['flavor-access-add', '512 MiB Server', 'proj2']) + self.assert_called('POST', '/flavors/2/action', + {'addTenantAccess': {'tenant': 'proj2'}}) + + def test_flavor_access_remove_by_id(self): + self.run_command('flavor-access-remove 2 proj2') + self.assert_called('POST', '/flavors/2/action', + {'removeTenantAccess': {'tenant': 'proj2'}}) + + def test_flavor_access_remove_by_name(self): + self.run_command(['flavor-access-remove', '512 MiB Server', 'proj2']) + self.assert_called('POST', '/flavors/2/action', + {'removeTenantAccess': {'tenant': 'proj2'}}) + + def test_create_image(self): + self.run_command('image-create sample-server mysnapshot') + self.assert_called( + 'POST', '/servers/1234/action', + {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, + ) + + def test_create_image_2_45(self): + """Tests the image-create command with microversion 2.45 which + does not change the output of the command, just how the response + from the server is processed. + """ + self.run_command('image-create sample-server mysnapshot', + api_version='2.45') + self.assert_called( + 'POST', '/servers/1234/action', + {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, + ) + + def test_create_image_with_incorrect_metadata(self): + cmd = 'image-create sample-server mysnapshot --metadata foo' + result = self.assertRaises(argparse.ArgumentTypeError, + self.run_command, cmd) + expected = "'['foo']' is not in the format of 'key=value'" + self.assertEqual(expected, result.args[0]) + + def test_create_image_with_metadata(self): + self.run_command( + 'image-create sample-server mysnapshot --metadata mykey=123') + self.assert_called( + 'POST', '/servers/1234/action', + {'createImage': {'name': 'mysnapshot', + 'metadata': {'mykey': '123'}}}, + ) + + def test_create_image_show(self): + output, _err = self.run_command( + 'image-create sample-server mysnapshot --show') + self.assert_called_anytime( + 'POST', '/servers/1234/action', + {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, + ) + self.assertIn('My Server Backup', output) + self.assertIn('SAVING', output) + + @mock.patch('novaclient.v2.shell._poll_for_status') + def test_create_image_with_poll(self, poll_method): + self.run_command( + 'image-create sample-server mysnapshot --poll') + self.assert_called_anytime( + 'POST', '/servers/1234/action', + {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, + ) + self.assertEqual(1, poll_method.call_count) + poll_method.assert_has_calls( + [mock.call(self.shell.cs.glance.find_image, + fakes.FAKE_IMAGE_UUID_SNAPSHOT, 'snapshotting', + ['active'])]) + + def test_create_image_with_poll_to_check_image_state_deleted(self): + self.assertRaises( + exceptions.InstanceInDeletedState, self.run_command, + 'image-create sample-server mysnapshot_deleted --poll') + + def test_list(self): + self.run_command('list') + self.assert_called('GET', '/servers/detail') + + def test_list_minimal(self): + self.run_command('list --minimal') + self.assert_called('GET', '/servers') + + def test_list_deleted(self): + self.run_command('list --deleted') + self.assert_called('GET', '/servers/detail?deleted=True') + + def test_list_with_images(self): + self.run_command('list --image %s' % FAKE_UUID_1) + self.assert_called('GET', '/servers/detail?image=%s' % FAKE_UUID_1) + + def test_list_with_flavors(self): + self.run_command('list --flavor 1') + self.assert_called('GET', '/servers/detail?flavor=1') + + def test_list_by_tenant(self): + self.run_command('list --tenant fake_tenant') + self.assert_called( + 'GET', + '/servers/detail?all_tenants=1&tenant_id=fake_tenant') + + def test_list_by_user(self): + self.run_command('list --user fake_user') + self.assert_called( + 'GET', + '/servers/detail?user_id=fake_user') + + def test_list_with_single_sort_key_no_dir(self): + self.run_command('list --sort 1') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=desc&sort_key=1')) + + def test_list_with_single_sort_key_and_dir(self): + self.run_command('list --sort 1:asc') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=asc&sort_key=1')) + + def test_list_with_sort_keys_no_dir(self): + self.run_command('list --sort 1,2') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=desc&sort_dir=desc&' + 'sort_key=1&sort_key=2')) + + def test_list_with_sort_keys_and_dirs(self): + self.run_command('list --sort 1:asc,2:desc') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=asc&sort_dir=desc&' + 'sort_key=1&sort_key=2')) + + def test_list_with_sort_keys_and_some_dirs(self): + self.run_command('list --sort 1,2:asc') + self.assert_called( + 'GET', ('/servers/detail?sort_dir=desc&sort_dir=asc&' + 'sort_key=1&sort_key=2')) + + def test_list_with_invalid_sort_dir_one(self): + cmd = 'list --sort 1:foo' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_list_with_invalid_sort_dir_two(self): + cmd = 'list --sort 1:asc,2:foo' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_list_sortby_index_with_sort(self): + # sortby_index is None if there is sort information + for cmd in ['list --sort key', + 'list --sort key:desc', + 'list --sort key1,key2:asc']: + with mock.patch('novaclient.utils.print_list') as mock_print_list: + self.run_command(cmd) + mock_print_list.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, sortby_index=None) + + def test_list_sortby_index_without_sort(self): + # sortby_index is 1 without sort information + for cmd in ['list', 'list --minimal', 'list --deleted']: + with mock.patch('novaclient.utils.print_list') as mock_print_list: + self.run_command(cmd) + mock_print_list.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, sortby_index=1) + + def test_list_fields(self): + output, _err = self.run_command( + 'list --fields ' + 'host,security_groups,OS-EXT-MOD:some_thing') + self.assert_called('GET', '/servers/detail') + self.assertIn('computenode1', output) + 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') + def test_list_fields_no_instances(self, mock_get_servers_detail): + mock_get_servers_detail.return_value = (200, {}, {"servers": []}) + stdout, _stderr = self.run_command('list --fields metadata,networks') + # Because there are no instances, you just get the default columns + # rather than the ones you actually asked for (Metadata, Networks). + defaults = 'ID | Name | Status | Task State | Power State | Networks' + self.assertIn(defaults, stdout) + + def test_list_invalid_fields(self): + self.assertRaises(exceptions.CommandError, + 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') + self.assert_called('GET', '/servers/detail?marker=some-uuid') + + def test_list_with_limit(self): + self.run_command('list --limit 3') + self.assert_called('GET', '/servers/detail?limit=3') + + def test_list_with_changes_since(self): + self.run_command('list --changes-since 2016-02-29T06:23:22') + self.assert_called( + 'GET', '/servers/detail?changes-since=2016-02-29T06%3A23%3A22') + + 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] + self.assertEqual(1, header.count('ID')) + self.assertEqual(0, header.count('Id')) + self.assertEqual(1, header.count('Status')) + + def test_meta_parsing(self): + meta = ['key1=meta1', 'key2=meta2'] + ref = {'key1': 'meta1', 'key2': 'meta2'} + parsed_meta = novaclient.v2.shell._meta_parsing(meta) + self.assertEqual(ref, parsed_meta) + + def test_reboot(self): + self.run_command('reboot sample-server') + self.assert_called('POST', '/servers/1234/action', + {'reboot': {'type': 'SOFT'}}) + self.run_command('reboot sample-server --hard') + self.assert_called('POST', '/servers/1234/action', + {'reboot': {'type': 'HARD'}}) + + def test_reboot_many(self): + self.run_command('reboot sample-server sample-server2') + self.assert_called('POST', '/servers/1234/action', + {'reboot': {'type': 'SOFT'}}, pos=-2) + self.assert_called('POST', '/servers/5678/action', + {'reboot': {'type': 'SOFT'}}, pos=-1) + + def test_rebuild(self): + output, _err = self.run_command('rebuild sample-server %s' + % FAKE_UUID_1) + 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}}, pos=3) + self.assert_called('GET', '/flavors/1', pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + self.assertIn('adminPass', output) + + def test_rebuild_password(self): + output, _err = self.run_command('rebuild sample-server %s' + ' --rebuild-password asdf' + % FAKE_UUID_1) + 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, + 'adminPass': 'asdf'}}, pos=3) + self.assert_called('GET', '/flavors/1', pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + self.assertIn('adminPass', output) + + def test_rebuild_preserve_ephemeral(self): + self.run_command('rebuild sample-server %s --preserve-ephemeral' + % FAKE_UUID_1) + 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, + 'preserve_ephemeral': True}}, pos=3) + self.assert_called('GET', '/flavors/1', pos=4) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=5) + + def test_rebuild_name_meta(self): + self.run_command('rebuild sample-server %s --name asdf --meta ' + 'foo=bar' % FAKE_UUID_1) + 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, + 'name': 'asdf', + 'metadata': {'foo': 'bar'}}}, pos=3) + 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, + self.run_command, cmd) + 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}) + + def test_start_with_all_tenants(self): + self.run_command('start sample-server --all-tenants') + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('POST', '/servers/1234/action', {'os-start': None}) + + def test_stop(self): + self.run_command('stop sample-server') + self.assert_called('POST', '/servers/1234/action', {'os-stop': None}) + + def test_stop_with_all_tenants(self): + self.run_command('stop sample-server --all-tenants') + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('POST', '/servers/1234/action', {'os-stop': None}) + + def test_pause(self): + self.run_command('pause sample-server') + self.assert_called('POST', '/servers/1234/action', {'pause': None}) + + def test_unpause(self): + self.run_command('unpause sample-server') + self.assert_called('POST', '/servers/1234/action', {'unpause': None}) + + 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}) + + def test_suspend(self): + self.run_command('suspend sample-server') + self.assert_called('POST', '/servers/1234/action', {'suspend': None}) + + def test_resume(self): + self.run_command('resume sample-server') + self.assert_called('POST', '/servers/1234/action', {'resume': None}) + + def test_rescue(self): + self.run_command('rescue sample-server') + self.assert_called('POST', '/servers/1234/action', {'rescue': None}) + + def test_rescue_password(self): + self.run_command('rescue sample-server --password asdf') + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'adminPass': 'asdf'}}) + + def test_rescue_image(self): + self.run_command('rescue sample-server --image %s' % FAKE_UUID_1) + self.assert_called('POST', '/servers/1234/action', + {'rescue': {'rescue_image_ref': FAKE_UUID_1}}) + + def test_unrescue(self): + self.run_command('unrescue sample-server') + self.assert_called('POST', '/servers/1234/action', {'unrescue': None}) + + def test_shelve(self): + self.run_command('shelve sample-server') + self.assert_called('POST', '/servers/1234/action', {'shelve': None}) + + def test_shelve_offload(self): + self.run_command('shelve-offload sample-server') + self.assert_called('POST', '/servers/1234/action', + {'shelveOffload': None}) + + 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', + {'resize': {'flavorRef': 1}}) + + def test_resize_confirm(self): + self.run_command('resize-confirm sample-server') + self.assert_called('POST', '/servers/1234/action', + {'confirmResize': None}) + + def test_resize_revert(self): + self.run_command('resize-revert sample-server') + self.assert_called('POST', '/servers/1234/action', + {'revertResize': None}) + + @mock.patch('getpass.getpass', mock.Mock(return_value='p')) + def test_set_password(self): + self.run_command('set-password sample-server') + self.assert_called('POST', '/servers/1234/action', + {'changePassword': {'adminPass': 'p'}}) + + def test_show(self): + self.run_command('show 1234') + 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', '/flavors/1', pos=3) + self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=4) + + def test_show_no_image(self): + self.run_command('show 9012') + self.assert_called('GET', '/servers/9012', pos=-2) + self.assert_called('GET', '/flavors/1', pos=-1) + + def test_show_bad_id(self): + self.assertRaises(exceptions.CommandError, + self.run_command, 'show xxx') + + def test_show_unavailable_image_and_flavor(self): + output, _ = self.run_command('show 9013') + self.assert_called('GET', '/servers/9013', pos=-6) + self.assert_called('GET', + '/flavors/80645cf4-6ad3-410a-bbc8-6f3e1e291f51', + pos=-5) + self.assert_called('GET', + '/v2/images/3e861307-73a6-4d1f-8d68-f68b03223032', + pos=-1) + self.assertIn('Image not found', output) + self.assertIn('Flavor not found', output) + + 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') + args, kwargs = mock_print_dict.call_args + parsed_server = args[0] + self.assertEqual('securitygroup1, securitygroup2', + parsed_server['security_groups']) + + def test_delete(self): + self.run_command('delete 1234') + self.assert_called('DELETE', '/servers/1234') + self.run_command('delete sample-server') + self.assert_called('DELETE', '/servers/1234') + + def test_force_delete(self): + self.run_command('force-delete 1234') + self.assert_called('POST', '/servers/1234/action', + {'forceDelete': None}) + self.run_command('force-delete sample-server') + self.assert_called('POST', '/servers/1234/action', + {'forceDelete': None}) + + def test_restore(self): + self.run_command('restore 1234') + self.assert_called('POST', '/servers/1234/action', {'restore': None}) + self.run_command('restore sample-server') + self.assert_called('POST', '/servers/1234/action', {'restore': None}) + + def test_restore_withname(self): + self.run_command('restore sample-server') + self.assert_called('GET', + '/servers?deleted=True&name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('POST', '/servers/1234/action', {'restore': None}, + pos=2) + + def test_delete_two_with_two_existent(self): + self.run_command('delete 1234 5678') + self.assert_called('DELETE', '/servers/1234', pos=-5) + self.assert_called('DELETE', '/servers/5678', pos=-1) + self.run_command('delete sample-server sample-server2') + self.assert_called('GET', + '/servers?name=sample-server', pos=-6) + self.assert_called('GET', '/servers/1234', pos=-5) + self.assert_called('DELETE', '/servers/1234', pos=-4) + self.assert_called('GET', + '/servers?name=sample-server2', + pos=-3) + self.assert_called('GET', '/servers/5678', pos=-2) + self.assert_called('DELETE', '/servers/5678', pos=-1) + + def test_delete_two_with_two_existent_all_tenants(self): + self.run_command('delete sample-server sample-server2 --all-tenants') + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('DELETE', '/servers/1234', pos=2) + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server2', + pos=3) + self.assert_called('GET', '/servers/5678', pos=4) + self.assert_called('DELETE', '/servers/5678', pos=5) + + def test_delete_two_with_one_nonexistent(self): + cmd = 'delete 1234 123456789' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + self.assert_called_anytime('DELETE', '/servers/1234') + cmd = 'delete sample-server nonexistentserver' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + self.assert_called_anytime('DELETE', '/servers/1234') + + def test_delete_one_with_one_nonexistent(self): + cmd = 'delete 123456789' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'delete nonexistent-server1' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_delete_two_with_two_nonexistent(self): + cmd = 'delete 123456789 987654321' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + cmd = 'delete nonexistent-server1 nonexistent-server2' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_diagnostics(self): + self.run_command('diagnostics 1234') + self.assert_called('GET', '/servers/1234/diagnostics') + 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', + {'events': [{'name': 'network-changed', + 'server_uuid': '1234'}]}) + + def test_set_meta_set(self): + self.run_command('meta 1234 set key1=val1 key2=val2') + self.assert_called('POST', '/servers/1234/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}) + + def test_set_meta_delete_dict(self): + self.run_command('meta 1234 delete key1=val1 key2=val2') + self.assert_called('DELETE', '/servers/1234/metadata/key1') + self.assert_called('DELETE', '/servers/1234/metadata/key2', pos=-2) + + def test_set_meta_delete_keys(self): + self.run_command('meta 1234 delete key1 key2') + self.assert_called('DELETE', '/servers/1234/metadata/key1') + self.assert_called('DELETE', '/servers/1234/metadata/key2', pos=-2) + + def test_set_host_meta(self): + self.run_command('host-meta hyper set key1=val1 key2=val2') + self.assert_called('GET', '/os-hypervisors/hyper/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) + self.assert_called('POST', '/servers/uuid3/metadata', + {'metadata': {'key1': 'val1', 'key2': 'val2'}}, + pos=3) + self.assert_called('POST', '/servers/uuid4/metadata', + {'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_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' + stdout, _stderr = self.run_command(cmd) + self.assert_called('GET', + '/os-simple-tenant-usage?' + + 'start=2000-01-20T00:00:00&' + + 'end=2005-02-01T00:00:00&' + + 'detailed=1') + # 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' + stdout, _stderr = self.run_command(cmd, api_version='2.40') + self.assert_called('GET', + '/os-simple-tenant-usage?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'detailed=1', pos=0) + markers = [ + 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'f079e394-2222-457b-b350-bb5ecc685cdd', + ] + for pos, marker in enumerate(markers): + self.assert_called('GET', + '/os-simple-tenant-usage?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'marker=%s&detailed=1' % (marker), pos=pos + 1) + # 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)) + self.addCleanup(timeutils.clear_time_override) + self.run_command('usage-list') + self.assert_called('GET', + '/os-simple-tenant-usage?' + + 'start=2005-01-04T00:00:00&' + + 'end=2005-02-02T00:00:00&' + + 'detailed=1') + + def test_usage(self): + cmd = 'usage --start 2000-01-20 --end 2005-02-01 --tenant test' + stdout, _stderr = self.run_command(cmd) + self.assert_called('GET', + '/os-simple-tenant-usage/test?' + + 'start=2000-01-20T00:00:00&' + + 'end=2005-02-01T00:00:00') + # 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' + stdout, _stderr = self.run_command(cmd, api_version='2.40') + self.assert_called('GET', + '/os-simple-tenant-usage/tenant_id?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00', pos=0) + markers = [ + 'f079e394-1111-457b-b350-bb5ecc685cdd', + 'f079e394-2222-457b-b350-bb5ecc685cdd', + ] + for pos, marker in enumerate(markers): + self.assert_called('GET', + '/os-simple-tenant-usage/tenant_id?' + 'start=2000-01-20T00:00:00&' + 'end=2005-02-01T00:00:00&' + 'marker=%s' % (marker), pos=pos + 1) + # 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') + self.assert_called('GET', + '/os-simple-tenant-usage/tenant_id?' + + 'start=2000-01-20T00:00:00&' + + 'end=2005-02-01T00:00:00') + + def test_flavor_delete(self): + self.run_command("flavor-delete 2") + self.assert_called('DELETE', '/flavors/2') + + def test_flavor_create(self): + self.run_command("flavor-create flavorcreate " + "1234 512 10 1 --swap 1024 --ephemeral 10 " + "--is-public true") + 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') + self.assertNotIn('UUID', out) + + def test_aggregate_list_v2_41(self): + out, err = self.run_command('aggregate-list', api_version='2.41') + self.assert_called('GET', '/os-aggregates') + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) + self.assertIn('30827713-5957-4b68-8fd3-ccaddb568c24', out) + self.assertIn('9a651b22-ce3f-4a87-acd7-98446ef591c4', out) + + def test_aggregate_create(self): + out, err = self.run_command('aggregate-create test_name nova1') + body = {"aggregate": {"name": "test_name", + "availability_zone": "nova1"}} + self.assert_called('POST', '/os-aggregates', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertNotIn('UUID', out) + + def test_aggregate_create_v2_41(self): + out, err = self.run_command('aggregate-create test_name nova1', + api_version='2.41') + body = {"aggregate": {"name": "test_name", + "availability_zone": "nova1"}} + self.assert_called('POST', '/os-aggregates', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) + + def test_aggregate_delete_by_id(self): + self.run_command('aggregate-delete 1') + self.assert_called('DELETE', '/os-aggregates/1') + + def test_aggregate_delete_by_name(self): + self.run_command('aggregate-delete test') + self.assert_called('DELETE', '/os-aggregates/1') + + def test_aggregate_update_by_id(self): + out, err = self.run_command('aggregate-update 1 --name new_name') + body = {"aggregate": {"name": "new_name"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertNotIn('UUID', out) + + def test_aggregate_update_by_id_v2_41(self): + out, err = self.run_command('aggregate-update 1 --name new_name', + api_version='2.41') + body = {"aggregate": {"name": "new_name"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) + + def test_aggregate_update_by_name(self): + self.run_command('aggregate-update test --name new_name ') + body = {"aggregate": {"name": "new_name"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_with_availability_zone_by_id(self): + self.run_command('aggregate-update 1 --name foo ' + '--availability-zone new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + self.assert_called('PUT', '/os-aggregates/1', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_update_with_availability_zone_by_name(self): + self.run_command('aggregate-update test --name foo ' + '--availability-zone new_zone') + body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} + 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"}}} + self.assert_called('POST', '/os-aggregates/3/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/3', pos=-1) + self.assertNotIn('UUID', out) + + def test_aggregate_set_metadata_add_by_id_v2_41(self): + out, err = self.run_command('aggregate-set-metadata 3 foo=bar', + api_version='2.41') + body = {"set_metadata": {"metadata": {"foo": "bar"}}} + self.assert_called('POST', '/os-aggregates/3/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/3', pos=-1) + self.assertIn('UUID', out) + self.assertIn('9a651b22-ce3f-4a87-acd7-98446ef591c4', out) + + def test_aggregate_set_metadata_add_duplicate_by_id(self): + cmd = 'aggregate-set-metadata 3 test=dup' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_aggregate_set_metadata_delete_by_id(self): + self.run_command('aggregate-set-metadata 3 none_key') + body = {"set_metadata": {"metadata": {"none_key": None}}} + self.assert_called('POST', '/os-aggregates/3/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/3', pos=-1) + + def test_aggregate_set_metadata_delete_missing_by_id(self): + cmd = 'aggregate-set-metadata 3 delete_key2' + self.assertRaises(exceptions.CommandError, self.run_command, cmd) + + def test_aggregate_set_metadata_by_name(self): + self.run_command('aggregate-set-metadata test foo=bar') + body = {"set_metadata": {"metadata": {"foo": "bar"}}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_add_host_by_id(self): + out, err = self.run_command('aggregate-add-host 1 host1') + body = {"add_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertNotIn('UUID', out) + + def test_aggregate_add_host_by_id_v2_41(self): + out, err = self.run_command('aggregate-add-host 1 host1', + api_version='2.41') + body = {"add_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) + + def test_aggregate_add_host_by_name(self): + self.run_command('aggregate-add-host test host1') + body = {"add_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_remove_host_by_id(self): + out, err = self.run_command('aggregate-remove-host 1 host1') + body = {"remove_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertNotIn('UUID', out) + + def test_aggregate_remove_host_by_id_v2_41(self): + out, err = self.run_command('aggregate-remove-host 1 host1', + api_version='2.41') + body = {"remove_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) + + def test_aggregate_remove_host_by_name(self): + self.run_command('aggregate-remove-host test host1') + body = {"remove_host": {"host": "host1"}} + self.assert_called('POST', '/os-aggregates/1/action', body, pos=-2) + self.assert_called('GET', '/os-aggregates/1', pos=-1) + + def test_aggregate_show_by_id(self): + out, err = self.run_command('aggregate-show 1') + self.assert_called('GET', '/os-aggregates/1') + self.assertNotIn('UUID', out) + + def test_aggregate_show_by_id_v2_41(self): + out, err = self.run_command('aggregate-show 1', api_version='2.41') + self.assert_called('GET', '/os-aggregates/1') + self.assertIn('UUID', out) + self.assertIn('80785864-087b-45a5-a433-b20eac9b58aa', out) + + 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', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}}) + self.run_command('live-migration sample-server hostname' + ' --block-migrate') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': True, + 'disk_over_commit': False}}) + self.run_command('live-migration sample-server hostname' + ' --block-migrate --disk-over-commit') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': True, + 'disk_over_commit': True}}) + + def test_live_migration_v225(self): + self.run_command('live-migration sample-server hostname', + api_version='2.25') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + self.run_command('live-migration sample-server hostname' + ' --block-migrate', api_version='2.25') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': True}}) + self.run_command('live-migration sample-server', api_version='2.25') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': None, + 'block_migration': 'auto'}}) + + def test_live_migration_v2_30(self): + self.run_command('live-migration sample-server hostname', + api_version='2.30') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + 'block_migration': 'auto'}}) + self.run_command('live-migration --force sample-server hostname', + api_version='2.30') + self.assert_called('POST', '/servers/1234/action', + {'os-migrateLive': {'host': 'hostname', + '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') + self.assert_called('POST', '/servers/1234/migrations/1/action', + {'force_complete': None}) + + def test_list_migrations(self): + self.run_command('server-migration-list sample-server', + 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') + self.assert_called('DELETE', '/servers/1234/migrations/1') + + def test_host_evacuate_live_with_no_target_host(self): + self.run_command('host-evacuate-live hyper') + self.assert_called('GET', '/os-hypervisors/hyper/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) + 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) + 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) + 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') + self.assert_called('GET', '/os-hypervisors/hyper/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) + 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', + api_version='2.30') + self.assert_called('GET', '/os-hypervisors/hyper/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) + 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) + body = {'os-migrateLive': {'host': None, + 'block_migration': True, + 'disk_over_commit': False}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + 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') + self.assert_called('GET', '/os-hypervisors/hyper/servers', pos=0) + body = {'os-migrateLive': {'host': None, 'block_migration': True}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + 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) + body = {'os-migrateLive': {'host': None, + 'block_migration': False, + 'disk_over_commit': True}} + self.assert_called('POST', '/servers/uuid1/action', body, pos=1) + self.assert_called('POST', '/servers/uuid2/action', body, pos=2) + 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) + body = {'os-migrateLive': {'host': None, + 'block_migration': False, + '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', + {'os-resetState': {'state': 'error'}}) + self.run_command('reset-state sample-server --active') + self.assert_called('POST', '/servers/1234/action', + {'os-resetState': {'state': 'active'}}) + + def test_reset_state_with_all_tenants(self): + self.run_command('reset-state sample-server --all-tenants') + self.assert_called('GET', + '/servers?all_tenants=1&name=sample-server', pos=0) + self.assert_called('GET', '/servers/1234', pos=1) + self.assert_called('POST', '/servers/1234/action', + {'os-resetState': {'state': 'error'}}) + + def test_reset_state_multiple(self): + self.run_command('reset-state sample-server sample-server2') + self.assert_called('POST', '/servers/1234/action', + {'os-resetState': {'state': 'error'}}, pos=-4) + self.assert_called('POST', '/servers/5678/action', + {'os-resetState': {'state': 'error'}}, pos=-1) + + def test_reset_state_active_multiple(self): + self.run_command('reset-state --active sample-server sample-server2') + self.assert_called('POST', '/servers/1234/action', + {'os-resetState': {'state': 'active'}}, pos=-4) + self.assert_called('POST', '/servers/5678/action', + {'os-resetState': {'state': 'active'}}, pos=-1) + + def test_reset_network(self): + self.run_command('reset-network sample-server') + self.assert_called('POST', '/servers/1234/action', + {'resetNetwork': None}) + + def test_services_list(self): + self.run_command('service-list') + self.assert_called('GET', '/os-services') + + def test_services_list_v2_53(self): + """Tests nova service-list at the 2.53 microversion.""" + 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') + + def test_services_list_with_binary(self): + self.run_command('service-list --binary nova-cert') + self.assert_called('GET', '/os-services?binary=nova-cert') + + def test_services_list_with_host_binary(self): + self.run_command('service-list --host host1 --binary nova-cert') + self.assert_called('GET', '/os-services?host=host1&binary=nova-cert') + + def test_services_enable(self): + 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): + """Tests nova service-enable at the 2.53 microversion.""" + self.run_command('service-enable %s' % fakes.FAKE_SERVICE_UUID_1, + api_version='2.53') + body = {'status': 'enabled'} + self.assert_called( + 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, body) + + def test_services_disable(self): + 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): + """Tests nova service-disable at the 2.53 microversion.""" + self.run_command('service-disable %s' % fakes.FAKE_SERVICE_UUID_1, + api_version='2.53') + body = {'status': 'disabled'} + self.assert_called( + 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, body) + + def test_services_disable_with_reason(self): + 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) + + def test_services_disable_with_reason_v2_53(self): + """Tests nova service-disable --reason at microversion 2.53.""" + self.run_command('service-disable %s --reason no_reason' % + fakes.FAKE_SERVICE_UUID_1, api_version='2.53') + body = {'status': 'disabled', 'disabled_reason': 'no_reason'} + self.assert_called( + 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, body) + + def test_service_force_down_v2_53(self): + """Tests nova service-force-down at the 2.53 microversion.""" + self.run_command('service-force-down %s' % + fakes.FAKE_SERVICE_UUID_1, api_version='2.53') + body = {'forced_down': True} + self.assert_called( + 'PUT', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1, body) + + def test_services_delete(self): + self.run_command('service-delete 1') + self.assert_called('DELETE', '/os-services/1') + + def test_services_delete_v2_53(self): + """Tests nova service-delete at the 2.53 microversion.""" + self.run_command('service-delete %s' % fakes.FAKE_SERVICE_UUID_1) + self.assert_called( + 'DELETE', '/os-services/%s' % fakes.FAKE_SERVICE_UUID_1) + + def test_host_evacuate_v2_14(self): + self.run_command('host-evacuate hyper --target target_hyper', + api_version='2.14') + self.assert_called('GET', '/os-hypervisors/hyper/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) + self.assert_called('POST', '/servers/uuid3/action', + {'evacuate': {'host': 'target_hyper'}}, pos=3) + 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) + 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) + self.assert_called('POST', '/servers/uuid3/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': False}}, pos=3) + self.assert_called('POST', '/servers/uuid4/action', + {'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') + self.assert_called('GET', '/os-hypervisors/hyper/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) + self.assert_called('POST', '/servers/uuid3/action', + {'evacuate': {'host': 'target_hyper', 'force': True} + }, pos=3) + self.assert_called('POST', '/servers/uuid4/action', + {'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') + self.assert_called('GET', '/os-hypervisors/hyper/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) + self.assert_called('POST', '/servers/uuid3/action', + {'evacuate': {'host': 'target_hyper', + 'onSharedStorage': True}}, pos=3) + self.assert_called('POST', '/servers/uuid4/action', + {'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) + self.assert_called('POST', '/servers/uuid1/action', + {'evacuate': {'onSharedStorage': True}}, pos=1) + self.assert_called('POST', '/servers/uuid2/action', + {'evacuate': {'onSharedStorage': True}}, pos=2) + self.assert_called('POST', '/servers/uuid3/action', + {'evacuate': {'onSharedStorage': True}}, pos=3) + 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) + self.assert_called('POST', + '/servers/uuid1/action', {'migrate': None}, pos=1) + self.assert_called('POST', + '/servers/uuid2/action', {'migrate': None}, pos=2) + self.assert_called('POST', + '/servers/uuid3/action', {'migrate': None}, pos=3) + 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') + + def test_hypervisor_list_matching(self): + self.run_command('hypervisor-list --matching hyper') + self.assert_called('GET', '/os-hypervisors/hyper/search') + + def test_hypervisor_list_limit_marker(self): + self.run_command('hypervisor-list --limit 10 --marker hyper1', + api_version='2.33') + self.assert_called('GET', '/os-hypervisors?limit=10&marker=hyper1') + + def test_hypervisor_servers(self): + self.run_command('hypervisor-servers hyper') + self.assert_called('GET', '/os-hypervisors/hyper/servers') + + def test_hypervisor_show_by_id(self): + self.run_command('hypervisor-show 1234') + self.assert_called('GET', '/os-hypervisors/1234') + + def test_hypervisor_list_show_by_cell_id(self): + self.run_command('hypervisor-show region!child@1') + self.assert_called('GET', '/os-hypervisors/region!child@1') + + def test_hypervisor_show_by_name(self): + self.run_command('hypervisor-show hyper1') + self.assert_called('GET', '/os-hypervisors/hyper1') + + def test_hypervisor_uptime_by_id(self): + self.run_command('hypervisor-uptime 1234') + self.assert_called('GET', '/os-hypervisors/1234/uptime') + + def test_hypervisor_uptime_by_cell_id(self): + self.run_command('hypervisor-uptime region!child@1') + self.assert_called('GET', '/os-hypervisors/region!child@1/uptime') + + def test_hypervisor_uptime_by_name(self): + self.run_command('hypervisor-uptime hyper1') + self.assert_called('GET', '/os-hypervisors/1234/uptime') + + 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 ' + '97f4c221bff44578b0300df4ef119353') + self.assert_called( + 'GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + + def test_quota_show_detail(self): + self.run_command( + 'quota-show --tenant ' + '97f4c221bff44578b0300df4ef119353 --detail') + self.assert_called( + 'GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353/detail') + + def test_user_quota_show(self): + self.run_command( + 'quota-show --tenant ' + '97f4c221bff44578b0300df4ef119353 --user u1') + self.assert_called( + 'GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1') + + def test_user_quota_show_detail(self): + self.run_command( + 'quota-show --tenant ' + '97f4c221bff44578b0300df4ef119353 --user u1 --detail') + self.assert_called( + 'GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353/detail' + '?user_id=u1') + + def test_quota_show_no_tenant(self): + self.run_command('quota-show') + self.assert_called('GET', '/os-quota-sets/tenant_id') + + def test_quota_defaults(self): + self.run_command( + 'quota-defaults --tenant ' + '97f4c221bff44578b0300df4ef119353') + self.assert_called( + 'GET', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353/defaults') + + def test_quota_defaults_no_tenant(self): + self.run_command('quota-defaults') + self.assert_called('GET', '/os-quota-sets/tenant_id/defaults') + + def test_quota_update(self): + self.run_command( + 'quota-update 97f4c221bff44578b0300df4ef119353' + ' --instances=5') + self.assert_called( + 'PUT', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'instances': 5}}) + + def test_user_quota_update(self): + self.run_command( + 'quota-update 97f4c221bff44578b0300df4ef119353' + ' --user=u1' + ' --instances=5') + self.assert_called( + 'PUT', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1', + {'quota_set': {'instances': 5}}) + + def test_quota_force_update(self): + self.run_command( + 'quota-update 97f4c221bff44578b0300df4ef119353' + ' --instances=5 --force') + self.assert_called( + 'PUT', '/os-quota-sets/97f4c221bff44578b0300df4ef119353', + {'quota_set': {'force': True, + 'instances': 5}}) + + def test_quota_update_fixed_ip(self): + self.run_command( + 'quota-update 97f4c221bff44578b0300df4ef119353' + ' --fixed-ips=5') + self.assert_called( + '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') + self.assert_called('DELETE', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353') + + def test_user_quota_delete(self): + self.run_command('quota-delete --tenant ' + '97f4c221bff44578b0300df4ef119353 ' + '--user u1') + self.assert_called( + 'DELETE', + '/os-quota-sets/97f4c221bff44578b0300df4ef119353?user_id=u1') + + def test_quota_class_show(self): + self.run_command('quota-class-show test') + self.assert_called('GET', '/os-quota-class-sets/test') + + def test_quota_class_update(self): + # The list of args we can update. + args = ( + '--instances', '--cores', '--ram', '--floating-ips', '--fixed-ips', + '--metadata-items', '--injected-files', + '--injected-file-content-bytes', '--injected-file-path-bytes', + '--key-pairs', '--security-groups', '--security-group-rules', + '--server-groups', '--server-group-members' + ) + for arg in args: + self.run_command('quota-class-update ' + '97f4c221bff44578b0300df4ef119353 ' + '%s=5' % arg) + request_param = arg[2:].replace('-', '_') + body = {'quota_class_set': {request_param: 5}} + self.assert_called( + 'PUT', '/os-quota-class-sets/97f4c221bff44578b0300df4ef119353', + body) + + 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') + # With microversion < 2.45 there is no output from this command. + self.assertEqual(0, len(out)) + self.assert_called('POST', '/servers/1234/action', + {'createBackup': {'name': 'back1', + 'backup_type': 'daily', + 'rotation': '1'}}) + self.run_command('backup 1234 back1 daily 1') + self.assert_called('POST', '/servers/1234/action', + {'createBackup': {'name': 'back1', + 'backup_type': 'daily', + 'rotation': '1'}}) + + def test_backup_2_45(self): + """Tests the backup command with the 2.45 microversion which + handles a different response and prints out the backup snapshot + image details. + """ + out, err = self.run_command( + 'backup sample-server back1 daily 1', + api_version='2.45') + # We should see the backup snapshot image name in the output. + self.assertIn('back1', out) + self.assertIn('SAVING', out) + self.assert_called_anytime( + 'POST', '/servers/1234/action', + {'createBackup': {'name': 'back1', + 'backup_type': 'daily', + 'rotation': '1'}}) + + def test_limits(self): + 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') + + self.run_command('limits --tenant 1234') + self.assert_called('GET', '/limits?tenant_id=1234') + + stdout, _err = self.run_command('limits --tenant 1234') + 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', + {'evacuate': {'host': 'new_host', + 'onSharedStorage': False}}) + self.run_command('evacuate sample-server new_host ' + '--password NewAdminPass') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'onSharedStorage': False, + 'adminPass': 'NewAdminPass'}}) + self.run_command('evacuate sample-server new_host ' + '--on-shared-storage') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'onSharedStorage': True}}) + + def test_evacuate_v2_29(self): + self.run_command('evacuate sample-server new_host', api_version="2.29") + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host'}}) + self.run_command('evacuate sample-server new_host ' + '--password NewAdminPass', api_version="2.29") + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'host': 'new_host', + 'adminPass': 'NewAdminPass'}}) + self.run_command('evacuate --force sample-server new_host', + api_version="2.29") + self.assert_called('POST', '/servers/1234/action', + {'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', + {'evacuate': {'onSharedStorage': False}}) + self.run_command('evacuate sample-server --password NewAdminPass') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'onSharedStorage': False, + 'adminPass': 'NewAdminPass'}}) + self.run_command('evacuate sample-server --on-shared-storage') + self.assert_called('POST', '/servers/1234/action', + {'evacuate': {'onSharedStorage': True}}) + + def test_get_password(self): + self.run_command('get-password sample-server /foo/id_rsa') + self.assert_called('GET', '/servers/1234/os-server-password') + + def test_get_password_without_key(self): + self.run_command('get-password sample-server') + self.assert_called('GET', '/servers/1234/os-server-password') + + def test_clear_password(self): + self.run_command('clear-password sample-server') + self.assert_called('DELETE', '/servers/1234/os-server-password') + + 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', + {'addSecurityGroup': {'name': 'testgroup'}}) + + def test_server_security_group_remove(self): + self.run_command('remove-secgroup sample-server testgroup') + self.assert_called('POST', '/servers/1234/action', + {'removeSecurityGroup': {'name': 'testgroup'}}) + + def test_server_security_group_list(self): + self.run_command('list-secgroup 1234') + self.assert_called('GET', '/servers/1234/os-security-groups') + + def test_interface_list(self): + 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') + self.assert_called('POST', '/servers/1234/os-interface', + {'interfaceAttachment': {'port_id': 'port_id'}}) + + def test_interface_attach_with_tag_pre_v2_49(self): + self.assertRaises( + SystemExit, self.run_command, + 'interface-attach --port-id port_id --tag test_tag 1234', + api_version='2.48') + + def test_interface_attach_with_tag(self): + 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'}}) + 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): + 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') + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'device': '/dev/vdb', + 'volumeId': 'Work'}}) + + def test_volume_attach_without_device(self): + self.run_command('volume-attach sample-server Work') + self.assert_called('POST', '/servers/1234/os-volume_attachments', + {'volumeAttachment': + {'volumeId': 'Work'}}) + + def test_volume_attach_with_tag_pre_v2_49(self): + self.assertRaises( + SystemExit, self.run_command, + 'volume-attach --tag test_tag sample-server Work /dev/vdb', + api_version='2.48') + + def test_volume_attach_with_tag(self): + out = self.run_command( + 'volume-attach --tag test_tag sample-server Work /dev/vdb', + 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_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', + '/servers/1234/os-volume_attachments/Work') + + def test_instance_action_list(self): + self.run_command('instance-action-list sample-server') + self.assert_called('GET', '/servers/1234/os-instance-actions') + + def test_instance_action_get(self): + self.run_command('instance-action sample-server req-abcde12345') + self.assert_called( + 'GET', + '/servers/1234/os-instance-actions/req-abcde12345') + + 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_instance_usage_audit_log(self): + self.run_command('instance-usage-audit-log') + self.assert_called('GET', '/os-instance_usage_audit_log') + + 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): + 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): + class FakeResources(object): + addresses = { + "skynet": [ + {'version': 4, 'addr': "1.1.1.1", + "OS-EXT-IPS:type": 'fixed'}, + {'version': 4, 'addr': "2.2.2.2", + "OS-EXT-IPS:type": 'floating'}, + {'version': 6, 'addr': "2607:f0d0:1002::4", + "OS-EXT-IPS:type": 'fixed'}, + {'version': 6, 'addr': "7612:a1b2:2004::6"} + ] + } + mock_find_server.return_value = FakeResources() + + self.run_command("ssh --login bob server") + mock_system.assert_called_with("ssh -4 -p22 bob@2.2.2.2 ") + self.run_command("ssh alice@server") + mock_system.assert_called_with("ssh -4 -p22 alice@2.2.2.2 ") + self.run_command("ssh --port 202 server") + mock_system.assert_called_with("ssh -4 -p202 root@2.2.2.2 ") + self.run_command("ssh --private server") + mock_system.assert_called_with("ssh -4 -p22 root@1.1.1.1 ") + self.run_command("ssh -i ~/my_rsa_key server --private") + mock_system.assert_called_with("ssh -4 -p22 -i ~/my_rsa_key " + "root@1.1.1.1 ") + self.run_command("ssh --extra-opts -1 server") + mock_system.assert_called_with("ssh -4 -p22 root@2.2.2.2 -1") + + self.run_command("ssh --ipv6 --login carol server") + mock_system.assert_called_with("ssh -6 -p22 carol@7612:a1b2:2004::6 ") + self.run_command("ssh --ipv6 dan@server") + mock_system.assert_called_with("ssh -6 -p22 dan@7612:a1b2:2004::6 ") + self.run_command("ssh --ipv6 --port 2022 server") + mock_system.assert_called_with("ssh -6 -p2022 " + "root@7612:a1b2:2004::6 ") + self.run_command("ssh --ipv6 --private server") + mock_system.assert_called_with("ssh -6 -p22 root@2607:f0d0:1002::4 ") + self.run_command("ssh --ipv6 --identity /home/me/my_dsa_key " + "--private server") + mock_system.assert_called_with("ssh -6 -p22 -i /home/me/my_dsa_key " + "root@2607:f0d0:1002::4 ") + self.run_command("ssh --ipv6 --private --extra-opts -1 server") + mock_system.assert_called_with("ssh -6 -p22 " + "root@2607:f0d0:1002::4 -1") + + @mock.patch('novaclient.v2.shell._find_server') + @mock.patch('os.system') + def test_ssh_multinet(self, mock_system, mock_find_server): + class FakeResources(object): + addresses = { + "skynet": [ + {'version': 4, 'addr': "1.1.1.1", + "OS-EXT-IPS:type": 'fixed'}, + {'version': 4, 'addr': "2.2.2.2"}, + {'version': 6, 'addr': "2607:f0d0:1002::4", + "OS-EXT-IPS:type": 'fixed'} + ], + "other": [ + {'version': 4, 'addr': "2.3.4.5"}, + {'version': 6, 'addr': "7612:a1b2:2004::6"} + ] + } + mock_find_server.return_value = FakeResources() + + self.run_command("ssh --network other server") + mock_system.assert_called_with("ssh -4 -p22 root@2.3.4.5 ") + self.run_command("ssh --ipv6 --network other server") + mock_system.assert_called_with("ssh -6 -p22 root@7612:a1b2:2004::6 ") + self.assertRaises(exceptions.ResourceNotFound, + self.run_command, + "ssh --ipv6 --network nonexistent server") + + def _check_keypair_add(self, expected_key_type=None, extra_args='', + api_version=None): + self.run_command("keypair-add %s test" % extra_args, + api_version=api_version) + expected_body = {"keypair": {"name": "test"}} + if expected_key_type: + expected_body["keypair"]["type"] = expected_key_type + self.assert_called("POST", "/os-keypairs", expected_body) + + def test_keypair_add_v20(self): + self._check_keypair_add(api_version="2.0") + + def test_keypair_add_v22(self): + self._check_keypair_add('ssh', api_version="2.2") + + def test_keypair_add_ssh(self): + self._check_keypair_add('ssh', '--key-type ssh', api_version="2.2") + + def test_keypair_add_ssh_x509(self): + self._check_keypair_add('x509', '--key-type x509', api_version="2.2") + + def _check_keypair_import(self, expected_key_type=None, extra_args='', + api_version=None): + with mock.patch.object(builtins, 'open', + mock.mock_open(read_data='FAKE_PUBLIC_KEY')): + self.run_command('keypair-add --pub-key test.pub %s test' % + extra_args, api_version=api_version) + expected_body = {"keypair": {'public_key': 'FAKE_PUBLIC_KEY', + 'name': 'test'}} + if expected_key_type: + expected_body["keypair"]["type"] = expected_key_type + self.assert_called( + 'POST', '/os-keypairs', expected_body) + + def test_keypair_import_v20(self): + self._check_keypair_import(api_version="2.0") + + def test_keypair_import_v22(self): + self._check_keypair_import('ssh', api_version="2.2") + + def test_keypair_import_ssh(self): + self._check_keypair_import('ssh', '--key-type ssh', api_version="2.2") + + def test_keypair_import_x509(self): + self._check_keypair_import('x509', '--key-type x509', + api_version="2.2") + + def test_keypair_stdin(self): + 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', { + 'keypair': + {'public_key': 'FAKE_PUBLIC_KEY', 'name': 'test', + 'type': 'ssh'}}) + + def test_keypair_list(self): + self.run_command('keypair-list') + self.assert_called('GET', '/os-keypairs') + + def test_keypair_list_with_user_id(self): + self.run_command('keypair-list --user test_user', api_version='2.10') + self.assert_called('GET', '/os-keypairs?user_id=test_user') + + def test_keypair_list_with_limit_and_marker(self): + self.run_command('keypair-list --marker test_kp --limit 3', + api_version='2.35') + self.assert_called('GET', '/os-keypairs?limit=3&marker=test_kp') + + def test_keypair_list_with_user_id_limit_and_marker(self): + self.run_command('keypair-list --user test_user --marker test_kp ' + '--limit 3', api_version='2.35') + self.assert_called( + 'GET', '/os-keypairs?limit=3&marker=test_kp&user_id=test_user') + + def test_keypair_show(self): + self.run_command('keypair-show test') + self.assert_called('GET', '/os-keypairs/test') + + def test_keypair_delete(self): + self.run_command('keypair-delete test') + self.assert_called('DELETE', '/os-keypairs/test') + + def test_create_server_group(self): + self.run_command('server-group-create wjsg affinity') + self.assert_called('POST', '/os-server-groups', + {'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') + self.assert_called('DELETE', '/os-server-groups/12345', pos=-2) + + def test_list_server_group(self): + self.run_command('server-group-list') + self.assert_called('GET', '/os-server-groups') + + def test_list_server_group_with_all_projects(self): + self.run_command('server-group-list --all-projects') + self.assert_called('GET', '/os-server-groups?all_projects=True') + + 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_versions(self): + exclusions = set([ + 1, # Same as version 2.0 + 3, # doesn't require any changes in novaclient + 4, # fixed-ip-get command is gone + 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 + # HttpClient and our SessionClient, which is based on + # keystoneauth1.session. Skipping this complicated change + # allows to unblock implementation further microversions + # before feature-freeze + # (we can do it, since nova-api change didn't actually add + # new microversion, just an additional checks. See + # 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. + 28, # doesn't require any changes in novaclient + 31, # doesn't require any changes in novaclient + 32, # doesn't require separate version-wrapped methods in + # novaclient + 34, # doesn't require any changes in novaclient + 37, # There are no versioned wrapped shell method changes for this + 38, # doesn't require any changes in novaclient + 39, # There are no versioned wrapped shell method changes for this + 41, # There are no version-wrapped shell method changes for this. + 42, # There are no version-wrapped shell method changes for this. + 43, # There are no version-wrapped shell method changes for this. + 44, # There are no version-wrapped shell method changes for this. + 45, # There are no version-wrapped shell method changes for this. + 46, # There are no version-wrapped shell method changes for this. + 47, # NOTE(cfriesen): 47 adds support for flavor details embedded + # within the server details + 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(): + # 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 + + failure_msg = ('Minor versions %s have been skipped. Please do not ' + 'raise API_MAX_VERSION without adding support or ' + 'excluding them.' % sorted(unaccounted_for)) + self.assertEqual(set([]), unaccounted_for, failure_msg) + + def test_list_v2_10(self): + self.run_command('list', api_version='2.10') + self.assert_called('GET', '/servers/detail') + + def test_server_tag_add(self): + self.run_command('server-tag-add sample-server tag', + api_version='2.26') + self.assert_called('PUT', '/servers/1234/tags/tag', None) + + def test_server_tag_add_many(self): + self.run_command('server-tag-add sample-server tag1 tag2 tag3', + api_version='2.26') + self.assert_called('PUT', '/servers/1234/tags/tag1', None, pos=-3) + self.assert_called('PUT', '/servers/1234/tags/tag2', None, pos=-2) + self.assert_called('PUT', '/servers/1234/tags/tag3', None, pos=-1) + + def test_server_tag_set(self): + self.run_command('server-tag-set sample-server tag1 tag2', + api_version='2.26') + self.assert_called('PUT', '/servers/1234/tags', + {'tags': ['tag1', 'tag2']}) + + def test_server_tag_list(self): + self.run_command('server-tag-list sample-server', api_version='2.26') + self.assert_called('GET', '/servers/1234/tags') + + def test_server_tag_delete(self): + self.run_command('server-tag-delete sample-server tag', + api_version='2.26') + self.assert_called('DELETE', '/servers/1234/tags/tag') + + def test_server_tag_delete_many(self): + self.run_command('server-tag-delete sample-server tag1 tag2 tag3', + api_version='2.26') + self.assert_called('DELETE', '/servers/1234/tags/tag1', pos=-3) + self.assert_called('DELETE', '/servers/1234/tags/tag2', pos=-2) + self.assert_called('DELETE', '/servers/1234/tags/tag3', pos=-1) + + def test_server_tag_delete_all(self): + self.run_command('server-tag-delete-all sample-server', + api_version='2.26') + self.assert_called('DELETE', '/servers/1234/tags') + + def test_list_v2_26_tags(self): + self.run_command('list --tags tag1,tag2', api_version='2.26') + self.assert_called('GET', '/servers/detail?tags=tag1%2Ctag2') + + def test_list_v2_26_tags_any(self): + self.run_command('list --tags-any tag1,tag2', api_version='2.26') + self.assert_called('GET', '/servers/detail?tags-any=tag1%2Ctag2') + + def test_list_v2_26_not_tags(self): + self.run_command('list --not-tags tag1,tag2', api_version='2.26') + self.assert_called('GET', '/servers/detail?not-tags=tag1%2Ctag2') + + 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") + def test_simple_usage(self, mock_time): + poll_period = 3 + some_id = "uuuuuuuuuuuiiiiiiiii" + updated_objects = ( + base.Resource(None, info={"not_default_field": "INPROGRESS"}), + base.Resource(None, info={"not_default_field": "OK"})) + poll_fn = mock.MagicMock(side_effect=updated_objects) + + novaclient.v2.shell._poll_for_status( + poll_fn=poll_fn, + obj_id=some_id, + status_field="not_default_field", + final_ok_states=["ok"], + poll_period=poll_period, + # just want to test printing in separate tests + action="some", + silent=True, + show_progress=False + ) + self.assertEqual([mock.call(poll_period)], + mock_time.sleep.call_args_list) + self.assertEqual([mock.call(some_id)] * 2, poll_fn.call_args_list) + + @mock.patch("novaclient.v2.shell.sys.stdout") + @mock.patch("novaclient.v2.shell.time") + def test_print_progress(self, mock_time, mock_stdout): + updated_objects = ( + base.Resource(None, info={"status": "INPROGRESS", "progress": 0}), + base.Resource(None, info={"status": "INPROGRESS", "progress": 50}), + base.Resource(None, info={"status": "OK", "progress": 100})) + poll_fn = mock.MagicMock(side_effect=updated_objects) + action = "some" + + novaclient.v2.shell._poll_for_status( + poll_fn=poll_fn, + obj_id="uuuuuuuuuuuiiiiiiiii", + final_ok_states=["ok"], + poll_period="3", + action=action, + show_progress=True, + silent=False) + + stdout_arg_list = [ + mock.call("\n"), + mock.call("\rServer %s... 0%% complete" % action), + mock.call("\rServer %s... 50%% complete" % action), + mock.call("\rServer %s... 100%% complete" % action), + mock.call("\nFinished"), + mock.call("\n")] + self.assertEqual( + stdout_arg_list, + mock_stdout.write.call_args_list + ) + + @mock.patch("novaclient.v2.shell.time") + def test_error_state(self, mock_time): + fault_msg = "Oops" + updated_objects = ( + base.Resource(None, info={"status": "error", + "fault": {"message": fault_msg}}), + base.Resource(None, info={"status": "error"})) + poll_fn = mock.MagicMock(side_effect=updated_objects) + action = "some" + + self.assertRaises(exceptions.ResourceInErrorState, + novaclient.v2.shell._poll_for_status, + poll_fn=poll_fn, + obj_id="uuuuuuuuuuuiiiiiiiii", + final_ok_states=["ok"], + poll_period="3", + action=action, + show_progress=True, + silent=False) + + self.assertRaises(exceptions.ResourceInErrorState, + novaclient.v2.shell._poll_for_status, + poll_fn=poll_fn, + obj_id="uuuuuuuuuuuiiiiiiiii", + final_ok_states=["ok"], + poll_period="3", + 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 new file mode 100644 index 000000000..c7473aa9d --- /dev/null +++ b/novaclient/tests/unit/v2/test_usage.py @@ -0,0 +1,122 @@ +# +# 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 novaclient import api_versions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import usage + + +class UsageTest(utils.TestCase): + def setUp(self): + super(UsageTest, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) + self.usage_type = self._get_usage_type() + + def _get_usage_type(self): + return usage.Usage + + def test_usage_list(self, detailed=False): + now = datetime.datetime.now() + usages = self.cs.usage.list(now, now, detailed) + self.assert_request_id(usages, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + "/os-simple-tenant-usage?" + + ("start=%s&" % now.isoformat()) + + ("end=%s&" % now.isoformat()) + + ("detailed=%s" % int(bool(detailed)))) + for u in usages: + self.assertIsInstance(u, usage.Usage) + + def test_usage_list_detailed(self): + self.test_usage_list(True) + + def test_usage_get(self): + now = datetime.datetime.now() + u = self.cs.usage.get("tenantfoo", now, now) + self.assert_request_id(u, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + "/os-simple-tenant-usage/tenantfoo?" + + ("start=%s&" % now.isoformat()) + + ("end=%s" % now.isoformat())) + self.assertIsInstance(u, usage.Usage) + + def test_usage_class_get(self): + start = '2012-01-22T19:48:41.750722' + stop = '2012-01-22T19:48:41.750722' + + info = {'tenant_id': 'tenantfoo', 'start': start, + 'stop': stop} + u = usage.Usage(self.cs.usage, info) + u.get() + self.assert_request_id(u, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + "/os-simple-tenant-usage/tenantfoo?start=%s&end=%s" % + (start, stop)) + + +class UsageV40Test(UsageTest): + def setUp(self): + super(UsageV40Test, self).setUp() + self.cs.api_version = api_versions.APIVersion('2.40') + + def test_usage_list_with_paging(self): + now = datetime.datetime.now() + usages = self.cs.usage.list(now, now, marker='some-uuid', limit=3) + self.assert_request_id(usages, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + '/os-simple-tenant-usage?' + + ('start=%s&' % now.isoformat()) + + ('end=%s&' % now.isoformat()) + + ('limit=3&marker=some-uuid&detailed=0')) + for u in usages: + self.assertIsInstance(u, usage.Usage) + + def test_usage_list_detailed_with_paging(self): + now = datetime.datetime.now() + usages = self.cs.usage.list( + now, now, detailed=True, marker='some-uuid', limit=3) + self.assert_request_id(usages, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + '/os-simple-tenant-usage?' + + ('start=%s&' % now.isoformat()) + + ('end=%s&' % now.isoformat()) + + ('limit=3&marker=some-uuid&detailed=1')) + for u in usages: + self.assertIsInstance(u, usage.Usage) + + def test_usage_get_with_paging(self): + now = datetime.datetime.now() + u = self.cs.usage.get( + 'tenantfoo', now, now, marker='some-uuid', limit=3) + self.assert_request_id(u, fakes.FAKE_REQUEST_ID_LIST) + + self.cs.assert_called( + 'GET', + '/os-simple-tenant-usage/tenantfoo?' + + ('start=%s&' % now.isoformat()) + + ('end=%s&' % now.isoformat()) + + ('limit=3&marker=some-uuid')) + self.assertIsInstance(u, usage.Usage) diff --git a/novaclient/tests/unit/v2/test_versions.py b/novaclient/tests/unit/v2/test_versions.py new file mode 100644 index 000000000..23b72840e --- /dev/null +++ b/novaclient/tests/unit/v2/test_versions.py @@ -0,0 +1,102 @@ +# Copyright 2015 NEC Corporation. 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 unittest import mock + +from novaclient import api_versions +from novaclient import exceptions as exc +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import versions + + +class VersionsTest(utils.TestCase): + def setUp(self): + super(VersionsTest, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion("2.0")) + self.service_type = versions.Version + + def test_list_services(self): + vl = self.cs.versions.list() + self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', 'http://nova-api:8774') + + def test_get_current(self): + self.cs.callback = [] + v = self.cs.versions.get_current() + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', 'http://nova-api:8774/v2.1/') + + @mock.patch.object(versions.VersionManager, '_get', + side_effect=exc.Unauthorized("401 RAX")) + def test_get_current_with_rax_workaround(self, get): + self.cs.callback = [] + self.assertIsNone(self.cs.versions.get_current()) + + def test_get_endpoint_without_project_id(self): + # create a fake client such that get_endpoint() + # doesn't return uuid in url + endpoint_type = 'v2.1' + expected_endpoint = 'http://nova-api:8774/v2.1/' + cs_2_1 = fakes.FakeClient(api_versions.APIVersion("2.0"), + endpoint_type=endpoint_type) + + result = cs_2_1.versions.get_current() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assertEqual(result.manager.api.client.endpoint_type, + endpoint_type, "Check endpoint_type was set") + + # check that the full request works as expected + cs_2_1.assert_called('GET', expected_endpoint) + + def test_v2_get_endpoint_without_project_id(self): + # create a fake client such that get_endpoint() + # doesn't return uuid in url + endpoint_type = 'v2' + expected_endpoint = 'http://nova-api:8774/v2/' + cs_2 = fakes.FakeClient(api_versions.APIVersion("2.0"), + endpoint_type=endpoint_type) + + result = cs_2.versions.get_current() + self.assert_request_id(result, fakes.FAKE_REQUEST_ID_LIST) + self.assertEqual(result.manager.api.client.endpoint_type, + endpoint_type, "Check v2 endpoint_type was set") + + # check that the full request works as expected + cs_2.assert_called('GET', expected_endpoint) + + def test_list_versions(self): + fapi = mock.Mock() + version_mgr = versions.VersionManager(fapi) + version_mgr._list = mock.Mock() + data = [ + ("https://example.com:777/v2", "https://example.com:777"), + ("https://example.com/v2", "https://example.com"), + ("http://example.com/compute/v2", "http://example.com/compute"), + ("https://example.com/v2/prrrooojeect-uuid", + "https://example.com"), + ("https://example.com:777/v2.1", "https://example.com:777"), + ("https://example.com/v2.1", "https://example.com"), + ("http://example.com/compute/v2.1", "http://example.com/compute"), + ("https://example.com/v2.1/prrrooojeect-uuid", + "https://example.com"), + ("http://example.com/compute", "http://example.com/compute"), + ("http://compute.example.com", "http://compute.example.com"), + ] + + for endpoint, expected in data: + version_mgr._list.reset_mock() + fapi.client.get_endpoint.return_value = endpoint + version_mgr.list() + version_mgr._list.assert_called_once_with(expected, "versions") diff --git a/novaclient/tests/unit/v2/test_volumes.py b/novaclient/tests/unit/v2/test_volumes.py new file mode 100644 index 000000000..c06661186 --- /dev/null +++ b/novaclient/tests/unit/v2/test_volumes.py @@ -0,0 +1,181 @@ +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from novaclient import api_versions +from novaclient.tests.unit import utils +from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import volumes + + +class VolumesTest(utils.TestCase): + api_version = "2.0" + + def setUp(self): + super(VolumesTest, self).setUp() + self.cs = fakes.FakeClient(api_versions.APIVersion(self.api_version)) + + def test_create_server_volume(self): + v = self.cs.volumes.create_server_volume( + server_id=1234, + volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', + device='/dev/vdb' + ) + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('POST', '/servers/1234/os-volume_attachments') + self.assertIsInstance(v, volumes.Volume) + + def test_update_server_volume(self): + vol_id = '15e59938-07d5-11e1-90e3-e3dffe0c5983' + v = self.cs.volumes.update_server_volume( + server_id=1234, + src_volid='Work', + dest_volid=vol_id + ) + 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_get_server_volume(self): + v = self.cs.volumes.get_server_volume(1234, 'Work') + self.assert_request_id(v, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', + '/servers/1234/os-volume_attachments/Work') + self.assertIsInstance(v, volumes.Volume) + + def test_get_server_volume_with_exception(self): + self.assertRaises(TypeError, + self.cs.volumes.get_server_volume, + "1234") + + self.assertRaises(TypeError, + self.cs.volumes.get_server_volume, + "1234", + volume_id="Work", + attachment_id="123") + + @mock.patch('warnings.warn') + def test_get_server_volume_with_warn(self, mock_warn): + self.cs.volumes.get_server_volume(1234, + volume_id=None, + attachment_id="Work") + mock_warn.assert_called_once() + + def test_list_server_volumes(self): + vl = self.cs.volumes.get_server_volumes(1234) + self.assert_request_id(vl, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', + '/servers/1234/os-volume_attachments') + for v in vl: + self.assertIsInstance(v, volumes.Volume) + + def test_delete_server_volume(self): + ret = self.cs.volumes.delete_server_volume(1234, 'Work') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('DELETE', + '/servers/1234/os-volume_attachments/Work') + + +class VolumesV249Test(VolumesTest): + api_version = "2.49" + + def test_create_server_volume_with_tag(self): + v = self.cs.volumes.create_server_volume( + server_id=1234, + volume_id='15e59938-07d5-11e1-90e3-e3dffe0c5983', + device='/dev/vdb', + tag='test_tag' + ) + 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': 'test_tag'}}) + self.assertIsInstance(v, volumes.Volume) + + def test_delete_server_volume_with_exception(self): + self.assertRaises(TypeError, + self.cs.volumes.delete_server_volume, + "1234") + + self.assertRaises(TypeError, + self.cs.volumes.delete_server_volume, + "1234", + volume_id="Work", + attachment_id="123") + + @mock.patch('warnings.warn') + def test_delete_server_volume_with_warn(self, mock_warn): + self.cs.volumes.delete_server_volume(1234, + 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/tests/v1_1/testfile.txt b/novaclient/tests/unit/v2/testfile.txt similarity index 100% rename from tests/v1_1/testfile.txt rename to novaclient/tests/unit/v2/testfile.txt diff --git a/novaclient/utils.py b/novaclient/utils.py index 02c961188..d0219795c 100644 --- a/novaclient/utils.py +++ b/novaclient/utils.py @@ -1,125 +1,114 @@ +# +# 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 contextlib import os import re -import sys -import uuid +import textwrap +import time +from urllib import parse +from oslo_serialization import jsonutils +from oslo_utils import encodeutils +from oslo_utils import uuidutils import prettytable from novaclient import exceptions +from novaclient.i18n import _ -def arg(*args, **kwargs): - """Decorator for CLI args.""" - def _decorator(func): - add_arg(func, *args, **kwargs) - return func - return _decorator +VALID_KEY_REGEX = re.compile(r"[\w\.\- :]+$", re.UNICODE) def env(*args, **kwargs): - """ - returns the first environment variable set - if none are non-empty, defaults to '' or keyword arg default + """Returns the first environment variable set. + + If all are empty, defaults to '' or keyword arg `default`. """ for arg in args: - value = os.environ.get(arg, None) + value = os.environ.get(arg) if value: return value return kwargs.get('default', '') -def add_arg(f, *args, **kwargs): - """Bind CLI arguments to a shell.py `do_foo` function.""" +def get_service_type(f): + """Retrieves service type from function.""" + return getattr(f, 'service_type', None) - if not hasattr(f, 'arguments'): - f.arguments = [] - # NOTE(sirp): avoid dups that can occur when the module is shared across - # tests. - if (args, kwargs) not in f.arguments: - # Because of the sematics of decorator composition if we just append - # to the options list positional options will appear to be backwards. - f.arguments.insert(0, (args, kwargs)) +def unauthenticated(func): + """Adds 'unauthenticated' attribute to decorated function. + Usage: -def bool_from_str(val): - """Convert a string representation of a bool into a bool value""" + >>> @unauthenticated + ... def mymethod(f): + ... pass + """ + func.unauthenticated = True + return func - if not val: - return False - try: - return bool(int(val)) - except ValueError: - if val.lower() in ['true', 'yes', 'y']: - return True - if val.lower() in ['false', 'no', 'n']: - return False - raise +def isunauthenticated(func): + """Checks if the function does not require authentication. -def add_resource_manager_extra_kwargs_hook(f, hook): - """Adds hook to bind CLI arguments to ResourceManager calls. + Mark such functions with the `@unauthenticated` decorator. - 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. + :returns: bool """ - if not hasattr(f, 'resource_manager_kwargs_hooks'): - f.resource_manager_kwargs_hooks = [] + return getattr(func, 'unauthenticated', False) - 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 arg(*args, **kwargs): + """Decorator for CLI args. -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_name = hook.__name__ - hook_kwargs = hook(args) - - conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys()) - if conflicting_keys and not allow_conflicts: - raise Exception("Hook '%(hook_name)s' is attempting to redefine" - " attributes '%(conflicting_keys)s'" % locals()) - - extra_kwargs.update(hook_kwargs) + Example: - return extra_kwargs + >>> @arg("name", help="Name of the new entity") + ... def entity_create(args): + ... pass + """ + def _decorator(func): + add_arg(func, *args, **kwargs) + return func + return _decorator -def unauthenticated(f): - """ - Adds 'unauthenticated' attribute to decorated function. - Usage: - @unauthenticated - def mymethod(f): - ... - """ - f.unauthenticated = True - return f +def add_arg(func, *args, **kwargs): + """Bind CLI arguments to a shell.py `do_foo` function.""" + if not hasattr(func, 'arguments'): + func.arguments = [] -def isunauthenticated(f): - """ - Checks to see if the function is marked as not requiring authentication - with the @unauthenticated decorator. Returns True if decorator is - set to True, False otherwise. - """ - return getattr(f, 'unauthenticated', False) + # NOTE(sirp): avoid dups that can occur when the module is shared across + # tests. + if (args, kwargs) not in func.arguments: + # Because of the semantics of decorator composition if we just append + # to the options list positional options will appear to be backwards. + func.arguments.insert(0, (args, kwargs)) def service_type(stype): - """ - Adds 'service_type' attribute to decorated function. + """Adds 'service_type' attribute to decorated function. + Usage: - @service_type('volume') - def mymethod(f): - ... + + .. code-block:: python + + @service_type('volume') + def mymethod(f): + ... """ def inner(f): f.service_type = stype @@ -127,19 +116,18 @@ def inner(f): return inner -def get_service_type(f): - """ - Retrieves service type from function - """ - return getattr(f, 'service_type', None) +def pretty_choice_list(values): + return ', '.join("'%s'" % x for x in values) -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) +def pretty_choice_dict(values): + """Returns a formatted dict as 'key=value'.""" + return pretty_choice_list( + ['%s=%s' % (k, values[k]) for k in sorted(values)]) -def print_list(objs, fields, formatters={}, sortby_index=0): - if sortby_index == None: +def print_list(objs, fields, formatters={}, sortby_index=None): + if sortby_index is None: sortby = None else: sortby = fields[sortby_index] @@ -158,64 +146,155 @@ def print_list(objs, fields, formatters={}, sortby_index=0): else: field_name = field.lower().replace(' ', '_') data = getattr(o, field_name, '') + if data is None: + data = '-' + # '\r' would break the table, so remove it. + data = str(data).replace("\r", "") row.append(data) pt.add_row(row) - print pt.get_string(sortby=sortby) + if sortby is not None: + result = encodeutils.safe_encode(pt.get_string(sortby=sortby)) + else: + result = encodeutils.safe_encode(pt.get_string()) + result = result.decode() -def print_dict(d, dict_property="Property"): - pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False) - pt.align = 'l' - [pt.add_row(list(r)) for r in d.iteritems()] - print pt.get_string(sortby=dict_property) + print(result) -def find_resource(manager, name_or_id): - """Helper for the _find_* methods.""" - # first try to get entity as integer id - try: - is_intid = isinstance(name_or_id, int) or name_or_id.isdigit() - except AttributeError: - is_intid = False +def _flatten(data, prefix=None): + """Flatten a dict, using name as a prefix for the keys of dict. + + >>> _flatten('cpu_info', {'arch':'x86_64'}) + [('cpu_info_arch': 'x86_64')] - if is_intid: + """ + if isinstance(data, dict): + for key, value in data.items(): + new_key = '%s_%s' % (prefix, key) if prefix else key + if isinstance(value, (dict, list)) and value: + for item in _flatten(value, new_key): + yield item + else: + yield new_key, value + else: + yield prefix, data + + +def flatten_dict(data): + """Return a new dict whose sub-dicts have been merged into the + original. Each of the parents keys are prepended to the child's + to prevent collisions. Any string elements will be JSON parsed + before flattening. + + >>> flatten_dict({'service': {'host':'cloud9@compute-068', 'id': 143}}) + {'service_host': colud9@compute-068', 'service_id': 143} + + """ + data = data.copy() + # Try and decode any nested JSON structures. + for key, value in data.items(): + if isinstance(value, str): + try: + data[key] = jsonutils.loads(value) + except ValueError: + pass + + return dict(_flatten(data)) + + +def print_dict(d, dict_property="Property", dict_value="Value", wrap=0): + pt = prettytable.PrettyTable([dict_property, dict_value], caching=False) + pt.align = 'l' + for k, v in sorted(d.items()): + # convert dict to str to check length + if isinstance(v, (dict, list)): + v = jsonutils.dumps(v, ensure_ascii=False) + if wrap > 0: + 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, 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', '') + lines = v.strip().split(r'\n') + col1 = k + for line in lines: + pt.add_row([col1, line]) + col1 = '' + else: + if v is None: + v = '-' + pt.add_row([k, v]) + + result = encodeutils.safe_encode(pt.get_string()) + + result = result.decode() + + print(result) + + +def find_resource(manager, name_or_id, wrap_exception=True, **find_args): + """Helper for the _find_* methods.""" + # for str id which is not uuid (for Flavor, Keypair and hypervsior in cells + # environments search currently) + if getattr(manager, 'is_alphanum_id_allowed', False): try: - return manager.get(int(name_or_id)) + return manager.get(name_or_id) except exceptions.NotFound: pass - # now try to get entity as uuid + # first try to get entity as uuid try: - uuid.UUID(str(name_or_id)) - return manager.get(name_or_id) - except (ValueError, exceptions.NotFound): + tmp_id = encodeutils.safe_encode(name_or_id) + + tmp_id = tmp_id.decode() + + if uuidutils.is_uuid_like(tmp_id): + return manager.get(tmp_id) + except (TypeError, exceptions.NotFound): pass + # then try to get entity as name try: - try: - return manager.find(human_id=name_or_id) - except exceptions.NotFound: - pass - - # finally try to find entity by name try: resource = getattr(manager, 'resource_class', None) name_attr = resource.NAME_ATTR if resource else 'name' kwargs = {name_attr: name_or_id} + kwargs.update(find_args) return manager.find(**kwargs) except exceptions.NotFound: - msg = "No %s with a name or ID of '%s' exists." % \ - (manager.resource_class.__name__.lower(), name_or_id) - raise exceptions.CommandError(msg) + pass + + # then try to find entity by human_id + try: + return manager.find(human_id=name_or_id, **find_args) + except exceptions.NotFound: + pass except exceptions.NoUniqueMatch: - msg = ("Multiple %s matches found for '%s', use an ID to be more" - " specific." % (manager.resource_class.__name__.lower(), - name_or_id)) - raise exceptions.CommandError(msg) + msg = (_("Multiple %(class)s matches found for '%(name)s', use an ID " + "to be more specific.") % + {'class': manager.resource_class.__name__.lower(), + 'name': name_or_id}) + if wrap_exception: + raise exceptions.CommandError(msg) + raise exceptions.NoUniqueMatch(msg) + + # finally try to get entity as integer id + try: + return manager.get(int(name_or_id)) + except (TypeError, ValueError, exceptions.NotFound): + msg = (_("No %(class)s with a name or ID of '%(name)s' exists.") % + {'class': manager.resource_class.__name__.lower(), + 'name': name_or_id}) + if wrap_exception: + raise exceptions.CommandError(msg) + raise exceptions.NotFound(404, msg) -def _format_servers_list_networks(server): +def format_servers_list_networks(server): output = [] for (network, addresses) in server.networks.items(): if len(addresses) == 0: @@ -227,22 +306,44 @@ def _format_servers_list_networks(server): return '; '.join(output) -class HookableMixin(object): - """Mixin so classes can register and run hooks.""" - _hooks_map = {} +def format_security_groups(groups): + return ', '.join(group['name'] for group in groups) + - @classmethod - def add_hook(cls, hook_type, hook_func): - if hook_type not in cls._hooks_map: - cls._hooks_map[hook_type] = [] +def _format_field_name(attr): + """Format an object attribute in a human-friendly way.""" + # Split at ':' and leave the extension name as-is. + parts = attr.rsplit(':', 1) + name = parts[-1].replace('_', ' ') + # Don't title() on mixed case + if name.isupper() or name.islower(): + name = name.title() + parts[-1] = name + return ': '.join(parts) - cls._hooks_map[hook_type].append(hook_func) - @classmethod - def run_hooks(cls, hook_type, *args, **kwargs): - hook_funcs = cls._hooks_map.get(hook_type) or [] - for hook_func in hook_funcs: - hook_func(*args, **kwargs) +def make_field_formatter(attr, filters=None): + """ + Given an object attribute, return a formatted field name and a + formatter suitable for passing to print_list. + + Optionally pass a dict mapping attribute names to a function. The function + will be passed the value of the attribute and should return the string to + display. + """ + filter_ = None + if filters: + filter_ = filters.get(attr) + + def get_field(obj): + field = getattr(obj, attr, '') + if field and filter_: + field = filter_(field) + return field + + name = _format_field_name(attr) + formatter = get_field + return name, formatter def safe_issubclass(*args): @@ -257,28 +358,82 @@ def safe_issubclass(*args): return False -def import_class(import_str): - """Returns a class from a string including module and class.""" - mod_str, _sep, class_str = import_str.rpartition('.') - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) +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 + -_slugify_strip_re = re.compile(r'[^\w\s-]') -_slugify_hyphenate_re = re.compile(r'[-\s]+') +def do_action_on_many(action, resources, success_msg, error_msg): + """Helper to run an action on many resources.""" + failure_flag = False + for resource in resources: + try: + action(resource) + print(success_msg % _get_resource_string(resource)) + except Exception as e: + failure_flag = True + print(encodeutils.safe_encode(str(e))) -# http://code.activestate.com/recipes/ -# 577257-slugify-make-a-string-usable-in-a-url-or-filename/ -def slugify(value): - """ - Normalizes string, converts to lowercase, removes non-alpha characters, - and converts spaces to hyphens. + if failure_flag: + raise exceptions.CommandError(error_msg) + + +def is_integer_like(val): + """Returns validation of a value as an integer.""" + try: + int(val) + return True + except (TypeError, ValueError, AttributeError): + return False + + +def validate_flavor_metadata_keys(keys): + for key in keys: + valid_name = VALID_KEY_REGEX.match(key) + if not valid_name: + msg = _('Invalid key: "%s". Keys may only contain letters, ' + 'numbers, spaces, underscores, periods, colons and ' + 'hyphens.') + raise exceptions.CommandError(msg % key) - From Django's "django/template/defaultfilters.py". + +@contextlib.contextmanager +def record_time(times, enabled, *args): + """Record the time of a specific action. + + :param times: A list of tuples holds time data. + :param enabled: Whether timing is enabled. + :param args: Other data to be stored besides time data, these args + will be joined to a string. """ - import unicodedata - if not isinstance(value, unicode): - value = unicode(value) - value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') - value = unicode(_slugify_strip_re.sub('', value).strip().lower()) - return _slugify_hyphenate_re.sub('-', value) + if not enabled: + yield + else: + start = time.time() + yield + end = time.time() + times.append((' '.join(args), start, end)) + + +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/v1_1/base.py b/novaclient/v1_1/base.py deleted file mode 100644 index 85d7141fa..000000000 --- a/novaclient/v1_1/base.py +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import base64 - -from novaclient import base - - -# FIXME(sirp): Now that v1_0 has been removed, this can be merged with -# base.ManagerWithFind -class BootingManagerWithFind(base.ManagerWithFind): - """Like a `ManagerWithFind`, but has the ability to boot servers.""" - def _boot(self, resource_url, response_key, name, image, flavor, - meta=None, files=None, userdata=None, - reservation_id=None, return_raw=False, min_count=None, - max_count=None, security_groups=None, key_name=None, - availability_zone=None, block_device_mapping=None, nics=None, - scheduler_hints=None, config_drive=None, admin_pass=None, - **kwargs): - """ - Create (boot) a new server. - - :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. A maximum of five entries is allowed, and both - keys and values must be 255 characters or less. - :param files: A dict of files to overrwrite 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: a UUID for the set of servers being requested. - :param return_raw: If True, don't try to coearse the result into - a Resource object. - :param security_groups: list of security group names - :param key_name: (optional extension) name of keypair to inject into - the instance - :param availability_zone: Name of the availability zone for instance - placement. - :param block_device_mapping: A dict of block device mappings for this - server. - :param nics: (optional extension) an ordered list of nics to be - added to this server, with information about - connected networks, fixed ips, etc. - :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 admin_pass: admin password for the server. - """ - body = {"server": { - "name": name, - "imageRef": str(base.getid(image)), - "flavorRef": str(base.getid(flavor)), - }} - if userdata: - if hasattr(userdata, 'read'): - userdata = userdata.read() - elif isinstance(userdata, unicode): - userdata = userdata.encode('utf-8') - body["server"]["user_data"] = base64.b64encode(userdata) - if meta: - body["server"]["metadata"] = meta - if reservation_id: - body["server"]["reservation_id"] = reservation_id - if key_name: - body["server"]["key_name"] = key_name - if scheduler_hints: - body['os:scheduler_hints'] = scheduler_hints - if config_drive: - body["server"]["config_drive"] = config_drive - if admin_pass: - body["server"]["adminPass"] = admin_pass - if not min_count: - min_count = 1 - if not max_count: - max_count = min_count - body["server"]["min_count"] = min_count - body["server"]["max_count"] = max_count - - if security_groups: - body["server"]["security_groups"] =\ - [{'name': sg} for sg in security_groups] - - # Files are a slight bit tricky. They're passed in a "personality" - # list to the POST. Each item is a dict giving a file name and the - # base64-encoded contents of the file. We want to allow passing - # either an open file *or* some contents as files here. - if files: - personality = body['server']['personality'] = [] - for filepath, file_or_string in files.items(): - if hasattr(file_or_string, 'read'): - data = file_or_string.read() - else: - data = file_or_string - personality.append({ - 'path': filepath, - 'contents': data.encode('base64'), - }) - - if availability_zone: - body["server"]["availability_zone"] = availability_zone - - # Block device mappings are passed as a list of dictionaries - if block_device_mapping: - bdm = body['server']['block_device_mapping'] = [] - for device_name, mapping in block_device_mapping.items(): - # - # The mapping is in the format: - # :[]:[]:[] - # - bdm_dict = {'device_name': device_name} - - mapping_parts = mapping.split(':') - id = mapping_parts[0] - if len(mapping_parts) == 1: - bdm_dict['volume_id'] = id - if len(mapping_parts) > 1: - type = mapping_parts[1] - if type.startswith('snap'): - bdm_dict['snapshot_id'] = id - else: - bdm_dict['volume_id'] = id - if len(mapping_parts) > 2: - bdm_dict['volume_size'] = mapping_parts[2] - if len(mapping_parts) > 3: - bdm_dict['delete_on_termination'] = mapping_parts[3] - bdm.append(bdm_dict) - - if nics is not None: - # NOTE(tr3buchet): nics can be an empty list - all_net_data = [] - for nic_info in nics: - net_data = {} - # if value is empty string, do not send value in body - if nic_info.get('net-id'): - net_data['uuid'] = nic_info['net-id'] - if nic_info.get('v4-fixed-ip'): - net_data['fixed_ip'] = nic_info['v4-fixed-ip'] - if nic_info.get('port-id'): - net_data['port'] = nic_info['port-id'] - all_net_data.append(net_data) - body['server']['networks'] = all_net_data - - return self._create(resource_url, body, response_key, - return_raw=return_raw, **kwargs) diff --git a/novaclient/v1_1/certs.py b/novaclient/v1_1/certs.py deleted file mode 100644 index 6e3a4c716..000000000 --- a/novaclient/v1_1/certs.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2011 OpenStack LLC. -# 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. - -""" -Certificate interface. -""" - -from novaclient import base - - -class Certificate(base.Resource): - def __repr__(self): - return "" % \ - (len(self.private_key) if self.private_key else 0, - len(self.data)) - - -class CertificateManager(base.ManagerWithFind): - """ - Manage :class:`Certificate` resources. - """ - resource_class = Certificate - - def create(self): - """ - Create a x509 certificates for a user in tenant. - """ - return self._create('/os-certificates', {}, 'certificate') - - def get(self): - """ - Get root certificate. - """ - return self._get("/os-certificates/root", 'certificate') diff --git a/novaclient/v1_1/client.py b/novaclient/v1_1/client.py deleted file mode 100644 index ff13780e0..000000000 --- a/novaclient/v1_1/client.py +++ /dev/null @@ -1,132 +0,0 @@ -from novaclient import client -from novaclient.v1_1 import certs -from novaclient.v1_1 import cloudpipe -from novaclient.v1_1 import aggregates -from novaclient.v1_1 import flavors -from novaclient.v1_1 import flavor_access -from novaclient.v1_1 import floating_ip_dns -from novaclient.v1_1 import floating_ips -from novaclient.v1_1 import floating_ip_pools -from novaclient.v1_1 import hosts -from novaclient.v1_1 import hypervisors -from novaclient.v1_1 import images -from novaclient.v1_1 import keypairs -from novaclient.v1_1 import limits -from novaclient.v1_1 import networks -from novaclient.v1_1 import quota_classes -from novaclient.v1_1 import quotas -from novaclient.v1_1 import security_group_rules -from novaclient.v1_1 import security_groups -from novaclient.v1_1 import servers -from novaclient.v1_1 import usage -from novaclient.v1_1 import virtual_interfaces -from novaclient.v1_1 import volumes -from novaclient.v1_1 import volume_snapshots -from novaclient.v1_1 import volume_types - - -class Client(object): - """ - Top-level object to access the OpenStack Compute API. - - Create an instance with your creds:: - - >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) - - Then call methods on its managers:: - - >>> client.servers.list() - ... - >>> client.flavors.list() - ... - - """ - - # FIXME(jesse): project_id isn't required to authenticate - def __init__(self, username, api_key, project_id, auth_url=None, - insecure=False, timeout=None, proxy_tenant_id=None, - proxy_token=None, region_name=None, - endpoint_type='publicURL', extensions=None, - service_type='compute', service_name=None, - volume_service_name=None, timings=False, - bypass_url=None, no_cache=False, http_log_debug=False, - auth_system='keystone'): - # FIXME(comstud): Rename the api_key argument above when we - # know it's not being used as keyword argument - password = api_key - self.flavors = flavors.FlavorManager(self) - self.flavor_access = flavor_access.FlavorAccessManager(self) - self.images = images.ImageManager(self) - self.limits = limits.LimitsManager(self) - self.servers = servers.ServerManager(self) - - # extensions - self.dns_domains = floating_ip_dns.FloatingIPDNSDomainManager(self) - self.dns_entries = floating_ip_dns.FloatingIPDNSEntryManager(self) - self.cloudpipe = cloudpipe.CloudpipeManager(self) - self.certs = certs.CertificateManager(self) - self.floating_ips = floating_ips.FloatingIPManager(self) - self.floating_ip_pools = floating_ip_pools.FloatingIPPoolManager(self) - self.volumes = volumes.VolumeManager(self) - self.volume_snapshots = volume_snapshots.SnapshotManager(self) - self.volume_types = volume_types.VolumeTypeManager(self) - self.keypairs = keypairs.KeypairManager(self) - self.networks = networks.NetworkManager(self) - self.quota_classes = quota_classes.QuotaClassSetManager(self) - self.quotas = quotas.QuotaSetManager(self) - self.security_groups = security_groups.SecurityGroupManager(self) - self.security_group_rules = \ - security_group_rules.SecurityGroupRuleManager(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) - - # Add in any extensions... - if extensions: - for extension in extensions: - if extension.manager_class: - setattr(self, extension.name, - extension.manager_class(self)) - - self.client = client.HTTPClient(username, - password, - project_id, - auth_url, - insecure=insecure, - timeout=timeout, - auth_system=auth_system, - proxy_token=proxy_token, - proxy_tenant_id=proxy_tenant_id, - region_name=region_name, - endpoint_type=endpoint_type, - service_type=service_type, - service_name=service_name, - volume_service_name=volume_service_name, - timings=timings, - bypass_url=bypass_url, - no_cache=no_cache, - http_log_debug=http_log_debug) - - def set_management_url(self, url): - 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.client.authenticate() diff --git a/novaclient/v1_1/cloudpipe.py b/novaclient/v1_1/cloudpipe.py deleted file mode 100644 index 4dea3f2a6..000000000 --- a/novaclient/v1_1/cloudpipe.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. - -"""Cloudpipe interface.""" - -from novaclient import base - - -class Cloudpipe(base.Resource): - """A cloudpipe instance is a VPN attached to a proejct's VLAN.""" - - def __repr__(self): - return "" % self.project_id - - def delete(self): - self.manager.delete(self) - - -class CloudpipeManager(base.ManagerWithFind): - resource_class = Cloudpipe - - def create(self, project): - """ - Launch a cloudpipe instance. - - :param project: name of the project for the cloudpipe - """ - body = {'cloudpipe': {'project_id': project}} - return self._create('/os-cloudpipe', body, 'instance_id', - return_raw=True) - - def list(self): - """ - Get a list of cloudpipe instances. - """ - return self._list('/os-cloudpipe', 'cloudpipes') diff --git a/novaclient/v1_1/contrib/list_extensions.py b/novaclient/v1_1/contrib/list_extensions.py deleted file mode 100644 index f184238dc..000000000 --- a/novaclient/v1_1/contrib/list_extensions.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# 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 -from novaclient import utils - - -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') - - -def do_list_extensions(client, _args): - """ - List all the os-api extensions that are available. - """ - extensions = client.list_extensions.show_all() - fields = ["Name", "Summary", "Alias", "Updated"] - utils.print_list(extensions, fields) diff --git a/novaclient/v1_1/flavors.py b/novaclient/v1_1/flavors.py deleted file mode 100644 index 48e826557..000000000 --- a/novaclient/v1_1/flavors.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -""" -Flavor interface. -""" - -from novaclient import base -from novaclient import exceptions -from novaclient import utils - - -class Flavor(base.Resource): - """ - A flavor is an available hardware configuration for a server. - """ - HUMAN_ID = True - - def __repr__(self): - return "" % self.name - - @property - def ephemeral(self): - """ - Provide a user-friendly accessor to OS-FLV-EXT-DATA:ephemeral - """ - return self._info.get("OS-FLV-EXT-DATA:ephemeral", 'N/A') - - @property - def is_public(self): - """ - Provide a user-friendly accessor to os-flavor-access:is_public - """ - return self._info.get("os-flavor-access:is_public", 'N/A') - - def get_keys(self): - """ - Get extra specs from a flavor. - - :param flavor: The :class:`Flavor` to get extra specs from - """ - _resp, body = self.manager.api.client.get( - "/flavors/%s/os-extra_specs" % - base.getid(self)) - return body["extra_specs"] - - def set_keys(self, metadata): - """ - Set extra specs on a flavor. - - :param flavor: The :class:`Flavor` to set extra spec on - :param metadata: A dict of key/value pairs to be set - """ - body = {'extra_specs': metadata} - return self.manager._create( - "/flavors/%s/os-extra_specs" % base.getid(self), - body, - "extra_specs", - return_raw=True) - - def unset_keys(self, keys): - """ - Unset extra specs on a flavor. - - :param flavor: The :class:`Flavor` to unset extra spec on - :param keys: A list of keys to be unset - """ - for k in keys: - return self.manager._delete( - "/flavors/%s/os-extra_specs/%s" % ( - base.getid(self), k)) - - -class FlavorManager(base.ManagerWithFind): - """ - Manage :class:`Flavor` resources. - """ - resource_class = Flavor - - def list(self, detailed=True): - """ - Get a list of all flavors. - - :rtype: list of :class:`Flavor`. - """ - if detailed is True: - return self._list("/flavors/detail", "flavors") - else: - return self._list("/flavors", "flavors") - - def get(self, flavor): - """ - Get a specific flavor. - - :param flavor: The ID of the :class:`Flavor` to get. - :rtype: :class:`Flavor` - """ - return self._get("/flavors/%s" % base.getid(flavor), "flavor") - - def delete(self, flavor): - """ - Delete a specific flavor. - - :param flavor: The ID of the :class:`Flavor` to get. - :param purge: Whether to purge record from the database - """ - self._delete("/flavors/%s" % base.getid(flavor)) - - def create(self, name, ram, vcpus, disk, flavorid, - ephemeral=0, swap=0, rxtx_factor=1, is_public=True): - """ - Create (allocate) a floating ip for a tenant - - :param name: Descriptive name of the flavor - :param ram: Memory in MB for the flavor - :param vcpu: Number of VCPUs for the flavor - :param disk: Size of local disk in GB - :param flavorid: Integer ID for the flavor - :param swap: Swap space in MB - :param rxtx_factor: RX/TX factor - :rtype: :class:`Flavor` - """ - - try: - ram = int(ram) - except: - raise exceptions.CommandError("Ram must be an integer.") - - try: - vcpus = int(vcpus) - except: - raise exceptions.CommandError("VCPUs must be an integer.") - - try: - disk = int(disk) - except: - raise exceptions.CommandError("Disk must be an integer.") - - if flavorid == "auto": - flavorid = None - - try: - swap = int(swap) - except: - raise exceptions.CommandError("Swap must be an integer.") - - try: - ephemerel = int(ephemeral) - except: - raise exceptions.CommandError("Ephemerel must be an integer.") - - try: - rxtx_factor = int(rxtx_factor) - except: - raise exceptions.CommandError("rxtx_factor must be an integer.") - - try: - is_public = utils.bool_from_str(is_public) - except: - raise exceptions.CommandError("is_public must be a boolean.") - - body = { - "flavor": { - "name": name, - "ram": int(ram), - "vcpus": int(vcpus), - "disk": int(disk), - "id": flavorid, - "swap": int(swap), - "OS-FLV-EXT-DATA:ephemeral": int(ephemeral), - "rxtx_factor": int(rxtx_factor), - "os-flavor-access:is_public": bool(is_public), - } - } - - return self._create("/flavors", body, "flavor") diff --git a/novaclient/v1_1/floating_ip_dns.py b/novaclient/v1_1/floating_ip_dns.py deleted file mode 100644 index c8bd563fd..000000000 --- a/novaclient/v1_1/floating_ip_dns.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright 2011 Andrew Bogott for The Wikimedia Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import urllib - -from novaclient import base - - -def _quote_domain(domain): - """Special quoting rule for placing domain names on a url line. - - Domain names tend to have .'s in them. Urllib doesn't quote dots, - but Routes tends to choke on them, so we need an extra level of - by-hand quoting here. - """ - return urllib.quote(domain.replace('.', '%2E')) - - -class FloatingIPDNSDomain(base.Resource): - def delete(self): - self.manager.delete(self.domain) - - def create(self): - if self.scope == 'public': - self.manager.create_public(self.domain, self.project) - else: - self.manager.create_private(self.domain, self.availability_zone) - - def get(self): - entries = self.manager.domains() - for entry in entries: - if entry.get('domain') == self.domain: - return entry - - return None - - -class FloatingIPDNSDomainManager(base.ManagerWithFind): - resource_class = FloatingIPDNSDomain - - def domains(self): - """Return the list of available dns domains.""" - return self._list("/os-floating-ip-dns", "domain_entries") - - def create_private(self, fqdomain, availability_zone): - """Add or modify a private DNS domain.""" - body = {'domain_entry': - {'scope': 'private', - 'availability_zone': availability_zone}} - - return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), - body) - - def create_public(self, fqdomain, project): - """Add or modify a public DNS domain.""" - body = {'domain_entry': - {'scope': 'public', - 'project': project}} - - return self._update('/os-floating-ip-dns/%s' % _quote_domain(fqdomain), - body) - - def delete(self, fqdomain): - """Delete the specified domain""" - self._delete("/os-floating-ip-dns/%s" % _quote_domain(fqdomain)) - - -class FloatingIPDNSEntry(base.Resource): - def delete(self): - self.manager.delete(self.name, self.domain) - - def create(self): - self.manager.create(self.domain, self.name, - self.ip, self.dns_type) - - def get(self): - return self.manager.get(self.domain, self.name) - - -class FloatingIPDNSEntryManager(base.ManagerWithFind): - resource_class = FloatingIPDNSEntry - - def get(self, domain, name): - """Return a list of entries for the given domain and ip or name.""" - return self._get("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name), - "dns_entry") - - def get_for_ip(self, domain, ip): - """Return a list of entries for the given domain and ip or name.""" - qparams = {'ip': ip} - params = "?%s" % urllib.urlencode(qparams) - - return self._list("/os-floating-ip-dns/%s/entries%s" % - (_quote_domain(domain), params), - "dns_entries") - - def create(self, domain, name, ip, dns_type): - """Add a new DNS entry.""" - body = {'dns_entry': - {'ip': ip, - 'dns_type': dns_type}} - - return self._update("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name), - body) - - def modify_ip(self, domain, name, ip): - """Add a new DNS entry.""" - body = {'dns_entry': - {'ip': ip, - 'dns_type': 'A'}} - - return self._update("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name), - body) - - def delete(self, domain, name): - """Delete entry specified by name and domain.""" - self._delete("/os-floating-ip-dns/%s/entries/%s" % - (_quote_domain(domain), name)) diff --git a/novaclient/v1_1/floating_ips.py b/novaclient/v1_1/floating_ips.py deleted file mode 100644 index c76467474..000000000 --- a/novaclient/v1_1/floating_ips.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2011 OpenStack LLC. -# 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 FloatingIP(base.Resource): - def delete(self): - """ - Delete this floating ip - """ - self.manager.delete(self) - - -class FloatingIPManager(base.ManagerWithFind): - resource_class = FloatingIP - - def list(self): - """ - List floating ips for a tenant - """ - return self._list("/os-floating-ips", "floating_ips") - - def create(self, pool=None): - """ - Create (allocate) a floating ip for a tenant - """ - return self._create("/os-floating-ips", {'pool': pool}, "floating_ip") - - def delete(self, floating_ip): - """ - Delete (deallocate) a floating ip for a tenant - - :param key: The :class:`Keypair` (or its ID) to delete. - """ - self._delete("/os-floating-ips/%s" % base.getid(floating_ip)) - - def get(self, floating_ip): - """ - Retrieve a floating ip - """ - return self._get("/os-floating-ips/%s" % base.getid(floating_ip), - "floating_ip") diff --git a/novaclient/v1_1/hosts.py b/novaclient/v1_1/hosts.py deleted file mode 100644 index b2377800d..000000000 --- a/novaclient/v1_1/hosts.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# 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. - -""" -host interface (1.1 extension). -""" -from novaclient import base - - -class Host(base.Resource): - 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) - - def update(self, values): - return self.manager.update(self.host, values) - - def startup(self): - return self.manager.host_action(self.host, 'startup') - - def shutdown(self): - return self.manager.host_action(self.host, 'shutdown') - - def reboot(self): - return self.manager.host_action(self.host, 'reboot') - - -class HostManager(base.ManagerWithFind): - resource_class = Host - - def get(self, host): - """ - Describes cpu/memory/hdd info for host. - - :param host: destination host name. - """ - return self._list("/os-hosts/%s" % host, "host") - - def update(self, host, values): - """Update status or maintenance mode for the host.""" - result = self._update("/os-hosts/%s" % host, values) - return self.resource_class(self, result) - - def host_action(self, host, action): - """Performs an action on a host.""" - url = "/os-hosts/%s/%s" % (host, action) - return self._get(url) - - def list_all(self, zone=None): - url = '/os-hosts' - if zone: - url = '/os-hosts?zone=%s' % zone - return self._list(url, "hosts") diff --git a/novaclient/v1_1/hypervisors.py b/novaclient/v1_1/hypervisors.py deleted file mode 100644 index dfb5c885a..000000000 --- a/novaclient/v1_1/hypervisors.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. - -""" -Hypervisors interface (1.1 extension). -""" - -import urllib - -from novaclient import base - - -class Hypervisor(base.Resource): - NAME_ATTR = 'hypervisor_hostname' - - def __repr__(self): - return "" % self.id - - -class HypervisorManager(base.ManagerWithFind): - resource_class = Hypervisor - - def list(self, detailed=True): - """ - Get a list of hypervisors. - """ - detail = "" - if detailed: - detail = "/detail" - return self._list('/os-hypervisors%s' % detail, 'hypervisors') - - def search(self, hypervisor_match, servers=False): - """ - Get a list of matching hypervisors. - - :param servers: If True, server information is also retrieved. - """ - target = 'servers' if servers else 'search' - url = ('/os-hypervisors/%s/%s' % - (urllib.quote(hypervisor_match, safe=''), target)) - return self._list(url, 'hypervisors') - - def get(self, hypervisor): - """ - Get a specific hypervisor. - """ - return self._get("/os-hypervisors/%s" % base.getid(hypervisor), - "hypervisor") - - def uptime(self, hypervisor): - """ - Get the uptime for a specific hypervisor. - """ - return self._get("/os-hypervisors/%s/uptime" % base.getid(hypervisor), - "hypervisor") - - def statistics(self): - """ - Get hypervisor statistics over all compute nodes. - """ - return self._get("/os-hypervisors/statistics", "hypervisor_statistics") diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py deleted file mode 100644 index dfde21bd5..000000000 --- a/novaclient/v1_1/images.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -""" -Image interface. -""" - -from novaclient import base - - -class Image(base.Resource): - """ - An image is a collection of files used to create or rebuild a server. - """ - HUMAN_ID = True - - def __repr__(self): - return "" % self.name - - def delete(self): - """ - Delete this image. - """ - self.manager.delete(self) - - -class ImageManager(base.ManagerWithFind): - """ - Manage :class:`Image` resources. - """ - resource_class = Image - - def get(self, image): - """ - Get an image. - - :param image: The ID of the image to get. - :rtype: :class:`Image` - """ - return self._get("/images/%s" % base.getid(image), "image") - - def list(self, detailed=True): - """ - Get a list of all images. - - :rtype: list of :class:`Image` - """ - if detailed is True: - return self._list("/images/detail", "images") - else: - return self._list("/images", "images") - - def delete(self, image): - """ - Delete an image. - - It should go without saying that you can't delete an image - that you didn't create. - - :param image: The :class:`Image` (or its ID) to delete. - """ - self._delete("/images/%s" % base.getid(image)) - - def set_meta(self, image, metadata): - """ - Set an images metadata - - :param image: The :class:`Image` to add metadata to - :param metadata: A dict of metadata to add to the image - """ - body = {'metadata': metadata} - return self._create("/images/%s/metadata" % base.getid(image), body, - "metadata") - - def delete_meta(self, image, keys): - """ - Delete metadata from an image - - :param image: The :class:`Image` to add metadata to - :param keys: A list of metadata keys to delete from the image - """ - for k in keys: - self._delete("/images/%s/metadata/%s" % (base.getid(image), k)) diff --git a/novaclient/v1_1/keypairs.py b/novaclient/v1_1/keypairs.py deleted file mode 100644 index bb31e8657..000000000 --- a/novaclient/v1_1/keypairs.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# 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. - -""" -Keypair interface (1.1 extension). -""" - -from novaclient import base - - -class Keypair(base.Resource): - """ - A keypair is a ssh key that can be injected into a server on launch. - """ - - def __repr__(self): - return "" % self.id - - def _add_details(self, info): - dico = 'keypair' in info and \ - info['keypair'] or info - for (k, v) in dico.items(): - setattr(self, k, v) - - @property - def id(self): - return self.name - - def delete(self): - self.manager.delete(self) - - -class KeypairManager(base.ManagerWithFind): - resource_class = Keypair - - def create(self, name, public_key=None): - """ - Create a keypair - - :param name: name for the keypair to create - :param public_key: existing public key to import - """ - body = {'keypair': {'name': name}} - if public_key: - body['keypair']['public_key'] = public_key - return self._create('/os-keypairs', body, 'keypair') - - def delete(self, key): - """ - Delete a keypair - - :param key: The :class:`Keypair` (or its ID) to delete. - """ - self._delete('/os-keypairs/%s' % (base.getid(key))) - - def list(self): - """ - Get a list of keypairs. - """ - return self._list('/os-keypairs', 'keypairs') diff --git a/novaclient/v1_1/networks.py b/novaclient/v1_1/networks.py deleted file mode 100644 index 54a2e3995..000000000 --- a/novaclient/v1_1/networks.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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. - -""" -Network interface. -""" - -from novaclient import base - - -class Network(base.Resource): - """ - A network. - """ - HUMAN_ID = False - NAME_ATTR = "label" - - def __repr__(self): - return "" % self.label - - def delete(self): - self.manager.delete(self) - - -class NetworkManager(base.ManagerWithFind): - """ - Manage :class:`Network` resources. - """ - resource_class = Network - - def list(self): - """ - Get a list of all networks. - - :rtype: list of :class:`Network`. - """ - return self._list("/os-networks", "networks") - - def get(self, network): - """ - Get a specific network. - - :param network: The ID of the :class:`Network` to get. - :rtype: :class:`Network` - """ - return self._get("/os-networks/%s" % base.getid(network), "network") - - def delete(self, network): - """ - Delete a specific network. - - :param network: The ID of the :class:`Network` to delete. - """ - self._delete("/os-networks/%s" % base.getid(network)) - - def create(self, **kwargs): - """ - Create (allocate) a network. The following parameters are - optional except for label; cidr or cidr_v6 must be specified, too. - - :param label: str - :param bridge: str - :param bridge_interface: str - :param cidr: str - :param cidr_v6: str - :param dns1: str - :param dns2: str - :param fixed_cidr: str - :param gateway: str - :param gateway_v6: str - :param multi_host: str - :param priority: str - :param project_id: str - :param vlan_start: int - :param vpn_start: int - - :rtype: list of :class:`Network` - """ - body = {"network": kwargs} - return self._create('/os-networks', body, 'network') - - def disassociate(self, network): - """ - Disassociate a specific network from project. - - :param network: The ID of the :class:`Network` to get. - """ - self.api.client.post("/os-networks/%s/action" % base.getid(network), - body={"disassociate": None}) - - def add(self, network=None): - """ - Associates the current project with a network. Network can be chosen - automatically or provided explicitly. - - :param network: The ID of the :class:`Network` to associate (optional). - """ - self.api.client.post( - "/os-networks/add", - body={"id": base.getid(network) if network else None}) diff --git a/novaclient/v1_1/quota_classes.py b/novaclient/v1_1/quota_classes.py deleted file mode 100644 index ae5b56308..000000000 --- a/novaclient/v1_1/quota_classes.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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 QuotaClassSet(base.Resource): - - @property - def id(self): - """QuotaClassSet does not have a 'id' attribute but base.Resource - needs it to self-refresh and QuotaSet is indexed by class_name""" - return self.class_name - - def update(self, *args, **kwargs): - self.manager.update(self.class_name, *args, **kwargs) - - -class QuotaClassSetManager(base.ManagerWithFind): - resource_class = QuotaClassSet - - def get(self, class_name): - return self._get("/os-quota-class-sets/%s" % (class_name), - "quota_class_set") - - def update(self, class_name, metadata_items=None, - injected_file_content_bytes=None, volumes=None, gigabytes=None, - ram=None, floating_ips=None, instances=None, - injected_files=None, cores=None): - - body = {'quota_class_set': { - 'class_name': class_name, - 'metadata_items': metadata_items, - 'injected_file_content_bytes': injected_file_content_bytes, - 'volumes': volumes, - 'gigabytes': gigabytes, - 'ram': ram, - 'floating_ips': floating_ips, - 'instances': instances, - 'injected_files': injected_files, - 'cores': cores}} - - for key in body['quota_class_set'].keys(): - if body['quota_class_set'][key] is None: - body['quota_class_set'].pop(key) - - self._update('/os-quota-class-sets/%s' % (class_name), body) diff --git a/novaclient/v1_1/quotas.py b/novaclient/v1_1/quotas.py deleted file mode 100644 index 0c31611a3..000000000 --- a/novaclient/v1_1/quotas.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# 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 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): - self.manager.update(self.tenant_id, *args, **kwargs) - - -class QuotaSetManager(base.ManagerWithFind): - resource_class = QuotaSet - - def get(self, tenant_id): - if hasattr(tenant_id, 'tenant_id'): - tenant_id = tenant_id.tenant_id - return self._get("/os-quota-sets/%s" % (tenant_id), "quota_set") - - def update(self, tenant_id, metadata_items=None, - injected_file_content_bytes=None, volumes=None, gigabytes=None, - ram=None, floating_ips=None, instances=None, - injected_files=None, cores=None): - - body = {'quota_set': { - 'tenant_id': tenant_id, - 'metadata_items': metadata_items, - 'injected_file_content_bytes': injected_file_content_bytes, - 'volumes': volumes, - 'gigabytes': gigabytes, - 'ram': ram, - 'floating_ips': floating_ips, - 'instances': instances, - 'injected_files': injected_files, - 'cores': cores}} - - for key in body['quota_set'].keys(): - if body['quota_set'][key] is None: - body['quota_set'].pop(key) - - self._update('/os-quota-sets/%s' % (tenant_id), body) - - def defaults(self, tenant_id): - return self._get('/os-quota-sets/%s/defaults' % tenant_id, - 'quota_set') diff --git a/novaclient/v1_1/security_group_rules.py b/novaclient/v1_1/security_group_rules.py deleted file mode 100644 index 2e3ba566b..000000000 --- a/novaclient/v1_1/security_group_rules.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# 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. - -""" -Security group rules interface (1.1 extension). -""" - -from novaclient import base - - -class SecurityGroupRule(base.Resource): - def __str__(self): - return str(self.id) - - def delete(self): - self.manager.delete(self) - - -class SecurityGroupRuleManager(base.ManagerWithFind): - resource_class = SecurityGroupRule - - def create(self, parent_group_id, ip_protocol=None, from_port=None, - to_port=None, cidr=None, group_id=None): - """ - Create a security group - - :param ip_protocol: IP protocol, one of 'tcp', 'udp' or 'icmp' - :param from_port: Source port - :param to_port: Destination port - :param cidr: Destination IP address(es) in CIDR notation - :param group_id: Security group id (int) - :param parent_group_id: Parent security group id (int) - """ - body = {"security_group_rule": { - "ip_protocol": ip_protocol, - "from_port": from_port, - "to_port": to_port, - "cidr": cidr, - "group_id": group_id, - "parent_group_id": parent_group_id}} - - return self._create('/os-security-group-rules', body, - 'security_group_rule') - - def delete(self, rule): - """ - Delete a security group rule - - :param rule: The security group rule to delete (ID or Class) - """ - self._delete('/os-security-group-rules/%s' % base.getid(rule)) diff --git a/novaclient/v1_1/security_groups.py b/novaclient/v1_1/security_groups.py deleted file mode 100644 index 0e2037eea..000000000 --- a/novaclient/v1_1/security_groups.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# 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. - -""" -Security group interface (1.1 extension). -""" - -import urllib - -from novaclient import base - - -class SecurityGroup(base.Resource): - def __str__(self): - return str(self.id) - - def delete(self): - self.manager.delete(self) - - -class SecurityGroupManager(base.ManagerWithFind): - resource_class = SecurityGroup - - def create(self, name, description): - """ - Create a security group - - :param name: name for the security group to create - :param description: description of the security group - :rtype: the security group object - """ - body = {"security_group": {"name": name, 'description': description}} - return self._create('/os-security-groups', body, 'security_group') - - def delete(self, group): - """ - Delete a security group - - :param group: The security group to delete (group or ID) - :rtype: None - """ - self._delete('/os-security-groups/%s' % base.getid(group)) - - def get(self, group_id): - """ - Get a security group - - :param group_id: The security group to get by ID - :rtype: :class:`SecurityGroup` - """ - return self._get('/os-security-groups/%s' % group_id, - 'security_group') - - def list(self, search_opts=None): - """ - Get a list of all security_groups - - :rtype: list of :class:`SecurityGroup` - """ - search_opts = search_opts or {} - - qparams = dict((k, v) for (k, v) in search_opts.iteritems() if v) - - query_string = '?%s' % urllib.urlencode(qparams) if qparams else '' - - return self._list('/os-security-groups%s' % query_string, - 'security_groups') diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py deleted file mode 100644 index 7b394e9f7..000000000 --- a/novaclient/v1_1/servers.py +++ /dev/null @@ -1,715 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2011 OpenStack LLC. -# 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. - -""" -Server interface. -""" - -import urllib - -from novaclient import base -from novaclient.v1_1 import base as local_base - - -REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' - - -class Server(base.Resource): - HUMAN_ID = True - - def __repr__(self): - return "" % self.name - - def delete(self): - """ - Delete (i.e. shut down and delete the image) this server. - """ - self.manager.delete(self) - - def update(self, name=None): - """ - Update the name or the password for this server. - - :param name: Update the server's name. - :param password: Update the root password. - """ - self.manager.update(self, name=name) - - def get_console_output(self, length=None): - """ - Get text console log output from Server. - - :param length: The number of lines you would like to retrieve (as int) - """ - return self.manager.get_console_output(self, length) - - def get_vnc_console(self, console_type): - """ - Get vnc console for a Server. - - :param console_type: Type of console ('novnc' or 'xvpvnc') - """ - return self.manager.get_vnc_console(self, console_type) - - 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. - """ - self.manager.add_fixed_ip(self, network_id) - - def add_floating_ip(self, address): - """ - Add floating IP to an instance - - :param address: The ip address or FloatingIP to add to the instance - """ - self.manager.add_floating_ip(self, address) - - def remove_floating_ip(self, address): - """ - Remove floating IP from an instance - - :param address: The ip address or FloatingIP to remove - """ - self.manager.remove_floating_ip(self, address) - - def stop(self): - """ - Stop -- Stop the running server. - """ - self.manager.stop(self) - - def start(self): - """ - Start -- Start the paused server. - """ - self.manager.start(self) - - def pause(self): - """ - Pause -- Pause the running server. - """ - self.manager.pause(self) - - def unpause(self): - """ - Unpause -- Unpause the paused server. - """ - self.manager.unpause(self) - - def lock(self): - """ - Lock -- Lock the instance from certain operations. - """ - self.manager.lock(self) - - def unlock(self): - """ - Unlock -- Remove instance lock. - """ - self.manager.unlock(self) - - def suspend(self): - """ - Suspend -- Suspend the running server. - """ - self.manager.suspend(self) - - def resume(self): - """ - Resume -- Resume the suspended server. - """ - self.manager.resume(self) - - def rescue(self): - """ - Rescue -- Rescue the problematic server. - """ - return self.manager.rescue(self) - - def unrescue(self): - """ - Unrescue -- Unrescue the rescued server. - """ - self.manager.unrescue(self) - - def diagnostics(self): - """Diagnostics -- Retrieve server diagnostics.""" - return self.manager.diagnostics(self) - - def actions(self): - """Actions -- Retrieve server actions.""" - return self.manager.actions(self) - - def migrate(self): - """ - Migrate a server to a new host. - """ - self.manager.migrate(self) - - def remove_fixed_ip(self, address): - """ - Remove an IP address. - - :param address: The IP address to remove. - """ - self.manager.remove_fixed_ip(self, address) - - def change_password(self, password): - """ - Update the password for a server. - """ - self.manager.change_password(self, password) - - def reboot(self, reboot_type=REBOOT_SOFT): - """ - Reboot the server. - - :param reboot_type: either :data:`REBOOT_SOFT` for a software-level - reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. - """ - self.manager.reboot(self, reboot_type) - - def rebuild(self, image, password=None, **kwargs): - """ - 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 password on the rebuilt server. - """ - return self.manager.rebuild(self, image, password=password, **kwargs) - - def resize(self, flavor, **kwargs): - """ - Resize the server's resources. - - :param flavor: the :class:`Flavor` (or its ID) to resize to. - - 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. - """ - self.manager.resize(self, flavor, **kwargs) - - def create_image(self, image_name, metadata=None): - """ - Create an image based on this server. - - :param image_name: The name to assign the newly create image. - :param metadata: Metadata to assign to the image. - """ - self.manager.create_image(self, image_name, metadata) - - def backup(self, backup_name, backup_type, rotation): - """ - Backup a server instance. - - :param backup_name: Name of the backup image - :param backup_type: The backup type, like 'daily' or 'weekly' - :param rotation: Int parameter representing how many backups to - keep around. - """ - self.manager.backup(self, backup_name, backup_type, rotation) - - def confirm_resize(self): - """ - Confirm that the resize worked, thus removing the original server. - """ - self.manager.confirm_resize(self) - - def revert_resize(self): - """ - Revert a previous resize, switching back to the old server. - """ - self.manager.revert_resize(self) - - @property - def networks(self): - """ - Generate a simplified list of addresses - """ - networks = {} - try: - for network_label, address_list in self.addresses.items(): - networks[network_label] = [a['addr'] for a in address_list] - return networks - except Exception: - return {} - - def live_migrate(self, host, - block_migration=False, - disk_over_commit=False): - """ - Migrates a running instance to a new machine. - """ - self.manager.live_migrate(self, host, - block_migration, - disk_over_commit) - - def reset_state(self, state='error'): - """ - Reset the state of an instance to active or error. - """ - self.manager.reset_state(self, state) - - def add_security_group(self, security_group): - """ - Add a security group to an instance. - """ - self.manager.add_security_group(self, security_group) - - def remove_security_group(self, security_group): - """ - Remova a security group from an instance. - """ - self.manager.remove_security_group(self, security_group) - - -class ServerManager(local_base.BootingManagerWithFind): - resource_class = Server - - def get(self, server): - """ - Get a server. - - :param server: ID of the :class:`Server` to get. - :rtype: :class:`Server` - """ - return self._get("/servers/%s" % base.getid(server), "server") - - def list(self, detailed=True, search_opts=None): - """ - Get a list of servers. - Optional detailed returns details server info. - Optional reservation_id only returns instances with that - reservation_id. - - :rtype: list of :class:`Server` - """ - if search_opts is None: - search_opts = {} - - qparams = {} - - for opt, val in search_opts.iteritems(): - if val: - qparams[opt] = val - - query_string = "?%s" % urllib.urlencode(qparams) if qparams else "" - - detail = "" - if detailed: - detail = "/detail" - return self._list("/servers%s%s" % (detail, query_string), "servers") - - def add_fixed_ip(self, server, network_id): - """ - 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. - """ - self._action('addFixedIp', server, {'networkId': network_id}) - - def remove_fixed_ip(self, server, address): - """ - Remove an IP address. - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param address: The IP address to remove. - """ - self._action('removeFixedIp', server, {'address': address}) - - def add_floating_ip(self, server, address): - """ - 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. - """ - - address = address.ip if hasattr(address, 'ip') else address - self._action('addFloatingIp', server, {'address': address}) - - def remove_floating_ip(self, server, address): - """ - 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. - """ - - address = address.ip if hasattr(address, 'ip') else address - self._action('removeFloatingIp', server, {'address': address}) - - def get_vnc_console(self, server, console_type): - """ - Get a vnc console for an instance - - :param server: The :class:`Server` (or its ID) to add an IP to. - :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') - """ - - return self._action('os-getVNCConsole', server, - {'type': console_type})[1] - - def stop(self, server): - """ - Stop the server. - """ - return self._action('os-stop', server, None) - - def start(self, server): - """ - Start the server. - """ - self._action('os-start', server, None) - - def pause(self, server): - """ - Pause the server. - """ - self._action('pause', server, None) - - def unpause(self, server): - """ - Unpause the server. - """ - self._action('unpause', server, None) - - def lock(self, server): - """ - Lock the server. - """ - self._action('lock', server, None) - - def unlock(self, server): - """ - Unlock the server. - """ - self._action('unlock', server, None) - - def suspend(self, server): - """ - Suspend the server. - """ - self._action('suspend', server, None) - - def resume(self, server): - """ - Resume the server. - """ - self._action('resume', server, None) - - def rescue(self, server): - """ - Rescue the server. - """ - return self._action('rescue', server, None) - - def unrescue(self, server): - """ - Unrescue the server. - """ - self._action('unrescue', server, None) - - def diagnostics(self, server): - """Retrieve server diagnostics.""" - return self.api.client.get("/servers/%s/diagnostics" % - base.getid(server)) - - def actions(self, server): - """Retrieve server actions.""" - return self._list("/servers/%s/actions" % base.getid(server), - "actions") - - def create(self, name, image, flavor, meta=None, files=None, - reservation_id=None, min_count=None, - max_count=None, security_groups=None, userdata=None, - key_name=None, availability_zone=None, - block_device_mapping=None, nics=None, scheduler_hints=None, - config_drive=None, **kwargs): - # TODO: (anthony) indicate in doc string if param is an extension - # and/or optional - """ - Create (boot) a new server. - - :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. A maximum of five entries is allowed, and both - keys and values must be 255 characters or less. - :param files: A dict of files to overrwrite 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 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 reservation_id: a UUID for the set of servers being requested. - :param key_name: (optional extension) 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 nics: (optional extension) an ordered list of nics to be - added to this server, with information about - connected networks, fixed ips, port etc. - :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 - """ - if not min_count: - min_count = 1 - if not max_count: - max_count = min_count - if min_count > max_count: - min_count = max_count - - boot_args = [name, image, flavor] - - boot_kwargs = dict( - meta=meta, files=files, userdata=userdata, - reservation_id=reservation_id, min_count=min_count, - max_count=max_count, security_groups=security_groups, - key_name=key_name, availability_zone=availability_zone, - scheduler_hints=scheduler_hints, config_drive=config_drive, - **kwargs) - - if block_device_mapping: - resource_url = "/os-volumes_boot" - boot_kwargs['block_device_mapping'] = block_device_mapping - else: - resource_url = "/servers" - boot_kwargs['nics'] = nics - - response_key = "server" - return self._boot(resource_url, response_key, *boot_args, - **boot_kwargs) - - def update(self, server, name=None): - """ - Update the name or the password for a server. - - :param server: The :class:`Server` (or its ID) to update. - :param name: Update the server's name. - """ - if name is None: - return - - body = { - "server": { - "name": name, - }, - } - - self._update("/servers/%s" % base.getid(server), body) - - def change_password(self, server, password): - """ - Update the password for a server. - """ - self._action("changePassword", server, {"adminPass": password}) - - def delete(self, server): - """ - Delete (i.e. shut down and delete the image) this server. - """ - self._delete("/servers/%s" % base.getid(server)) - - def reboot(self, server, reboot_type=REBOOT_SOFT): - """ - Reboot a server. - - :param server: The :class:`Server` (or its ID) to share onto. - :param reboot_type: either :data:`REBOOT_SOFT` for a software-level - reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. - """ - self._action('reboot', server, {'type': reboot_type}) - - def rebuild(self, server, image, password=None, **kwargs): - """ - 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. - """ - body = {'imageRef': base.getid(image)} - if password is not None: - body['adminPass'] = password - _resp, body = self._action('rebuild', server, body, **kwargs) - return Server(self, body['server']) - - def migrate(self, server): - """ - Migrate a server to a new host. - - :param server: The :class:`Server` (or its ID). - """ - self._action('migrate', server) - - def resize(self, server, flavor, **kwargs): - """ - 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. - - 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. - """ - info = {'flavorRef': base.getid(flavor)} - self._action('resize', server, info=info, **kwargs) - - def confirm_resize(self, server): - """ - Confirm that the resize worked, thus removing the original server. - - :param server: The :class:`Server` (or its ID) to share onto. - """ - self._action('confirmResize', server) - - def revert_resize(self, server): - """ - Revert a previous resize, switching back to the old server. - - :param server: The :class:`Server` (or its ID) to share onto. - """ - self._action('revertResize', server) - - def create_image(self, server, image_name, metadata=None): - """ - Snapshot a server. - - :param server: The :class:`Server` (or its ID) to share onto. - :param image_name: Name to give the snapshot image - :param meta: Metadata to give newly-created image entity - """ - body = {'name': image_name, 'metadata': metadata or {}} - location = self._action('createImage', server, body)[0]['location'] - image_uuid = location.split('/')[-1] - return image_uuid - - def backup(self, server, backup_name, backup_type, rotation): - """ - Backup a server instance. - - :param server: The :class:`Server` (or its ID) to share onto. - :param backup_name: Name of the backup image - :param backup_type: The backup type, like 'daily' or 'weekly' - :param rotation: Int parameter representing how many backups to - keep around. - """ - body = {'name': backup_name, - 'backup_type': backup_type, - 'rotation': rotation} - self._action('createBackup', server, body) - - def set_meta(self, server, metadata): - """ - Set a servers metadata - :param server: The :class:`Server` to add metadata to - :param metadata: A dict of metadata to add to the server - """ - body = {'metadata': metadata} - return self._create("/servers/%s/metadata" % base.getid(server), - body, "metadata") - - def get_console_output(self, server, length=None): - """ - Get text console log output from Server. - - :param server: The :class:`Server` (or its ID) whose console output - you would like to retrieve. - :param length: The number of tail loglines you would like to retrieve. - """ - return self._action('os-getConsoleOutput', - server, - {'length': length})[1]['output'] - - def delete_meta(self, server, keys): - """ - Delete metadata from an server - :param server: The :class:`Server` to add metadata to - :param keys: A list of metadata keys to delete from the server - """ - for k in keys: - self._delete("/servers/%s/metadata/%s" % (base.getid(server), k)) - - def live_migrate(self, server, host, block_migration, disk_over_commit): - """ - 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. - :param disk_over_commit: if True, Allow overcommit. - - """ - self._action('os-migrateLive', server, - {'host': host, - 'block_migration': block_migration, - 'disk_over_commit': disk_over_commit}) - - def reset_state(self, server, state='error'): - """ - Reset the state of an instance to active or error. - - :param server: ID of the instance to reset the state of. - :param state: Desired state; either 'active' or 'error'. - Defaults to 'error'. - """ - self._action('os-resetState', server, dict(state=state)) - - def add_security_group(self, server, security_group): - """ - Add a Security Group to a instance - - :param server: ID of the instance. - :param security_grou: The name of security group to add. - - """ - self._action('addSecurityGroup', server, {'name': security_group}) - - def remove_security_group(self, server, security_group): - """ - Add a Security Group to a instance - - :param server: ID of the instance. - :param security_grou: The name of security group to remove. - - """ - self._action('removeSecurityGroup', server, {'name': security_group}) - - def _action(self, action, server, info=None, **kwargs): - """ - Perform a server "action" -- reboot/rebuild/resize/etc. - """ - body = {action: info} - self.run_hooks('modify_body_for_action', body, **kwargs) - url = '/servers/%s/action' % base.getid(server) - return self.api.client.post(url, body=body) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py deleted file mode 100644 index c296d61d3..000000000 --- a/novaclient/v1_1/shell.py +++ /dev/null @@ -1,2314 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import argparse -import datetime -import getpass -import locale -import os -import sys -import time - -from novaclient import exceptions -from novaclient.openstack.common import timeutils -from novaclient import utils -from novaclient.v1_1 import servers - - -def _boot(cs, args, reservation_id=None, min_count=None, max_count=None): - """Boot a new server.""" - if min_count is None: - min_count = 1 - if max_count is None: - max_count = min_count - if min_count > max_count: - raise exceptions.CommandError("min_instances should be <= " - "max_instances") - if not min_count or not max_count: - raise exceptions.CommandError("min_instances nor max_instances should" - "be 0") - - if not args.image and not args.block_device_mapping: - raise exceptions.CommandError("you need to specify an Image ID " - "or a block device mapping ") - if not args.flavor: - raise exceptions.CommandError("you need to specify a Flavor ID ") - - flavor = _find_flavor(cs, args.flavor) - image = _find_image(cs, args.image) - - meta = dict(v.split('=', 1) for v in args.meta) - - files = {} - for f in args.files: - dst, src = f.split('=', 1) - try: - files[dst] = open(src) - except IOError, e: - raise exceptions.CommandError("Can't open '%s': %s" % (src, e)) - - # use the os-keypair extension - key_name = None - if args.key_name is not None: - key_name = args.key_name - - if args.user_data: - try: - userdata = open(args.user_data) - except IOError, e: - raise exceptions.CommandError("Can't open '%s': %s" % \ - (args.user_data, e)) - else: - userdata = None - - if args.availability_zone: - availability_zone = args.availability_zone - else: - availability_zone = None - - if args.security_groups: - security_groups = args.security_groups.split(',') - else: - security_groups = None - - block_device_mapping = {} - for bdm in args.block_device_mapping: - device_name, mapping = bdm.split('=', 1) - block_device_mapping[device_name] = mapping - - nics = [] - for nic_str in args.nics: - nic_info = {"net-id": "", "v4-fixed-ip": "", "port-id": ""} - for kv_str in nic_str.split(","): - k, v = kv_str.split("=", 1) - nic_info[k] = v - nics.append(nic_info) - - hints = {} - if args.scheduler_hints: - for hint in args.scheduler_hints: - key, _sep, value = hint.partition('=') - # NOTE(vish): multiple copies of the same hint will - # result in a list of values - if key in hints: - if isinstance(hints[key], basestring): - hints[key] = [hints[key]] - hints[key] += [value] - else: - hints[key] = value - boot_args = [args.name, image, flavor] - - if str(args.config_drive).lower() in ("true", "1"): - config_drive = True - elif str(args.config_drive).lower() in ("false", "0", "", "none"): - config_drive = None - else: - config_drive = args.config_drive - - boot_kwargs = dict( - meta=meta, - files=files, - key_name=key_name, - reservation_id=reservation_id, - min_count=min_count, - max_count=max_count, - userdata=userdata, - availability_zone=availability_zone, - security_groups=security_groups, - block_device_mapping=block_device_mapping, - nics=nics, - scheduler_hints=hints, - config_drive=config_drive) - - return boot_args, boot_kwargs - - -@utils.arg('--flavor', - default=None, - metavar='', - help="Flavor ID (see 'nova flavor-list').") -@utils.arg('--image', - default=None, - metavar='', - help="Image ID (see 'nova image-list'). ") -@utils.arg('--meta', - metavar="", - action='append', - default=[], - help="Record arbitrary key/value metadata to /meta.js "\ - "on the new server. Can be specified multiple times.") -@utils.arg('--file', - metavar="", - action='append', - dest='files', - default=[], - help="Store arbitrary files from locally to "\ - "on the new server. You may store up to 5 files.") -@utils.arg('--key-name', - metavar='', - help="Key name of keypair that should be created earlier with \ - the command keypair-add") -@utils.arg('--key_name', - help=argparse.SUPPRESS) -@utils.arg('name', metavar='', help='Name for the new server') -@utils.arg('--user-data', - default=None, - metavar='', - help="user data file to pass to be exposed by the metadata server.") -@utils.arg('--user_data', - help=argparse.SUPPRESS) -@utils.arg('--availability-zone', - default=None, - metavar='', - help="The availability zone for instance placement.") -@utils.arg('--availability_zone', - help=argparse.SUPPRESS) -@utils.arg('--security-groups', - default=None, - metavar='', - help="Comma separated list of security group names.") -@utils.arg('--security_groups', - help=argparse.SUPPRESS) -@utils.arg('--block-device-mapping', - metavar="", - action='append', - default=[], - help="Block device mapping in the format " - "=:::.") -@utils.arg('--block_device_mapping', - action='append', - help=argparse.SUPPRESS) -@utils.arg('--hint', - action='append', - dest='scheduler_hints', - default=[], - metavar='', - help="Send arbitrary key/value pairs to the scheduler for custom use.") -@utils.arg('--nic', - metavar="", - action='append', - dest='nics', - default=[], - help="Create a NIC on the server.\n" - "Specify option multiple times to create multiple NICs.\n" - "net-id: attach NIC to network with this UUID (optional)\n" - "v4-fixed-ip: IPv4 fixed address for NIC (optional).\n" - "port-id: attach NIC to port with this UUID (optional)") -@utils.arg('--config-drive', - metavar="", - dest='config_drive', - default=False, - help="Enable config drive") -@utils.arg('--poll', - dest='poll', - action="store_true", - default=False, - help='Blocks while instance builds so progress can be reported.') -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) - - # Keep any information (like adminPass) returned by create - info = server._info - server = cs.servers.get(info['id']) - info.update(server._info) - - flavor = info.get('flavor', {}) - flavor_id = flavor.get('id', '') - info['flavor'] = _find_flavor(cs, flavor_id).name - - image = info.get('image', {}) - image_id = image.get('id', '') - info['image'] = _find_image(cs, image_id).name - - info.pop('links', None) - info.pop('addresses', None) - - utils.print_dict(info) - - if args.poll: - _poll_for_status(cs.servers.get, info['id'], 'building', ['active']) - - -def do_cloudpipe_list(cs, _args): - """Print a list of all cloudpipe instances.""" - cloudpipes = cs.cloudpipe.list() - columns = ['Project Id', "Public IP", "Public Port", "Internal IP"] - utils.print_list(cloudpipes, columns) - - -@utils.arg('project', metavar='', help='Name of the project.') -def do_cloudpipe_create(cs, args): - """Create a cloudpipe instance for the given project""" - cs.cloudpipe.create(args.project) - - -def _poll_for_status(poll_fn, obj_id, action, final_ok_states, - poll_period=5, show_progress=True, - status_field="status", silent=False): - """Block while an action is being performed, periodically printing - progress. - """ - def print_progress(progress): - if show_progress: - msg = ('\rInstance %(action)s... %(progress)s%% complete' - % dict(action=action, progress=progress)) - else: - msg = '\rInstance %(action)s...' % dict(action=action) - - sys.stdout.write(msg) - sys.stdout.flush() - - if not silent: - print - - while True: - obj = poll_fn(obj_id) - - status = getattr(obj, status_field) - - if status: - status = status.lower() - - progress = getattr(obj, 'progress', None) or 0 - if status in final_ok_states: - if not silent: - print_progress(100) - print "\nFinished" - break - elif status == "error": - if not silent: - print "\nError %(action)s instance" % locals() - break - - if not silent: - print_progress(progress) - - time.sleep(poll_period) - - -def _translate_flavor_keys(collection): - convert = [('ram', 'memory_mb')] - for item in collection: - keys = item.__dict__.keys() - for from_key, to_key in convert: - if from_key in keys and to_key not in keys: - setattr(item, to_key, item._info[from_key]) - - -def _print_flavor_extra_specs(flavor): - try: - return flavor.get_keys() - except exceptions.NotFound: - return "N/A" - - -def _print_flavor_list(cs, flavors): - _translate_flavor_keys(flavors) - formatters = {'extra_specs': _print_flavor_extra_specs} - utils.print_list(flavors, [ - 'ID', - 'Name', - 'Memory_MB', - 'Disk', - 'Ephemeral', - 'Swap', - 'VCPUs', - 'RXTX_Factor', - 'Is_Public', - 'extra_specs'], formatters) - - -def do_flavor_list(cs, _args): - """Print a list of available 'flavors' (sizes of servers).""" - flavors = cs.flavors.list() - _print_flavor_list(cs, flavors) - - -@utils.arg('id', - metavar='', - help="Unique ID of the flavor to delete") -def do_flavor_delete(cs, args): - """Delete a specific flavor""" - cs.flavors.delete(args.id) - - -@utils.arg('flavor', - metavar='', - help="Name or ID of flavor") -def do_flavor_show(cs, args): - """Show details about the given flavor.""" - flavor = _find_flavor(cs, args.flavor) - _print_flavor(cs, flavor) - - -@utils.arg('name', - metavar='', - help="Name of the new flavor") -@utils.arg('id', - metavar='', - help="Unique integer ID for the new flavor") -@utils.arg('ram', - metavar='', - help="Memory size in MB") -@utils.arg('disk', - metavar='', - help="Disk size in GB") -@utils.arg('--ephemeral', - metavar='', - help="Ephemeral space size in GB (default 0)", - default=0) -@utils.arg('vcpus', - metavar='', - help="Number of vcpus") -@utils.arg('--swap', - metavar='', - help="Swap space size in MB (default 0)", - default=0) -@utils.arg('--rxtx-factor', - metavar='', - help="RX/TX factor (default 1)", - default=1) -@utils.arg('--is-public', - metavar='', - help="Make flavor accessible to the public (default true)", - type=utils.bool_from_str, - default=True) -def do_flavor_create(cs, args): - """Create a new flavor""" - 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(cs, [f]) - - -@utils.arg('flavor', - metavar='', - help="Name or ID of flavor") -@utils.arg('action', - metavar='', - choices=['set', 'unset'], - help="Actions: 'set' or 'unset'") -@utils.arg('metadata', - metavar='', - nargs='+', - action='append', - default=[], - help='Extra_specs to set/unset (only key is necessary on unset)') -def do_flavor_key(cs, args): - """Set or unset extra_spec for a flavor.""" - flavor = _find_flavor(cs, args.flavor) - keypair = _extract_metadata(args) - - if args.action == 'set': - flavor.set_keys(keypair) - elif args.action == 'unset': - flavor.unset_keys(keypair.keys()) - - -@utils.arg('--flavor', - metavar='', - help="Filter results by flavor name or ID.") -@utils.arg('--tenant', metavar='', - help='Filter results by tenant ID.') -def do_flavor_access_list(cs, args): - """Print access information about the given flavor.""" - if args.flavor and args.tenant: - raise exceptions.CommandError("Unable to filter results by " - "both --flavor and --tenant.") - elif args.flavor: - flavor = _find_flavor(cs, args.flavor) - if flavor.is_public: - raise exceptions.CommandError("Failed to get access list " - "for public flavor type.") - kwargs = {'flavor': flavor} - elif args.tenant: - kwargs = {'tenant': args.tenant} - else: - raise exceptions.CommandError("Unable to get all access lists. " - "Specify --flavor or --tenant") - - try: - access_list = cs.flavor_access.list(**kwargs) - except NotImplementedError, e: - raise exceptions.CommandError("%s" % str(e)) - - columns = ['Flavor_ID', 'Tenant_ID'] - utils.print_list(access_list, columns) - - -@utils.arg('flavor', - metavar='', - help="Filter results by flavor name or ID.") -@utils.arg('tenant', metavar='', - help='Filter results by tenant ID.') -def do_flavor_access_add(cs, args): - """Add flavor access for the given tenant.""" - flavor = _find_flavor(cs, args.flavor) - access_list = cs.flavor_access.add_tenant_access(flavor, args.tenant) - columns = ['Flavor_ID', 'Tenant_ID'] - utils.print_list(access_list, columns) - - -@utils.arg('flavor', - metavar='', - help="Filter results by flavor name or ID.") -@utils.arg('tenant', metavar='', - help='Filter results by tenant ID.') -def do_flavor_access_remove(cs, args): - """Remove flavor access for the given tenant.""" - flavor = _find_flavor(cs, args.flavor) - access_list = cs.flavor_access.remove_tenant_access(flavor, args.tenant) - columns = ['Flavor_ID', 'Tenant_ID'] - utils.print_list(access_list, columns) - - -def do_network_list(cs, _args): - """Print a list of available networks.""" - network_list = cs.networks.list() - columns = ['ID', 'Label', 'Cidr'] - utils.print_list(network_list, columns) - - -@utils.arg('network', - metavar='', - help="uuid or label of network") -def do_network_show(cs, args): - """Show details about the given network.""" - network = utils.find_resource(cs.networks, args.network) - utils.print_dict(network._info) - - -def do_image_list(cs, _args): - """Print a list of available images to boot from.""" - image_list = cs.images.list() - - def parse_server_name(image): - try: - return image.server['id'] - except (AttributeError, KeyError): - return '' - - fmts = {'Server': parse_server_name} - utils.print_list(image_list, ['ID', 'Name', 'Status', 'Server'], - fmts, sortby_index=1) - - -@utils.arg('image', - metavar='', - help="Name or ID of image") -@utils.arg('action', - metavar='', - choices=['set', 'delete'], - help="Actions: 'set' or 'delete'") -@utils.arg('metadata', - metavar='', - nargs='+', - action='append', - default=[], - help='Metadata to add/update or delete (only key is necessary on delete)') -def do_image_meta(cs, args): - """Set or Delete metadata on an image.""" - image = _find_image(cs, args.image) - metadata = _extract_metadata(args) - - if args.action == 'set': - cs.images.set_meta(image, metadata) - elif args.action == 'delete': - cs.images.delete_meta(image, metadata.keys()) - - -def _extract_metadata(args): - metadata = {} - for metadatum in args.metadata[0]: - # Can only pass the key in on 'delete' - # So this doesn't have to have '=' - if metadatum.find('=') > -1: - (key, value) = metadatum.split('=', 1) - else: - key = metadatum - value = None - - metadata[key] = value - return metadata - - -def _print_image(image): - info = image._info.copy() - - # ignore links, we don't need to present those - info.pop('links') - - # try to replace a server entity to just an id - server = info.pop('server', None) - try: - info['server'] = server['id'] - except (KeyError, TypeError): - pass - - # break up metadata and display each on its own row - metadata = info.pop('metadata', {}) - try: - for key, value in metadata.items(): - _key = 'metadata %s' % key - info[_key] = value - except AttributeError: - pass - - utils.print_dict(info) - - -def _print_flavor(cs, flavor): - info = flavor._info.copy() - # ignore links, we don't need to present those - info.pop('links') - info.update({"extra_specs": _print_flavor_extra_specs(flavor)}) - utils.print_dict(info) - - -@utils.arg('image', - metavar='', - help="Name or ID of image") -def do_image_show(cs, args): - """Show details about the given image.""" - image = _find_image(cs, args.image) - _print_image(image) - - -@utils.arg('image', metavar='', help='Name or ID of image.') -def do_image_delete(cs, args): - """ - Delete an image. - - It should go without saying, but you can only delete images you - created. - """ - image = _find_image(cs, args.image) - image.delete() - - -@utils.arg('--reservation-id', - dest='reservation_id', - metavar='', - default=None, - help='Only return instances that match reservation-id.') -@utils.arg('--reservation_id', - help=argparse.SUPPRESS) -@utils.arg('--ip', - dest='ip', - metavar='', - default=None, - help='Search with regular expression match by IP address (Admin only).') -@utils.arg('--ip6', - dest='ip6', - metavar='', - default=None, - help='Search with regular expression match by IPv6 address (Admin only).') -@utils.arg('--name', - dest='name', - 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 instance name (Admin only).') -@utils.arg('--instance_name', - help=argparse.SUPPRESS) -@utils.arg('--status', - dest='status', - metavar='', - default=None, - help='Search by server status') -@utils.arg('--flavor', - dest='flavor', - metavar='', - type=int, - default=None, - help='Search by flavor ID') -@utils.arg('--image', - dest='image', - metavar='', - default=None, - help='Search by image ID') -@utils.arg('--host', - dest='host', - metavar='', - default=None, - help='Search instances by hostname to which they are assigned ' - '(Admin only).') -@utils.arg('--all-tenants', - dest='all_tenants', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), - help='Display information from all tenants (Admin only).') -@utils.arg('--all_tenants', - nargs='?', - type=int, - const=1, - help=argparse.SUPPRESS) -def do_list(cs, args): - """List active servers.""" - search_opts = { - 'all_tenants': args.all_tenants, - 'reservation_id': args.reservation_id, - 'ip': args.ip, - 'ip6': args.ip6, - 'name': args.name, - 'image': args.image, - 'flavor': args.flavor, - 'status': args.status, - 'host': args.host, - 'instance_name': args.instance_name} - - id_col = 'ID' - - columns = [id_col, 'Name', 'Status', 'Networks'] - formatters = {'Networks': utils._format_servers_list_networks} - utils.print_list(cs.servers.list(search_opts=search_opts), columns, - formatters, sortby_index=1) - - -@utils.arg('--hard', - dest='reboot_type', - action='store_const', - const=servers.REBOOT_HARD, - default=servers.REBOOT_SOFT, - help='Perform a hard reboot (instead of a soft one).') -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('--poll', - dest='poll', - action="store_true", - default=False, - help='Blocks while instance is rebooting.') -def do_reboot(cs, args): - """Reboot a server.""" - server = _find_server(cs, args.server) - server.reboot(args.reboot_type) - - if args.poll: - _poll_for_status(cs.servers.get, server.id, 'rebooting', ['active'], - show_progress=False) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('image', metavar='', help="Name or ID of new image.") -@utils.arg('--rebuild-password', - dest='rebuild_password', - metavar='', - default=False, - help="Set the provided password on the rebuild instance.") -@utils.arg('--rebuild_password', - help=argparse.SUPPRESS) -@utils.arg('--poll', - dest='poll', - action="store_true", - default=False, - help='Blocks while instance rebuilds so progress can be reported.') -@utils.arg('--minimal', - dest='minimal', - action="store_true", - default=False, - help='Skips flavor/image lookups when showing instances') -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) - - if args.rebuild_password is not False: - _password = args.rebuild_password - else: - _password = None - - kwargs = utils.get_resource_manager_extra_kwargs(do_rebuild, args) - s = server.rebuild(image, _password, **kwargs) - _print_server(cs, args) - - if args.poll: - _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) - - -@utils.arg('server', metavar='', - help='Name (old name) or ID of server.') -@utils.arg('name', metavar='', help='New name for the server.') -def do_rename(cs, args): - """Rename a server.""" - _find_server(cs, args.server).update(name=args.name) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('flavor', metavar='', help="Name or ID of new flavor.") -@utils.arg('--poll', - dest='poll', - action="store_true", - default=False, - help='Blocks while instance resizes so progress can be reported.') -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) - if args.poll: - _poll_for_status(cs.servers.get, server.id, 'resizing', - ['active', 'verify_resize']) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_resize_confirm(cs, args): - """Confirm a previous resize.""" - _find_server(cs, args.server).confirm_resize() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_resize_revert(cs, args): - """Revert a previous resize (and return to the previous VM).""" - _find_server(cs, args.server).revert_resize() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('--poll', - dest='poll', - action="store_true", - default=False, - help='Blocks while instance migrates so progress can be reported.') -def do_migrate(cs, args): - """Migrate a server.""" - server = _find_server(cs, args.server) - server.migrate() - - if args.poll: - _poll_for_status(cs.servers.get, server.id, 'migrating', - ['active', 'verify_resize']) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_pause(cs, args): - """Pause a server.""" - _find_server(cs, args.server).pause() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_unpause(cs, args): - """Unpause a server.""" - _find_server(cs, args.server).unpause() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_stop(cs, args): - """Stop a server.""" - _find_server(cs, args.server).stop() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_start(cs, args): - """Start a server.""" - _find_server(cs, args.server).start() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_lock(cs, args): - """Lock a server.""" - _find_server(cs, args.server).lock() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_unlock(cs, args): - """Unlock a server.""" - _find_server(cs, args.server).unlock() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_suspend(cs, args): - """Suspend a server.""" - _find_server(cs, args.server).suspend() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_resume(cs, args): - """Resume a server.""" - _find_server(cs, args.server).resume() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_rescue(cs, args): - """Rescue a server.""" - utils.print_dict(_find_server(cs, args.server).rescue()[1]) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_unrescue(cs, args): - """Unrescue a server.""" - _find_server(cs, args.server).unrescue() - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_diagnostics(cs, args): - """Retrieve server diagnostics.""" - server = _find_server(cs, args.server) - utils.print_dict(cs.servers.diagnostics(server)[1]) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_actions(cs, args): - """Retrieve server actions.""" - server = _find_server(cs, args.server) - utils.print_list( - cs.servers.actions(server), - ["Created_At", "Action", "Error"]) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_root_password(cs, args): - """ - Change the root password for a server. - """ - server = _find_server(cs, args.server) - p1 = getpass.getpass('New password: ') - p2 = getpass.getpass('Again: ') - if p1 != p2: - raise exceptions.CommandError("Passwords do not match.") - server.change_password(p1) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('name', metavar='', help='Name of snapshot.') -@utils.arg('--poll', - dest='poll', - action="store_true", - default=False, - help='Blocks while instance snapshots so progress can be reported.') -def do_image_create(cs, args): - """Create a new image by taking a snapshot of a running server.""" - server = _find_server(cs, args.server) - image_uuid = cs.servers.create_image(server, args.name) - - if args.poll: - _poll_for_status(cs.images.get, image_uuid, 'snapshotting', - ['active']) - - # NOTE(sirp): A race-condition exists between when the image finishes - # uploading and when the servers's `task_state` is cleared. To account - # for this, we need to poll a second time to ensure the `task_state` is - # cleared before returning, ensuring that a snapshot taken immediately - # after this function returns will succeed. - # - # A better long-term solution will be to separate 'snapshotting' and - # 'image-uploading' in Nova and clear the task-state once the VM - # snapshot is complete but before the upload begins. - task_state_field = "OS-EXT-STS:task_state" - if hasattr(server, task_state_field): - _poll_for_status(cs.servers.get, server.id, 'image_snapshot', - [None], status_field=task_state_field, - show_progress=False, silent=True) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('name', metavar='', help='Name of the backup image.') -@utils.arg('backup_type', metavar='', - help='The backup type, like "daily" or "weekly".') -@utils.arg('rotation', metavar='', - help='Int parameter representing how many backups to keep around.') -def do_backup(cs, args): - """ Backup a instance by create a 'backup' type snapshot """ - _find_server(cs, args.server).backup(args.name, - args.backup_type, - args.rotation) - - -@utils.arg('server', - metavar='', - help="Name or ID of server") -@utils.arg('action', - metavar='', - choices=['set', 'delete'], - help="Actions: 'set' or 'delete'") -@utils.arg('metadata', - metavar='', - nargs='+', - action='append', - default=[], - help='Metadata to set or delete (only key is necessary on delete)') -def do_meta(cs, args): - """Set or Delete metadata on a server.""" - server = _find_server(cs, args.server) - metadata = _extract_metadata(args) - - if args.action == 'set': - cs.servers.set_meta(server, metadata) - elif args.action == 'delete': - cs.servers.delete_meta(server, metadata.keys()) - - -def _print_server(cs, args): - # By default when searching via name we will do a - # findall(name=blah) and due a REST /details which is not the same - # as a .get() and doesn't get the information about flavors and - # images. This fix it as we redo the call with the id which does a - # .get() to get all informations. - server = _find_server(cs, args.server) - - networks = server.networks - info = server._info.copy() - for network_label, address_list in networks.items(): - info['%s network' % network_label] = ', '.join(address_list) - - flavor = info.get('flavor', {}) - flavor_id = flavor.get('id', '') - if args.minimal: - info['flavor'] = flavor_id - else: - info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, - flavor_id) - - image = info.get('image', {}) - image_id = image.get('id', '') - if args.minimal: - info['image'] = image_id - else: - try: - info['image'] = '%s (%s)' % (_find_image(cs, image_id).name, - image_id) - except Exception: - info['image'] = '%s (%s)' % ("Image not found", image_id) - - info.pop('links', None) - info.pop('addresses', None) - - utils.print_dict(info) - - -@utils.arg('--minimal', - dest='minimal', - action="store_true", - default=False, - help='Skips flavor/image lookups when showing instances') -@utils.arg('server', metavar='', help='Name or ID of server.') -def do_show(cs, args): - """Show details about the given server.""" - _print_server(cs, args) - - -@utils.arg('server', metavar='', nargs='+', - help='Name or ID of server(s).') -def do_delete(cs, args): - """Immediately shut down and delete specified server(s).""" - for server in args.server: - try: - _find_server(cs, server).delete() - except Exception, e: - print e - - -def _find_server(cs, server): - """Get a server by name or ID.""" - return utils.find_resource(cs.servers, server) - - -def _find_image(cs, image): - """Get an image by name or ID.""" - return utils.find_resource(cs.images, image) - - -def _find_flavor(cs, flavor): - """Get a flavor by name, ID, or RAM size.""" - try: - return utils.find_resource(cs.flavors, flavor) - except exceptions.NotFound: - return cs.flavors.find(ram=flavor) - - -@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): - """Add new IP address to network.""" - 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): - """Remove an IP address from a server.""" - server = _find_server(cs, args.server) - server.remove_fixed_ip(args.address) - - -def _find_volume(cs, volume): - """Get a volume by ID.""" - return utils.find_resource(cs.volumes, volume) - - -def _find_volume_snapshot(cs, snapshot): - """Get a volume snapshot by ID.""" - return utils.find_resource(cs.volume_snapshots, snapshot) - - -def _print_volume(volume): - utils.print_dict(volume._info) - - -def _print_volume_snapshot(snapshot): - utils.print_dict(snapshot._info) - - -def _translate_volume_keys(collection): - convert = [('displayName', 'display_name'), ('volumeType', 'volume_type')] - for item in collection: - keys = item.__dict__.keys() - for from_key, to_key in convert: - if from_key in keys and to_key not in keys: - setattr(item, to_key, item._info[from_key]) - - -def _translate_volume_snapshot_keys(collection): - convert = [('displayName', 'display_name'), ('volumeId', 'volume_id')] - for item in collection: - keys = item.__dict__.keys() - for from_key, to_key in convert: - if from_key in keys and to_key not in keys: - setattr(item, to_key, item._info[from_key]) - - -@utils.arg('--all-tenants', - dest='all_tenants', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), - help='Display information from all tenants (Admin only).') -@utils.arg('--all_tenants', - nargs='?', - type=int, - const=1, - help=argparse.SUPPRESS) -@utils.service_type('volume') -def do_volume_list(cs, args): - """List all the volumes.""" - search_opts = {'all_tenants': args.all_tenants} - volumes = cs.volumes.list(search_opts=search_opts) - _translate_volume_keys(volumes) - - # Create a list of servers to which the volume is attached - for vol in volumes: - servers = [s.get('server_id') for s in vol.attachments] - setattr(vol, 'attached_to', ','.join(map(str, servers))) - utils.print_list(volumes, ['ID', 'Status', 'Display Name', - 'Size', 'Volume Type', 'Attached to']) - - -@utils.arg('volume', metavar='', help='ID of the volume.') -@utils.service_type('volume') -def do_volume_show(cs, args): - """Show details about a volume.""" - volume = _find_volume(cs, args.volume) - _print_volume(volume) - - -@utils.arg('size', - metavar='', - type=int, - help='Size of volume in GB') -@utils.arg('--snapshot-id', - metavar='', - default=None, - help='Optional snapshot id to create the volume from. (Default=None)') -@utils.arg('--snapshot_id', - help=argparse.SUPPRESS) -@utils.arg('--image-id', - metavar='', - help='Optional image id to create the volume from. (Default=None)', - default=None) -@utils.arg('--display-name', - metavar='', - default=None, - help='Optional volume name. (Default=None)') -@utils.arg('--display_name', - help=argparse.SUPPRESS) -@utils.arg('--display-description', - metavar='', - default=None, - help='Optional volume description. (Default=None)') -@utils.arg('--display_description', - help=argparse.SUPPRESS) -@utils.arg('--volume-type', - metavar='', - default=None, - help='Optional volume type. (Default=None)') -@utils.arg('--volume_type', - help=argparse.SUPPRESS) -@utils.arg('--availability-zone', metavar='', - help='Optional Availability Zone for volume. (Default=None)', - default=None) -@utils.service_type('volume') -def do_volume_create(cs, args): - """Add a new volume.""" - volume = cs.volumes.create(args.size, - args.snapshot_id, - args.display_name, - args.display_description, - args.volume_type, - args.availability_zone, - imageRef=args.image_id) - _print_volume(volume) - - -@utils.arg('volume', metavar='', help='ID of the volume to delete.') -@utils.service_type('volume') -def do_volume_delete(cs, args): - """Remove a volume.""" - volume = _find_volume(cs, args.volume) - volume.delete() - - -@utils.arg('server', - metavar='', - help='Name or ID of server.') -@utils.arg('volume', - metavar='', - help='ID of the volume to attach.') -@utils.arg('device', metavar='', - help='Name of the device e.g. /dev/vdb. ' - 'Use "auto" for autoassign (if supported)') -def do_volume_attach(cs, args): - """Attach a volume to a server.""" - if args.device == 'auto': - args.device = None - - volume = cs.volumes.create_server_volume(_find_server(cs, args.server).id, - args.volume, - args.device) - _print_volume(volume) - - -@utils.arg('server', - metavar='', - help='Name or ID of server.') -@utils.arg('attachment_id', - metavar='', - help='Attachment ID of the volume.') -def do_volume_detach(cs, args): - """Detach a volume from a server.""" - cs.volumes.delete_server_volume(_find_server(cs, args.server).id, - args.attachment_id) - - -@utils.service_type('volume') -def do_volume_snapshot_list(cs, _args): - """List all the snapshots.""" - snapshots = cs.volume_snapshots.list() - _translate_volume_snapshot_keys(snapshots) - utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Display Name', - 'Size']) - - -@utils.arg('snapshot', metavar='', help='ID of the snapshot.') -@utils.service_type('volume') -def do_volume_snapshot_show(cs, args): - """Show details about a snapshot.""" - snapshot = _find_volume_snapshot(cs, args.snapshot) - _print_volume_snapshot(snapshot) - - -@utils.arg('volume_id', - metavar='', - help='ID of the volume to snapshot') -@utils.arg('--force', - metavar='', - help='Optional flag to indicate whether to snapshot a volume even if its ' - 'attached to an instance. (Default=False)', - default=False) -@utils.arg('--display-name', - metavar='', - default=None, - help='Optional snapshot name. (Default=None)') -@utils.arg('--display_name', - help=argparse.SUPPRESS) -@utils.arg('--display-description', - metavar='', - default=None, - help='Optional snapshot description. (Default=None)') -@utils.arg('--display_description', - help=argparse.SUPPRESS) -@utils.service_type('volume') -def do_volume_snapshot_create(cs, args): - """Add a new snapshot.""" - snapshot = cs.volume_snapshots.create(args.volume_id, - args.force, - args.display_name, - args.display_description) - _print_volume_snapshot(snapshot) - - -@utils.arg('snapshot_id', - metavar='', - help='ID of the snapshot to delete.') -@utils.service_type('volume') -def do_volume_snapshot_delete(cs, args): - """Remove a snapshot.""" - snapshot = _find_volume_snapshot(cs, args.snapshot_id) - snapshot.delete() - - -def _print_volume_type_list(vtypes): - utils.print_list(vtypes, ['ID', 'Name']) - - -@utils.service_type('volume') -def do_volume_type_list(cs, args): - """Print a list of available 'volume types'.""" - vtypes = cs.volume_types.list() - _print_volume_type_list(vtypes) - - -@utils.arg('name', - metavar='', - help="Name of the new flavor") -@utils.service_type('volume') -def do_volume_type_create(cs, args): - """Create a new volume type.""" - vtype = cs.volume_types.create(args.name) - _print_volume_type_list([vtype]) - - -@utils.arg('id', - metavar='', - help="Unique ID of the volume type to delete") -@utils.service_type('volume') -def do_volume_type_delete(cs, args): - """Delete a specific flavor""" - cs.volume_types.delete(args.id) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('console_type', - metavar='', - help='Type of vnc console ("novnc" or "xvpvnc").') -def do_get_vnc_console(cs, args): - """Get a vnc console to a server.""" - server = _find_server(cs, args.server) - data = server.get_vnc_console(args.console_type) - - class VNCConsole: - def __init__(self, console_dict): - self.type = console_dict['type'] - self.url = console_dict['url'] - - utils.print_list([VNCConsole(data['console'])], ['Type', 'Url']) - - -def _print_floating_ip_list(floating_ips): - utils.print_list(floating_ips, ['Ip', 'Instance Id', 'Fixed Ip', 'Pool']) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('--length', - metavar='', - default=None, - help='Length in lines to tail.') -def do_console_log(cs, args): - """Get console log output of a server.""" - server = _find_server(cs, args.server) - data = server.get_console_output(length=args.length) - print data - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('address', metavar='
', help='IP Address.') -def do_add_floating_ip(cs, args): - """Add a floating IP address to a server.""" - server = _find_server(cs, args.server) - server.add_floating_ip(args.address) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('address', metavar='
', help='IP Address.') -def do_remove_floating_ip(cs, args): - """Remove a floating IP address from a server.""" - 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', metavar='', help='Name of Security Group.') -def do_add_secgroup(cs, args): - """Add a Security Group to a server.""" - server = _find_server(cs, args.server) - server.add_security_group(args.secgroup) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('secgroup', metavar='', help='Name of Security Group.') -def do_remove_secgroup(cs, args): - """Remove a Security Group from a server.""" - server = _find_server(cs, args.server) - server.remove_security_group(args.secgroup) - - -@utils.arg('pool', - metavar='', - help='Name of Floating IP Pool. (Optional)', - nargs='?', - default=None) -def do_floating_ip_create(cs, args): - """Allocate a floating IP for the current tenant.""" - _print_floating_ip_list([cs.floating_ips.create(pool=args.pool)]) - - -@utils.arg('address', metavar='
', help='IP of Floating Ip.') -def do_floating_ip_delete(cs, args): - """De-allocate a floating IP.""" - floating_ips = cs.floating_ips.list() - for floating_ip in floating_ips: - if floating_ip.ip == args.address: - return cs.floating_ips.delete(floating_ip.id) - raise exceptions.CommandError("Floating ip %s not found." % args.address) - - -def do_floating_ip_list(cs, _args): - """List floating ips for this tenant.""" - _print_floating_ip_list(cs.floating_ips.list()) - - -def do_floating_ip_pool_list(cs, _args): - """List all floating ip pools.""" - utils.print_list(cs.floating_ip_pools.list(), ['name']) - - -def _print_dns_list(dns_entries): - utils.print_list(dns_entries, ['ip', 'name', 'domain']) - - -def _print_domain_list(domain_entries): - utils.print_list(domain_entries, ['domain', 'scope', - 'project', 'availability_zone']) - - -def do_dns_domains(cs, args): - """Print a list of available dns domains.""" - domains = cs.dns_domains.domains() - _print_domain_list(domains) - - -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('--ip', metavar='', help='ip address', default=None) -@utils.arg('--name', metavar='', help='DNS name', default=None) -def do_dns_list(cs, args): - """List current DNS entries for domain and ip or domain and name.""" - if not (args.ip or args.name): - raise exceptions.CommandError( - "You must specify either --ip or --name") - if args.name: - entry = cs.dns_entries.get(args.domain, args.name) - _print_dns_list([entry]) - else: - entries = cs.dns_entries.get_for_ip(args.domain, - ip=args.ip) - _print_dns_list(entries) - - -@utils.arg('ip', metavar='', help='ip address') -@utils.arg('name', metavar='', help='DNS name') -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('--type', metavar='', help='dns type (e.g. "A")', default='A') -def do_dns_create(cs, args): - """Create a DNS entry for domain, name and ip.""" - cs.dns_entries.create(args.domain, args.name, args.ip, args.type) - - -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('name', metavar='', help='DNS name') -def do_dns_delete(cs, args): - """Delete the specified DNS entry.""" - cs.dns_entries.delete(args.domain, args.name) - - -@utils.arg('domain', metavar='', help='DNS domain') -def do_dns_delete_domain(cs, args): - """Delete the specified DNS domain.""" - cs.dns_domains.delete(args.domain) - - -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('--availability-zone', - metavar='', - default=None, - help='Limit access to this domain to instances ' - 'in the specified availability zone.') -@utils.arg('--availability_zone', - help=argparse.SUPPRESS) -def do_dns_create_private_domain(cs, args): - """Create the specified DNS domain.""" - cs.dns_domains.create_private(args.domain, - args.availability_zone) - - -@utils.arg('domain', metavar='', help='DNS domain') -@utils.arg('--project', metavar='', - help='Limit access to this domain to users ' - 'of the specified project.', - default=None) -def do_dns_create_public_domain(cs, args): - """Create the specified DNS domain.""" - cs.dns_domains.create_public(args.domain, - args.project) - - -def _print_secgroup_rules(rules): - class FormattedRule: - def __init__(self, obj): - items = (obj if isinstance(obj, dict) else obj._info).items() - for k, v in items: - if k == 'ip_range': - v = v.get('cidr') - elif k == 'group': - k = 'source_group' - v = v.get('name') - if v is None: - v = '' - - setattr(self, k, v) - - rules = [FormattedRule(rule) for rule in rules] - utils.print_list(rules, ['IP Protocol', 'From Port', 'To Port', - 'IP Range', 'Source Group']) - - -def _print_secgroups(secgroups): - utils.print_list(secgroups, ['Name', 'Description']) - - -def _get_secgroup(cs, secgroup): - for s in cs.security_groups.list(): - encoding = (locale.getpreferredencoding() or - sys.stdin.encoding or - 'UTF-8') - s.name = s.name.encode(encoding) - if secgroup == s.name: - return s - raise exceptions.CommandError("Secgroup %s not found" % secgroup) - - -@utils.arg('secgroup', metavar='', help='ID of security group.') -@utils.arg('ip_proto', - metavar='', - help='IP protocol (icmp, tcp, udp).') -@utils.arg('from_port', - metavar='', - help='Port at start of range.') -@utils.arg('to_port', - metavar='', - help='Port at end of range.') -@utils.arg('cidr', metavar='', help='CIDR for address range.') -def do_secgroup_add_rule(cs, args): - """Add a rule to a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - rule = cs.security_group_rules.create(secgroup.id, - args.ip_proto, - args.from_port, - args.to_port, - args.cidr) - _print_secgroup_rules([rule]) - - -@utils.arg('secgroup', metavar='', help='ID of security group.') -@utils.arg('ip_proto', - metavar='', - help='IP protocol (icmp, tcp, udp).') -@utils.arg('from_port', - metavar='', - help='Port at start of range.') -@utils.arg('to_port', - metavar='', - help='Port at end of range.') -@utils.arg('cidr', metavar='', help='CIDR for address range.') -def do_secgroup_delete_rule(cs, args): - """Delete a rule from a security group.""" - - secgroup = _get_secgroup(cs, args.secgroup) - for rule in secgroup.rules: - if (rule['ip_protocol'] == args.ip_proto and - rule['from_port'] == int(args.from_port) and - rule['to_port'] == int(args.to_port) and - rule['ip_range']['cidr'] == args.cidr): - return cs.security_group_rules.delete(rule['id']) - - raise exceptions.CommandError("Rule not found") - - -@utils.arg('name', metavar='', help='Name of security group.') -@utils.arg('description', metavar='', - help='Description of security group.') -def do_secgroup_create(cs, args): - """Create a security group.""" - _print_secgroups([cs.security_groups.create(args.name, args.description)]) - - -@utils.arg('secgroup', metavar='', help='Name of security group.') -def do_secgroup_delete(cs, args): - """Delete a security group.""" - cs.security_groups.delete(_get_secgroup(cs, args.secgroup)) - - -@utils.arg('--all-tenants', - dest='all_tenants', - metavar='<0|1>', - nargs='?', - type=int, - const=1, - default=int(utils.bool_from_str(os.environ.get("ALL_TENANTS", 'false'))), - help='Display information from all tenants (Admin only).') -@utils.arg('--all_tenants', - nargs='?', - type=int, - const=1, - help=argparse.SUPPRESS) -def do_secgroup_list(cs, args): - """List security groups for the current tenant.""" - search_opts = {'all_tenants': args.all_tenants} - _print_secgroups(cs.security_groups.list(search_opts=search_opts)) - - -@utils.arg('secgroup', metavar='', help='Name of security group.') -def do_secgroup_list_rules(cs, args): - """List rules for a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - _print_secgroup_rules(secgroup.rules) - - -@utils.arg('secgroup', metavar='', help='ID of security group.') -@utils.arg('source_group', - metavar='', - help='ID of source group.') -@utils.arg('ip_proto', - metavar='', - help='IP protocol (icmp, tcp, udp).') -@utils.arg('from_port', - metavar='', - help='Port at start of range.') -@utils.arg('to_port', - metavar='', - help='Port at end of range.') -def do_secgroup_add_group_rule(cs, args): - """Add a source group rule to a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - source_group = _get_secgroup(cs, args.source_group) - params = {} - params['group_id'] = source_group.id - - if args.ip_proto or args.from_port or args.to_port: - if not (args.ip_proto and args.from_port and args.to_port): - raise exceptions.CommandError("ip_proto, from_port, and to_port" - " must be specified together") - params['ip_protocol'] = args.ip_proto - params['from_port'] = args.from_port - params['to_port'] = args.to_port - - rule = cs.security_group_rules.create(secgroup.id, **params) - _print_secgroup_rules([rule]) - - -@utils.arg('secgroup', metavar='', help='ID of security group.') -@utils.arg('source_group', - metavar='', - help='ID of source group.') -@utils.arg('ip_proto', - metavar='', - help='IP protocol (icmp, tcp, udp).') -@utils.arg('from_port', - metavar='', - help='Port at start of range.') -@utils.arg('to_port', - metavar='', - help='Port at end of range.') -def do_secgroup_delete_group_rule(cs, args): - """Delete a source group rule from a security group.""" - secgroup = _get_secgroup(cs, args.secgroup) - source_group = _get_secgroup(cs, args.source_group) - params = {} - params['group_name'] = source_group.name - - if args.ip_proto or args.from_port or args.to_port: - if not (args.ip_proto and args.from_port and args.to_port): - raise exceptions.CommandError("ip_proto, from_port, and to_port" - " must be specified together") - params['ip_protocol'] = args.ip_proto - params['from_port'] = int(args.from_port) - params['to_port'] = int(args.to_port) - - for rule in secgroup.rules: - if (rule.get('ip_protocol') == params.get('ip_protocol') and - rule.get('from_port') == params.get('from_port') and - rule.get('to_port') == params.get('to_port') and - rule.get('group', {}).get('name') == \ - params.get('group_name')): - return cs.security_group_rules.delete(rule['id']) - - raise exceptions.CommandError("Rule not found") - - -@utils.arg('name', metavar='', help='Name of key.') -@utils.arg('--pub-key', - metavar='', - default=None, - help='Path to a public ssh key.') -@utils.arg('--pub_key', - help=argparse.SUPPRESS) -def do_keypair_add(cs, args): - """Create a new key pair for use with instances""" - name = args.name - pub_key = args.pub_key - - if pub_key: - try: - with open(pub_key) as f: - pub_key = f.read() - except IOError, e: - raise exceptions.CommandError("Can't open or read '%s': %s" % \ - (pub_key, e)) - - keypair = cs.keypairs.create(name, pub_key) - - if not pub_key: - private_key = keypair.private_key - print private_key - - -@utils.arg('name', metavar='', help='Keypair name to delete.') -def do_keypair_delete(cs, args): - """Delete keypair by its id""" - name = args.name - cs.keypairs.delete(name) - - -def do_keypair_list(cs, args): - """Print a list of keypairs for a user""" - keypairs = cs.keypairs.list() - columns = ['Name', 'Fingerprint'] - utils.print_list(keypairs, columns) - - -def do_absolute_limits(cs, args): - """Print a list of absolute limits for a user""" - limits = cs.limits.get().absolute - columns = ['Name', 'Value'] - utils.print_list(limits, columns) - - -def do_rate_limits(cs, args): - """Print a list of rate limits for a user""" - limits = cs.limits.get().rate - columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] - utils.print_list(limits, columns) - - -@utils.arg('--start', metavar='', - help='Usage range start date ex 2012-01-20 (default: 4 weeks ago)', - default=None) -@utils.arg('--end', metavar='', - help='Usage range end date, ex 2012-01-20 (default: tomorrow) ', - default=None) -def do_usage_list(cs, args): - """List usage data for all tenants""" - dateformat = "%Y-%m-%d" - rows = ["Tenant ID", "Instances", "RAM MB-Hours", "CPU Hours", - "Disk GB-Hours"] - - now = timeutils.utcnow() - - if args.start: - start = datetime.datetime.strptime(args.start, dateformat) - else: - start = now - datetime.timedelta(weeks=4) - - if args.end: - end = datetime.datetime.strptime(args.end, dateformat) - else: - end = now + datetime.timedelta(days=1) - - def simplify_usage(u): - simplerows = map(lambda x: x.lower().replace(" ", "_"), rows) - - setattr(u, simplerows[0], u.tenant_id) - setattr(u, simplerows[1], "%d" % len(u.server_usages)) - setattr(u, simplerows[2], "%.2f" % u.total_memory_mb_usage) - setattr(u, simplerows[3], "%.2f" % u.total_vcpus_usage) - setattr(u, simplerows[4], "%.2f" % u.total_local_gb_usage) - - usage_list = cs.usage.list(start, end, detailed=True) - - print "Usage from %s to %s:" % (start.strftime(dateformat), - end.strftime(dateformat)) - - for usage in usage_list: - simplify_usage(usage) - - utils.print_list(usage_list, rows) - - -@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): - """Create x509 cert for a user in tenant""" - - 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() - - with open(args.pk_filename, 'w') as private_key: - private_key.write(certs.private_key) - print "Wrote private key to %s" % args.pk_filename - - 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): - """Fetches the x509 root cert.""" - 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 - - -def do_aggregate_list(cs, args): - """Print a list of all aggregates.""" - aggregates = cs.aggregates.list() - columns = ['Id', 'Name', 'Availability Zone'] - utils.print_list(aggregates, columns) - - -@utils.arg('name', metavar='', help='Name of aggregate.') -@utils.arg('availability_zone', - metavar='', - help='The availability zone of the aggregate.') -def do_aggregate_create(cs, args): - """Create a new aggregate with the specified details.""" - aggregate = cs.aggregates.create(args.name, args.availability_zone) - _print_aggregate_details(aggregate) - - -@utils.arg('id', metavar='', help='Aggregate id to delete.') -def do_aggregate_delete(cs, args): - """Delete the aggregate by its id.""" - cs.aggregates.delete(args.id) - print "Aggregate %s has been successfully deleted." % args.id - - -@utils.arg('id', metavar='', help='Aggregate id to update.') -@utils.arg('name', metavar='', help='Name of aggregate.') -@utils.arg('availability_zone', - metavar='', - nargs='?', - default=None, - help='The availability zone of the aggregate.') -def do_aggregate_update(cs, args): - """Update the aggregate's name and optionally availability zone.""" - updates = {"name": args.name} - if args.availability_zone: - updates["availability_zone"] = args.availability_zone - - aggregate = cs.aggregates.update(args.id, updates) - print "Aggregate %s has been successfully updated." % args.id - _print_aggregate_details(aggregate) - - -@utils.arg('id', metavar='', help='Aggregate id to update.') -@utils.arg('metadata', - metavar='', - nargs='+', - action='append', - default=[], - help='Metadata to add/update to aggregate') -def do_aggregate_set_metadata(cs, args): - """Update the metadata associated with the aggregate.""" - metadata = _extract_metadata(args) - aggregate = cs.aggregates.set_metadata(args.id, metadata) - print "Aggregate %s has been successfully updated." % args.id - _print_aggregate_details(aggregate) - - -@utils.arg('id', metavar='', help='Host aggregate id to delete.') -@utils.arg('host', metavar='', help='The host to add to the aggregate.') -def do_aggregate_add_host(cs, args): - """Add the host to the specified aggregate.""" - aggregate = cs.aggregates.add_host(args.id, args.host) - print "Aggregate %s has been successfully updated." % args.id - _print_aggregate_details(aggregate) - - -@utils.arg('id', metavar='', help='Host aggregate id to delete.') -@utils.arg('host', metavar='', help='The host to add to the aggregate.') -def do_aggregate_remove_host(cs, args): - """Remove the specified host from the specified aggregate.""" - aggregate = cs.aggregates.remove_host(args.id, args.host) - print "Aggregate %s has been successfully updated." % args.id - _print_aggregate_details(aggregate) - - -@utils.arg('id', metavar='', help='Host aggregate id to delete.') -def do_aggregate_details(cs, args): - """Show details of the specified aggregate.""" - _print_aggregate_details(cs.aggregates.get_details(args.id)) - - -def _print_aggregate_details(aggregate): - columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] - utils.print_list([aggregate], columns) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('host', metavar='', help='destination host name.') -@utils.arg('--block-migrate', - action='store_true', - dest='block_migrate', - default=False, - help='True in case of block_migration.\ - (Default=False:live_migration)') -@utils.arg('--block_migrate', - action='store_true', - help=argparse.SUPPRESS) -@utils.arg('--disk-over-commit', - action='store_true', - dest='disk_over_commit', - default=False, - help='Allow overcommit.(Default=Flase)') -@utils.arg('--disk_over_commit', - action='store_true', - help=argparse.SUPPRESS) -def do_live_migration(cs, args): - """Migrates a running instance to a new machine.""" - _find_server(cs, args.server).live_migrate(args.host, - args.block_migrate, - args.disk_over_commit) - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('--active', action='store_const', dest='state', - default='error', const='active', - help='Request the instance be reset to "active" state instead ' - 'of "error" state (the default).') -def do_reset_state(cs, args): - """Reset the state of an instance""" - _find_server(cs, args.server).reset_state(args.state) - - -@utils.arg('host', metavar='', help='Name of host.') -def do_host_describe(cs, args): - """Describe a specific host""" - 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): - """List all hosts by service""" - columns = ["host_name", "service", "zone"] - result = cs.hosts.list_all(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): - """Update host settings.""" - 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): - """Perform a power action on a host.""" - result = cs.hosts.host_action(args.host, args.action) - utils.print_list([result], ['HOST', 'power_action']) - - -@utils.arg('--matching', metavar='', default=None, - help='List hypervisors matching the given .') -def do_hypervisor_list(cs, args): - """List hypervisors.""" - columns = ['ID', 'Hypervisor hostname'] - if args.matching: - utils.print_list(cs.hypervisors.search(args.matching), columns) - else: - # Since we're not outputting detail data, choose - # detailed=False for server-side efficiency - utils.print_list(cs.hypervisors.list(False), columns) - - -@utils.arg('hostname', metavar='', - help='The hypervisor hostname (or pattern) to search for.') -def do_hypervisor_servers(cs, args): - """List instances belonging to specific hypervisors.""" - hypers = cs.hypervisors.search(args.hostname, servers=True) - - class InstanceOnHyper(object): - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - - # Massage the result into a list to be displayed - instances = [] - for hyper in hypers: - hyper_host = hyper.hypervisor_hostname - hyper_id = hyper.id - instances.extend([InstanceOnHyper(id=serv['uuid'], - name=serv['name'], - hypervisor_hostname=hyper_host, - hypervisor_id=hyper_id) - for serv in hyper.servers]) - - # Output the data - utils.print_list(instances, ['ID', 'Name', 'Hypervisor ID', - 'Hypervisor Hostname']) - - -@utils.arg('hypervisor_id', - metavar='', - help='The ID of the hypervisor to show the details of.') -def do_hypervisor_show(cs, args): - """Display the details of the specified hypervisor.""" - hyper = utils.find_resource(cs.hypervisors, args.hypervisor_id) - - # Build up the dict - info = hyper._info.copy() - info['service_id'] = info['service']['id'] - info['service_host'] = info['service']['host'] - del info['service'] - - utils.print_dict(info) - - -@utils.arg('hypervisor_id', - metavar='', - help='The ID of the hypervisor to show the uptime of.') -def do_hypervisor_uptime(cs, args): - """Display the uptime of the specified hypervisor.""" - hyper = cs.hypervisors.uptime(args.hypervisor_id) - - # Output the uptime information - utils.print_dict(hyper._info.copy()) - - -def do_hypervisor_stats(cs, args): - """Get hypervisor statistics over all compute nodes.""" - stats = cs.hypervisors.statistics() - utils.print_dict(stats._info.copy()) - - -def ensure_service_catalog_present(cs): - if not hasattr(cs.client, 'service_catalog'): - # Turn off token caching and re-auth - cs.client.unauthenticate() - cs.client.use_token_cache(False) - cs.client.authenticate() - - -def do_endpoints(cs, _args): - """Discover endpoints that get returned from the authenticate services""" - ensure_service_catalog_present(cs) - catalog = cs.client.service_catalog.catalog - for e in catalog['access']['serviceCatalog']: - utils.print_dict(e['endpoints'][0], e['name']) - - -def do_credentials(cs, _args): - """Show user credentials returned from auth""" - ensure_service_catalog_present(cs) - catalog = cs.client.service_catalog.catalog - utils.print_dict(catalog['access']['user'], "User Credentials") - utils.print_dict(catalog['access']['token'], "Token") - - -@utils.arg('server', metavar='', help='Name or ID of server.') -@utils.arg('--port', - dest='port', - action='store', - type=int, - default=22, - help='Optional flag to indicate which port to use for ssh. ' - '(Default=22)') -@utils.arg('--private', - dest='private', - action='store_true', - default=False, - help='Optional flag to indicate whether to use private address ' - 'attached to an instance. (Default=False)') -@utils.arg('--ipv6', - dest='ipv6', - action='store_true', - default=False, - help='Optional flag to indicate whether to use an IPv6 address ' - 'attached to an instance. (Defaults to IPv4 address)') -@utils.arg('--login', metavar='', help='Login to use.', default="root") -@utils.arg('-i', '--identity', - dest='identity', - help='Private key file, same as the -i option to the ssh command.', - default='') -def do_ssh(cs, args): - """SSH into a server.""" - addresses = _find_server(cs, args.server).addresses - address_type = "private" if args.private else "public" - version = 6 if args.ipv6 else 4 - - if address_type not in addresses: - print "ERROR: No %s addresses found for '%s'." % (address_type, - args.server) - return - - ip_address = None - for address in addresses[address_type]: - if address['version'] == version: - ip_address = address['addr'] - break - - identity = '-i %s' % args.identity if len(args.identity) else '' - - if ip_address: - os.system("ssh -%d -p%d %s %s@%s" % (version, args.port, identity, - args.login, ip_address)) - else: - pretty_version = "IPv%d" % version - print "ERROR: No %s %s address found." % (address_type, - pretty_version) - return - - -_quota_resources = ['instances', 'cores', 'ram', 'volumes', 'gigabytes', - 'floating_ips', 'metadata_items', 'injected_files', - 'injected_file_content_bytes'] - - -def _quota_show(quotas): - quota_dict = {} - for resource in _quota_resources: - quota_dict[resource] = getattr(quotas, resource, None) - utils.print_dict(quota_dict) - - -def _quota_update(manager, identifier, args): - updates = {} - for resource in _quota_resources: - val = getattr(args, resource, None) - if val is not None: - updates[resource] = val - - if updates: - manager.update(identifier, **updates) - - -@utils.arg('tenant', - metavar='', - help='UUID of tenant to list the quotas for.') -def do_quota_show(cs, args): - """List the quotas for a tenant.""" - - _quota_show(cs.quotas.get(args.tenant)) - - -@utils.arg('tenant', - metavar='', - help='UUID of tenant to list the default quotas for.') -def do_quota_defaults(cs, args): - """List the default quotas for a tenant.""" - - _quota_show(cs.quotas.defaults(args.tenant)) - - -@utils.arg('tenant', - metavar='', - help='UUID of tenant to set the quotas for.') -@utils.arg('--instances', - metavar='', - type=int, default=None, - help='New value for the "instances" quota.') -@utils.arg('--cores', - metavar='', - type=int, default=None, - help='New value for the "cores" quota.') -@utils.arg('--ram', - metavar='', - type=int, default=None, - help='New value for the "ram" quota.') -@utils.arg('--volumes', - metavar='', - type=int, default=None, - help='New value for the "volumes" quota.') -@utils.arg('--gigabytes', - metavar='', - type=int, default=None, - help='New value for the "gigabytes" quota.') -@utils.arg('--floating-ips', - metavar='', - type=int, - default=None, - help='New value for the "floating-ips" quota.') -@utils.arg('--floating_ips', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--metadata-items', - metavar='', - type=int, - default=None, - help='New value for the "metadata-items" quota.') -@utils.arg('--metadata_items', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--injected-files', - metavar='', - type=int, - default=None, - help='New value for the "injected-files" quota.') -@utils.arg('--injected_files', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--injected-file-content-bytes', - metavar='', - type=int, - default=None, - help='New value for the "injected-file-content-bytes" quota.') -@utils.arg('--injected_file_content_bytes', - type=int, - help=argparse.SUPPRESS) -def do_quota_update(cs, args): - """Update the quotas for a tenant.""" - - _quota_update(cs.quotas, args.tenant, args) - - -@utils.arg('class_name', - metavar='', - help='Name of quota class to list the quotas for.') -def do_quota_class_show(cs, args): - """List the quotas for a quota class.""" - - _quota_show(cs.quota_classes.get(args.class_name)) - - -@utils.arg('class_name', - metavar='', - help='Name of quota class to set the quotas for.') -@utils.arg('--instances', - metavar='', - type=int, default=None, - help='New value for the "instances" quota.') -@utils.arg('--cores', - metavar='', - type=int, default=None, - help='New value for the "cores" quota.') -@utils.arg('--ram', - metavar='', - type=int, default=None, - help='New value for the "ram" quota.') -@utils.arg('--volumes', - metavar='', - type=int, default=None, - help='New value for the "volumes" quota.') -@utils.arg('--gigabytes', - metavar='', - type=int, default=None, - help='New value for the "gigabytes" quota.') -@utils.arg('--floating-ips', - metavar='', - type=int, - default=None, - help='New value for the "floating-ips" quota.') -@utils.arg('--floating_ips', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--metadata-items', - metavar='', - type=int, - default=None, - help='New value for the "metadata-items" quota.') -@utils.arg('--metadata_items', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--injected-files', - metavar='', - type=int, - default=None, - help='New value for the "injected-files" quota.') -@utils.arg('--injected_files', - type=int, - help=argparse.SUPPRESS) -@utils.arg('--injected-file-content-bytes', - metavar='', - type=int, - default=None, - help='New value for the "injected-file-content-bytes" quota.') -@utils.arg('--injected_file_content_bytes', - type=int, - help=argparse.SUPPRESS) -def do_quota_class_update(cs, args): - """Update the quotas for a quota class.""" - - _quota_update(cs.quota_classes, args.class_name, args) diff --git a/novaclient/v1_1/usage.py b/novaclient/v1_1/usage.py deleted file mode 100644 index b55f159ba..000000000 --- a/novaclient/v1_1/usage.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -Usage interface. -""" - -from novaclient import base - - -class Usage(base.Resource): - """ - Usage contains infomartion about a tenants physical resource usage - """ - def __repr__(self): - return "" - - -class UsageManager(base.ManagerWithFind): - """ - Manage :class:`Usage` resources. - """ - resource_class = Usage - - def list(self, start, end, detailed=False): - """ - Get usage for all tenants - - :param start: :class:`datetime.datetime` Start date - :param end: :class:`datetime.datetime` End date - :param detailed: Whether to include information about each - instance whose usage is part of the report - :rtype: list of :class:`Usage`. - """ - return self._list( - "/os-simple-tenant-usage?start=%s&end=%s&detailed=%s" % - (start.isoformat(), end.isoformat(), int(bool(detailed))), - "tenant_usages") - - def get(self, tenant_id, start, end): - """ - Get usage for a specific tenant. - - :param tenant_id: Tenant ID to fetch usage for - :param start: :class:`datetime.datetime` Start date - :param end: :class:`datetime.datetime` End date - :rtype: :class:`Usage` - """ - return self._get("/os-simple-tenant-usage/%s?start=%s&end=%s" % - (tenant_id, start.isoformat(), end.isoformat()), - "tenant_usage") diff --git a/novaclient/v1_1/volume_snapshots.py b/novaclient/v1_1/volume_snapshots.py deleted file mode 100644 index b30a60a82..000000000 --- a/novaclient/v1_1/volume_snapshots.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright 2011 Denali Systems, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Volume snapshot interface (1.1 extension). -""" - -from novaclient import base - - -class Snapshot(base.Resource): - """ - A Snapshot is a point-in-time snapshot of an openstack volume. - """ - NAME_ATTR = 'display_name' - - def __repr__(self): - return "" % self.id - - def delete(self): - """ - Delete this snapshot. - """ - self.manager.delete(self) - - -class SnapshotManager(base.ManagerWithFind): - """ - Manage :class:`Snapshot` resources. - """ - resource_class = Snapshot - - def create(self, volume_id, force=False, - display_name=None, display_description=None): - - """ - Create a snapshot of the given volume. - - :param volume_id: The ID of the volume to snapshot. - :param force: If force is True, create a snapshot even if the volume is - attached to an instance. Default is False. - :param display_name: Name of the snapshot - :param display_description: Description of the snapshot - :rtype: :class:`Snapshot` - """ - body = {'snapshot': {'volume_id': volume_id, - 'force': force, - 'display_name': display_name, - 'display_description': display_description}} - return self._create('/snapshots', body, 'snapshot') - - def get(self, snapshot_id): - """ - Get a snapshot. - - :param snapshot_id: The ID of the snapshot to get. - :rtype: :class:`Snapshot` - """ - return self._get("/snapshots/%s" % snapshot_id, "snapshot") - - def list(self, detailed=True): - """ - Get a list of all snapshots. - - :rtype: list of :class:`Snapshot` - """ - if detailed is True: - return self._list("/snapshots/detail", "snapshots") - else: - return self._list("/snapshots", "snapshots") - - def delete(self, snapshot): - """ - Delete a snapshot. - - :param snapshot: The :class:`Snapshot` to delete. - """ - self._delete("/snapshots/%s" % base.getid(snapshot)) diff --git a/novaclient/v1_1/volume_types.py b/novaclient/v1_1/volume_types.py deleted file mode 100644 index 3d1c7f531..000000000 --- a/novaclient/v1_1/volume_types.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) 2011 Rackspace US, 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. - - -""" -Volume Type interface. -""" - -from novaclient import base - - -class VolumeType(base.Resource): - """ - A Volume Type is the type of volume to be created - """ - def __repr__(self): - return "" % self.name - - -class VolumeTypeManager(base.ManagerWithFind): - """ - Manage :class:`VolumeType` resources. - """ - resource_class = VolumeType - - def list(self): - """ - Get a list of all volume types. - - :rtype: list of :class:`VolumeType`. - """ - return self._list("/types", "volume_types") - - def get(self, volume_type): - """ - Get a specific volume type. - - :param volume_type: The ID of the :class:`VolumeType` to get. - :rtype: :class:`VolumeType` - """ - return self._get("/types/%s" % base.getid(volume_type), "volume_type") - - def delete(self, volume_type): - """ - Delete a specific volume_type. - - :param volume_type: The ID of the :class:`VolumeType` to get. - """ - self._delete("/types/%s" % base.getid(volume_type)) - - def create(self, name): - """ - Create a volume type. - - :param name: Descriptive name of the volume type - :rtype: :class:`VolumeType` - """ - - body = { - "volume_type": { - "name": name, - } - } - - return self._create("/types", body, "volume_type") diff --git a/novaclient/v1_1/volumes.py b/novaclient/v1_1/volumes.py deleted file mode 100644 index 7e371ac7b..000000000 --- a/novaclient/v1_1/volumes.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright 2011 Denali Systems, Inc. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Volume interface (1.1 extension). -""" - -import urllib - -from novaclient import base - - -class Volume(base.Resource): - """ - A volume is an extra block level storage to the OpenStack instances. - """ - NAME_ATTR = 'display_name' - - def __repr__(self): - return "" % self.id - - def delete(self): - """ - Delete this volume. - """ - self.manager.delete(self) - - -class VolumeManager(base.ManagerWithFind): - """ - Manage :class:`Volume` resources. - """ - resource_class = Volume - - def create(self, size, snapshot_id=None, - display_name=None, display_description=None, - volume_type=None, availability_zone=None, - imageRef=None): - """ - Create a volume. - - :param size: Size of volume in GB - :param snapshot_id: ID of the snapshot - :param display_name: Name of the volume - :param display_description: Description of the volume - :param volume_type: Type of volume - :param availability_zone: Availability Zone for volume - :rtype: :class:`Volume` - :param imageRef: reference to an image stored in glance - """ - body = {'volume': {'size': size, - 'snapshot_id': snapshot_id, - 'display_name': display_name, - 'display_description': display_description, - 'volume_type': volume_type, - 'availability_zone': availability_zone, - 'imageRef': imageRef}} - return self._create('/volumes', body, 'volume') - - def get(self, volume_id): - """ - Get a volume. - - :param volume_id: The ID of the volume to delete. - :rtype: :class:`Volume` - """ - return self._get("/volumes/%s" % volume_id, "volume") - - def list(self, detailed=True, search_opts=None): - """ - Get a list of all volumes. - - :rtype: list of :class:`Volume` - """ - search_opts = search_opts or {} - - qparams = dict((k, v) for (k, v) in search_opts.iteritems() if v) - - query_string = '?%s' % urllib.urlencode(qparams) if qparams else '' - - if detailed is True: - return self._list("/volumes/detail%s" % query_string, "volumes") - else: - return self._list("/volumes%s" % query_string, "volumes") - - def delete(self, volume): - """ - Delete a volume. - - :param volume: The :class:`Volume` to delete. - """ - self._delete("/volumes/%s" % base.getid(volume)) - - def create_server_volume(self, server_id, volume_id, device): - """ - 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 - :rtype: :class:`Volume` - """ - body = {'volumeAttachment': {'volumeId': volume_id, - 'device': device}} - return self._create("/servers/%s/os-volume_attachments" % server_id, - body, "volumeAttachment") - - def get_server_volume(self, server_id, attachment_id): - """ - Get the volume identified by the attachment ID, that is attached to - the given server ID - - :param server_id: The ID of the server - :param attachment_id: The ID of the attachment - :rtype: :class:`Volume` - """ - return self._get("/servers/%s/os-volume_attachments/%s" % (server_id, - attachment_id,), "volumeAttachment") - - def get_server_volumes(self, server_id): - """ - Get a list of all the attached volumes for the given server ID - - :param server_id: The ID of the server - :rtype: list of :class:`Volume` - """ - return self._list("/servers/%s/os-volume_attachments" % server_id, - "volumeAttachments") - - def delete_server_volume(self, server_id, attachment_id): - """ - Detach a volume identified by the attachment ID from the given server - - :param server_id: The ID of the server - :param attachment_id: The ID of the attachment - """ - self._delete("/servers/%s/os-volume_attachments/%s" % - (server_id, attachment_id,)) diff --git a/novaclient/v1_1/__init__.py b/novaclient/v2/__init__.py similarity index 88% rename from novaclient/v1_1/__init__.py rename to novaclient/v2/__init__.py index c8538c1e2..21d7c4fcc 100644 --- a/novaclient/v1_1/__init__.py +++ b/novaclient/v2/__init__.py @@ -1,4 +1,3 @@ -# Copyright (c) 2012 OpenStack, LLC. # # All Rights Reserved. # @@ -14,4 +13,4 @@ # License for the specific language governing permissions and limitations # under the License. -from novaclient.v1_1.client import Client +from novaclient.v2.client import Client # noqa diff --git a/novaclient/v2/agents.py b/novaclient/v2/agents.py new file mode 100644 index 000000000..dac3ff214 --- /dev/null +++ b/novaclient/v2/agents.py @@ -0,0 +1,77 @@ +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +agent interface +""" + +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): + return "" % self.agent + + def _add_details(self, info): + dico = 'resource' in info and info['resource'] or info + for (k, v) in dico.items(): + setattr(self, k, v) + + +class AgentsManager(base.ManagerWithFind): + resource_class = Agent + + def list(self, hypervisor=None): + """List all agent builds.""" + url = "/os-agents" + if hypervisor: + url = "/os-agents?hypervisor=%s" % hypervisor + return self._list(url, "agents") + + def _build_update_body(self, version, url, md5hash): + return {'para': {'version': version, + 'url': url, + 'md5hash': md5hash}} + + def update(self, id, version, + url, md5hash): + """Update an existing agent build.""" + body = self._build_update_body(version, url, md5hash) + return self._update('/os-agents/%s' % id, body, 'agent') + + def create(self, os, architecture, version, + url, md5hash, hypervisor): + """Create a new agent build.""" + body = {'agent': {'hypervisor': hypervisor, + 'os': os, + 'architecture': architecture, + 'version': version, + 'url': url, + 'md5hash': md5hash}} + return self._create('/os-agents', body, 'agent') + + def delete(self, id): + """ + Deletes an existing agent build. + + :param id: The agent's id to delete + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete('/os-agents/%s' % id) diff --git a/novaclient/v1_1/aggregates.py b/novaclient/v2/aggregates.py similarity index 62% rename from novaclient/v1_1/aggregates.py rename to novaclient/v2/aggregates.py index f55f486bc..d2cbaa858 100644 --- a/novaclient/v1_1/aggregates.py +++ b/novaclient/v2/aggregates.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -15,6 +15,7 @@ """Aggregate interface.""" +from novaclient import api_versions from novaclient import base @@ -38,7 +39,16 @@ def set_metadata(self, metadata): return self.manager.set_metadata(self, metadata) def delete(self): - self.manager.delete(self) + """ + Delete the own aggregate. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + 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): @@ -54,17 +64,23 @@ def create(self, name, availability_zone): 'availability_zone': availability_zone}} return self._create('/os-aggregates', body, 'aggregate') - def get_details(self, aggregate): + def get(self, aggregate): """Get details of the specified aggregate.""" return self._get('/os-aggregates/%s' % (base.getid(aggregate)), "aggregate") + # NOTE:(dtroyer): utils.find_resource() uses manager.get() but we need to + # keep the API backward compatible + def get_details(self, aggregate): + """Get details of the specified aggregate.""" + return self.get(aggregate) + def update(self, aggregate, values): """Update the name and/or availability zone.""" body = {'aggregate': values} - result = self._update("/os-aggregates/%s" % base.getid(aggregate), - body) - return self.resource_class(self, result["aggregate"]) + return self._update("/os-aggregates/%s" % base.getid(aggregate), + body, + "aggregate") def add_host(self, aggregate, host): """Add a host into the Host Aggregate.""" @@ -79,11 +95,33 @@ def remove_host(self, aggregate, host): body, "aggregate") def set_metadata(self, aggregate, metadata): - """Set a aggregate metadata, replacing the existing metadata.""" + """Set aggregate metadata, replacing the existing metadata.""" body = {'set_metadata': {'metadata': metadata}} return self._create("/os-aggregates/%s/action" % base.getid(aggregate), body, "aggregate") def delete(self, aggregate): - """Delete the specified aggregates.""" - self._delete('/os-aggregates/%s' % (base.getid(aggregate))) + """ + Delete the specified aggregate. + + :param aggregate: The aggregate to delete + :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 new file mode 100644 index 000000000..9e0675a93 --- /dev/null +++ b/novaclient/v2/assisted_volume_snapshots.py @@ -0,0 +1,55 @@ +# 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 oslo_serialization import jsonutils + +from novaclient import base + + +class Snapshot(base.Resource): + def __repr__(self): + return "" % self.id + + def delete(self): + """ + Delete this snapshot. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) + + +class AssistedSnapshotManager(base.Manager): + resource_class = Snapshot + + def create(self, volume_id, create_info): + body = {'snapshot': {'volume_id': volume_id, + 'create_info': create_info}} + return self._create('/os-assisted-volume-snapshots', body, 'snapshot') + + def delete(self, snapshot, delete_info): + """ + Delete a specified assisted volume snapshot. + + :param snapshot: an assisted volume snapshot to delete + :param delete_info: Information for snapshot deletion + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete("/os-assisted-volume-snapshots/%s?delete_info=%s" % + (base.getid(snapshot), + jsonutils.dumps(delete_info))) diff --git a/novaclient/v2/availability_zones.py b/novaclient/v2/availability_zones.py new file mode 100644 index 000000000..5a6639242 --- /dev/null +++ b/novaclient/v2/availability_zones.py @@ -0,0 +1,48 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Availability Zone interface +""" + +from novaclient import base + + +class AvailabilityZone(base.Resource): + """An availability zone object.""" + NAME_ATTR = 'display_name' + + def __repr__(self): + return "" % self.zoneName + + +class AvailabilityZoneManager(base.ManagerWithFind): + """Manage :class:`AvailabilityZone` resources.""" + resource_class = AvailabilityZone + return_parameter_name = "availabilityZoneInfo" + + def list(self, detailed=True): + """Get a list of all availability zones. + + :param detailed: If True, list availability zones with details. + :returns: list of :class:`AvailabilityZone` + """ + if detailed is True: + return self._list("/os-availability-zone/detail", + self.return_parameter_name) + else: + return self._list("/os-availability-zone", + self.return_parameter_name) diff --git a/novaclient/v2/client.py b/novaclient/v2/client.py new file mode 100644 index 000000000..e7b677b72 --- /dev/null +++ b/novaclient/v2/client.py @@ -0,0 +1,235 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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 logging + +from novaclient import client +from novaclient import exceptions +from novaclient.i18n import _ +from novaclient.v2 import agents +from novaclient.v2 import aggregates +from novaclient.v2 import assisted_volume_snapshots +from novaclient.v2 import availability_zones +from novaclient.v2 import flavor_access +from novaclient.v2 import flavors +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 migrations +from novaclient.v2 import networks +from novaclient.v2 import quota_classes +from novaclient.v2 import quotas +from novaclient.v2 import server_external_events +from novaclient.v2 import server_groups +from novaclient.v2 import server_migrations +from novaclient.v2 import servers +from novaclient.v2 import services +from novaclient.v2 import usage +from novaclient.v2 import versions +from novaclient.v2 import volumes + + +class Client(object): + """Top-level object to access the OpenStack Compute API. + + .. warning:: All scripts and projects should not initialize this class + directly. It should be done via `novaclient.client.Client` interface. + """ + + def __init__(self, + api_version=None, + auth=None, + auth_token=None, + auth_url=None, + cacert=None, + cert=None, + direct_use=True, + endpoint_override=None, + endpoint_type='publicURL', + extensions=None, + http_log_debug=False, + insecure=False, + logger=None, + os_cache=False, + password=None, + project_domain_id=None, + project_domain_name=None, + project_id=None, + project_name=None, + region_name=None, + service_name=None, + service_type='compute', + session=None, + timeout=None, + timings=False, + user_domain_id=None, + user_domain_name=None, + user_id=None, + username=None, + **kwargs): + """Initialization of Client object. + + :param api_version: Compute API version + :type api_version: novaclient.api_versions.APIVersion + :param str auth: Auth + :param str auth_token: Auth token + :param str auth_url: Auth URL + :param str cacert: ca-certificate + :param str cert: certificate + :param bool direct_use: Inner variable of novaclient. Do not use it + outside novaclient. It's restricted. + :param str endpoint_override: Bypass URL + :param str endpoint_type: Endpoint Type + :param str extensions: Extensions + :param bool http_log_debug: Enable debugging for HTTP connections + :param bool insecure: Allow insecure + :param logging.Logger logger: Logger instance to be used for all + logging stuff + :param str password: User password + :param bool os_cache: OS cache + :param str project_domain_id: ID of project domain + :param str project_domain_name: Name of project domain + :param str project_id: Project/Tenant ID + :param str project_name: Project/Tenant name + :param str region_name: Region Name + :param str service_name: Service Name + :param str service_type: Service Type + :param str session: Session + :param float timeout: API timeout, None or 0 disables + :param bool timings: Timings + :param str user_domain_id: ID of user domain + :param str user_domain_name: Name of user domain + :param str user_id: User ID + :param str username: Username + """ + if direct_use: + raise exceptions.Forbidden( + 403, _("'novaclient.v2.client.Client' is not designed to be " + "initialized directly. It is inner class of " + "novaclient. You should use " + "'novaclient.client.Client' instead. Related lp " + "bug-report: 1493576")) + + # NOTE(cyeoh): In the novaclient context (unlike Nova) the + # project_id is not the same as the tenant_id. Here project_id + # is a name (what the Nova API often refers to as a project or + # tenant name) and tenant_id is a UUID (what the Nova API + # often refers to as a project_id or tenant_id). + + self.project_id = project_id + self.project_name = project_name + self.user_id = user_id + self.flavors = flavors.FlavorManager(self) + self.flavor_access = flavor_access.FlavorAccessManager(self) + self.glance = images.GlanceManager(self) + self.limits = limits.LimitsManager(self) + self.servers = servers.ServerManager(self) + self.versions = versions.VersionManager(self) + + # extensions + self.agents = agents.AgentsManager(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.aggregates = aggregates.AggregateManager(self) + self.hypervisors = hypervisors.HypervisorManager(self) + self.hypervisor_stats = hypervisors.HypervisorStatsManager(self) + self.services = services.ServiceManager(self) + self.os_cache = os_cache + self.availability_zones = \ + availability_zones.AvailabilityZoneManager(self) + self.server_groups = server_groups.ServerGroupsManager(self) + self.server_migrations = \ + server_migrations.ServerMigrationsManager(self) + + # V2.0 extensions: + # NOTE(andreykurilin): tenant_networks extension is + # deprecated now, which is why it is not initialized by default. + self.assisted_volume_snapshots = \ + assisted_volume_snapshots.AssistedSnapshotManager(self) + self.instance_action = instance_action.InstanceActionManager(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) + + self.logger = logger or logging.getLogger(__name__) + + # Add in any extensions... + if extensions: + for extension in extensions: + if extension.manager_class: + setattr(self, extension.name, + extension.manager_class(self)) + + self.client = client._construct_http_client( + api_version=api_version, + auth=auth, + auth_token=auth_token, + auth_url=auth_url, + cacert=cacert, + cert=cert, + endpoint_override=endpoint_override, + endpoint_type=endpoint_type, + http_log_debug=http_log_debug, + insecure=insecure, + logger=self.logger, + os_cache=self.os_cache, + password=password, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name, + project_id=project_id, + project_name=project_name, + region_name=region_name, + service_name=service_name, + service_type=service_type, + session=session, + timeout=timeout, + timings=timings, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name, + user_id=user_id, + username=username, + **kwargs) + + @property + def api_version(self): + return self.client.api_version + + @api_version.setter + def api_version(self, value): + self.client.api_version = value + + def __enter__(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 get_timings(self): + return self.client.get_timings() + + def reset_timings(self): + self.client.reset_timings() diff --git a/novaclient/v1_1/flavor_access.py b/novaclient/v2/flavor_access.py similarity index 58% rename from novaclient/v1_1/flavor_access.py rename to novaclient/v2/flavor_access.py index ef3992afa..ecb0cc57b 100644 --- a/novaclient/v1_1/flavor_access.py +++ b/novaclient/v2/flavor_access.py @@ -1,4 +1,4 @@ -# Copyright 2012 OpenStack LLC. +# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,36 +16,29 @@ """Flavor access interface.""" from novaclient import base +from novaclient.i18n import _ class FlavorAccess(base.Resource): def __repr__(self): - return "" % self.name + return ("" % + (self.flavor_id, self.tenant_id)) class FlavorAccessManager(base.ManagerWithFind): - """ - Manage :class:`FlavorAccess` resources. - """ + """Manage :class:`FlavorAccess` resources.""" resource_class = FlavorAccess def list(self, **kwargs): - if kwargs.get('flavor', None): - return self._list_by_flavor(kwargs['flavor']) - elif kwargs.get('tenant', None): - return self._list_by_tenant(kwargs['tenant']) - else: - raise NotImplementedError('Unknown list options.') - - def _list_by_flavor(self, flavor): - return self._list('/flavors/%s/os-flavor-access' % base.getid(flavor), - 'flavor_access') - - def _list_by_tenant(self, tenant): - """Print flavor list shared with the given tenant""" - # TODO(uni): need to figure out a proper URI for list_by_tenant - # since current API already provided current tenant_id information - raise NotImplementedError('Sorry, query by tenant not supported.') + # NOTE(mriedem): This looks a bit weird, you would normally expect this + # method to just take a flavor arg, but it used to erroneously accept + # flavor or tenant, but never actually implemented support for listing + # flavor access by tenant. We leave the interface unchanged though for + # backward compatibility. + if kwargs.get('flavor'): + return self._list('/flavors/%s/os-flavor-access' % + base.getid(kwargs['flavor']), 'flavor_access') + raise NotImplementedError(_('Unknown list options.')) def add_tenant_access(self, flavor, tenant): """Add a tenant to the given flavor access list.""" @@ -62,7 +55,9 @@ def _action(self, action, flavor, info, **kwargs): body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/flavors/%s/action' % base.getid(flavor) - _resp, body = self.api.client.post(url, body=body) + resp, body = self.api.client.post(url, body=body) + + items = [self.resource_class(self, res) + for res in body['flavor_access']] - return [self.resource_class(self, res) - for res in body['flavor_access']] + return base.ListWithMeta(items, resp) diff --git a/novaclient/v2/flavors.py b/novaclient/v2/flavors.py new file mode 100644 index 000000000..803e226a0 --- /dev/null +++ b/novaclient/v2/flavors.py @@ -0,0 +1,264 @@ +# Copyright 2010 Jacob Kaplan-Moss +# +# 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. + +""" +Flavor interface. +""" + +from oslo_utils import strutils + +from novaclient import api_versions +from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ +from novaclient import utils + + +class Flavor(base.Resource): + """A flavor is an available hardware configuration for a server.""" + HUMAN_ID = True + + def __repr__(self): + return "" % self.name + + @property + def ephemeral(self): + """Provide a user-friendly accessor to OS-FLV-EXT-DATA:ephemeral.""" + return self._info.get("OS-FLV-EXT-DATA:ephemeral", 'N/A') + + @property + def is_public(self): + """Provide a user-friendly accessor to os-flavor-access:is_public.""" + return self._info.get("os-flavor-access:is_public", 'N/A') + + def get_keys(self): + """ + Get extra specs from a flavor. + + :returns: An instance of novaclient.base.DictWithMeta + """ + resp, body = self.manager.api.client.get( + "/flavors/%s/os-extra_specs" % base.getid(self)) + return self.manager.convert_into_with_meta(body["extra_specs"], resp) + + def set_keys(self, metadata): + """Set extra specs on a flavor. + + :param metadata: A dict of key/value pairs to be set + """ + utils.validate_flavor_metadata_keys(metadata.keys()) + + body = {'extra_specs': metadata} + return self.manager._create( + "/flavors/%s/os-extra_specs" % base.getid(self), body, + "extra_specs", return_raw=True) + + def unset_keys(self, keys): + """Unset extra specs on a flavor. + + :param keys: A list of keys to be unset + :returns: An instance of novaclient.base.TupleWithMeta + """ + result = base.TupleWithMeta((), None) + for k in keys: + ret = self.manager._delete( + "/flavors/%s/os-extra_specs/%s" % (base.getid(self), k)) + result.append_request_ids(ret.request_ids) + + return result + + def delete(self): + """ + Delete this flavor. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + 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.""" + resource_class = Flavor + is_alphanum_id_allowed = True + + def list(self, detailed=True, is_public=True, marker=None, min_disk=None, + min_ram=None, limit=None, sort_key=None, sort_dir=None): + """Get a list of all flavors. + + :param detailed: Whether flavor needs to be return with details + (optional). + :param is_public: Filter flavors with provided access type (optional). + None means give all flavors and only admin has query + access to all flavor types. + :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 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`. + """ + qparams = {} + # is_public is ternary - None means give all flavors. + # By default Nova assumes True and gives admins public flavors + # and flavors from their own projects only. + if marker: + qparams['marker'] = str(marker) + if min_disk: + qparams['minDisk'] = int(min_disk) + if min_ram: + qparams['minRam'] = int(min_ram) + if limit: + qparams['limit'] = int(limit) + if sort_key: + qparams['sort_key'] = str(sort_key) + if sort_dir: + qparams['sort_dir'] = str(sort_dir) + if not is_public: + qparams['is_public'] = is_public + detail = "" + if detailed: + detail = "/detail" + + return self._list("/flavors%s" % detail, "flavors", filters=qparams) + + def get(self, flavor): + """Get a specific flavor. + + :param flavor: The ID of the :class:`Flavor` to get. + :returns: :class:`Flavor` + """ + return self._get("/flavors/%s" % base.getid(flavor), "flavor") + + def delete(self, flavor): + """Delete a specific flavor. + + :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)) + + def _build_body(self, name, ram, vcpus, disk, id, swap, + ephemeral, rxtx_factor, is_public): + return { + "flavor": { + "name": name, + "ram": ram, + "vcpus": vcpus, + "disk": disk, + "id": id, + "swap": swap, + "OS-FLV-EXT-DATA:ephemeral": ephemeral, + "rxtx_factor": rxtx_factor, + "os-flavor-access:is_public": is_public, + } + } + + def create(self, name, ram, vcpus, disk, flavorid="auto", + 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 MiB for the flavor + :param vcpus: Number of VCPUs for the flavor + :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 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` + """ + + try: + ram = int(ram) + except (TypeError, ValueError): + raise exceptions.CommandError(_("Ram must be an integer.")) + try: + vcpus = int(vcpus) + except (TypeError, ValueError): + raise exceptions.CommandError(_("VCPUs must be an integer.")) + try: + disk = int(disk) + except (TypeError, ValueError): + raise exceptions.CommandError(_("Disk must be an integer.")) + + if flavorid == "auto": + flavorid = None + + try: + swap = int(swap) + except (TypeError, ValueError): + raise exceptions.CommandError(_("Swap must be an integer.")) + try: + ephemeral = int(ephemeral) + except (TypeError, ValueError): + raise exceptions.CommandError(_("Ephemeral must be an integer.")) + try: + rxtx_factor = float(rxtx_factor) + except (TypeError, ValueError): + raise exceptions.CommandError(_("rxtx_factor must be a float.")) + + try: + is_public = strutils.bool_from_string(is_public, True) + 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/hypervisors.py b/novaclient/v2/hypervisors.py new file mode 100644 index 000000000..f0cc5863c --- /dev/null +++ b/novaclient/v2/hypervisors.py @@ -0,0 +1,176 @@ +# 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. + +""" +Hypervisors interface +""" + +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 + + +class Hypervisor(base.Resource): + NAME_ATTR = 'hypervisor_hostname' + + def __repr__(self): + return "" % self.id + + +class HypervisorManager(base.ManagerWithFind): + resource_class = Hypervisor + is_alphanum_id_allowed = True + + def _list_base(self, detailed=True, marker=None, limit=None): + path = '/os-hypervisors' + if detailed: + path += '/detail' + params = {} + if limit is not None: + params['limit'] = int(limit) + if marker is not None: + params['marker'] = str(marker) + path += utils.prepare_query_string(params) + return self._list(path, 'hypervisors') + + @api_versions.wraps("2.0", "2.32") + def list(self, detailed=True): + """ + Get a list of hypervisors. + + :param detailed: Include a detailed response. + """ + return self._list_base(detailed=detailed) + + @api_versions.wraps("2.33") + def list(self, detailed=True, marker=None, limit=None): + """ + Get a list of hypervisors. + + :param detailed: Include a detailed response. + :param marker: Begin returning hypervisors that appear later in the + hypervisors list than that represented by this + hypervisor ID. Starting with microversion 2.53 the + 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, detailed=False): + """ + Get a list of matching hypervisors. + + :param hypervisor_match: The hypervisor host name or a portion of it. + 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%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)) + return self._list(url, 'hypervisors') + + def get(self, hypervisor): + """ + Get a specific 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" % base.getid(hypervisor), + "hypervisor") + + def uptime(self, hypervisor): + """ + Get the uptime for a specific hypervisor. + + :param hypervisor: Either a Hypervisor object or an ID. Starting with + microversion 2.53 the ID must be a UUID value. + """ + # 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): + """ + Get hypervisor statistics over all compute nodes. + + Kept for backwards compatibility, new code should call + hypervisor_stats.statistics() instead of hypervisors.statistics() + """ + return self.api.hypervisor_stats.statistics() + + +class HypervisorStats(base.Resource): + def __repr__(self): + return ("" % + (self.count, "s" if self.count != 1 else "")) + + +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 new file mode 100644 index 000000000..0aced5c7a --- /dev/null +++ b/novaclient/v2/images.py @@ -0,0 +1,113 @@ +# Copyright 2010 Jacob Kaplan-Moss +# +# 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 import base +from novaclient import exceptions +from novaclient.i18n import _ + + +class Image(base.Resource): + HUMAN_ID = True + + def __repr__(self): + return "" % self.name + + +class GlanceManager(base.Manager): + """Use glance directly from service catalog. + + This is used to do name to id lookups for images and listing images for + the --image-with option to the 'boot' command. Do not use it + for anything else besides that. You have been warned. + + """ + + resource_class = Image + + def find_image(self, name_or_id): + """Find an image by name or id (user provided input).""" + + with self.alternate_service_type( + 'image', allowed_types=('image',)): + # glance catalog entries are the unversioned endpoint, so + # we need to jam a version bit in here. + if uuidutils.is_uuid_like(name_or_id): + # if it's a uuid, then just go collect the info and be + # done with it. + return self._get('/v2/images/%s' % name_or_id, None) + else: + matches = self._list('/v2/images?name=%s' % name_or_id, + "images") + num_matches = len(matches) + if num_matches == 0: + msg = "No %s matching %s." % ( + self.resource_class.__name__, name_or_id) + raise exceptions.NotFound(404, msg) + elif num_matches > 1: + msg = (_("Multiple %(class)s matches found for " + "'%(name)s', use an ID to be more specific.") % + {'class': self.resource_class.__name__.lower(), + 'name': name_or_id}) + raise exceptions.NoUniqueMatch(msg) + else: + 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. + + :rtype: list of :class:`Image` + """ + with self.alternate_service_type('image', allowed_types=('image',)): + return self._list('/v2/images', 'images') diff --git a/novaclient/v2/instance_action.py b/novaclient/v2/instance_action.py new file mode 100644 index 000000000..d84c50f90 --- /dev/null +++ b/novaclient/v2/instance_action.py @@ -0,0 +1,104 @@ +# 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 base + + +class InstanceAction(base.Resource): + pass + + +class InstanceActionManager(base.ManagerWithFind): + resource_class = InstanceAction + + def get(self, server, request_id): + """ + Get details of an action performed on an instance. + + :param request_id: The request_id of the action to get. + """ + 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 new file mode 100644 index 000000000..5d12f8cd4 --- /dev/null +++ b/novaclient/v2/keypairs.py @@ -0,0 +1,217 @@ +# 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. + +""" +Keypair interface +""" + +from novaclient import api_versions +from novaclient import base + + +class Keypair(base.Resource): + """ + A keypair is a ssh key that can be injected into a server on launch. + """ + + def __repr__(self): + return "" % self.id + + def _add_details(self, info): + dico = 'keypair' in info and \ + info['keypair'] or info + for (k, v) in dico.items(): + # NOTE(rpodolyaka): keypair name allows us to uniquely identify + # a specific keypair, while its id attribute + # is nothing more than an implementation + # detail. We can safely omit the id attribute + # here to ensure setattr() won't raise + # AttributeError trying to set read-only + # property id + if k != 'id': + setattr(self, k, v) + + @property + def id(self): + return self.name + + def delete(self): + """ + Delete this keypair. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) + + +class KeypairManager(base.ManagerWithFind): + resource_class = Keypair + keypair_prefix = "os-keypairs" + is_alphanum_id_allowed = True + + @api_versions.wraps("2.0", "2.9") + def get(self, keypair): + """ + Get a keypair. + + :param keypair: The ID of the keypair to get. + :rtype: :class:`Keypair` + """ + return self._get("/%s/%s" % (self.keypair_prefix, base.getid(keypair)), + "keypair") + + @api_versions.wraps("2.10") + def get(self, keypair, user_id=None): + """ + Get a keypair. + + :param keypair: The ID of the keypair to get. + :param user_id: Id of key-pair owner (Admin only). + :rtype: :class:`Keypair` + """ + query_string = "?user_id=%s" % user_id if user_id else "" + url = "/%s/%s%s" % (self.keypair_prefix, base.getid(keypair), + query_string) + return self._get(url, "keypair") + + @api_versions.wraps("2.0", "2.1") + def create(self, name, public_key=None): + """ + Create a keypair + + :param name: name for the keypair to create + :param public_key: existing public key to import + """ + body = {'keypair': {'name': name}} + if public_key: + body['keypair']['public_key'] = public_key + return self._create('/%s' % self.keypair_prefix, body, 'keypair') + + @api_versions.wraps("2.2", "2.9") + def create(self, name, public_key=None, key_type="ssh"): + """ + 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 + """ + body = {'keypair': {'name': name, + 'type': key_type}} + if public_key: + body['keypair']['public_key'] = public_key + return self._create('/%s' % self.keypair_prefix, body, 'keypair') + + @api_versions.wraps("2.10", "2.91") + def create(self, name, public_key=None, 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}} + if public_key: + body['keypair']['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.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): + """ + Delete a keypair + + :param key: The :class:`Keypair` (or its ID) to delete. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete('/%s/%s' % (self.keypair_prefix, base.getid(key))) + + @api_versions.wraps("2.10") + def delete(self, key, user_id=None): + """ + Delete a keypair + + :param key: The :class:`Keypair` (or its ID) to delete. + :param user_id: Id of key-pair owner (Admin only). + :returns: An instance of novaclient.base.TupleWithMeta + """ + query_string = "?user_id=%s" % user_id if user_id else "" + url = '/%s/%s%s' % (self.keypair_prefix, base.getid(key), query_string) + return self._delete(url) + + @api_versions.wraps("2.0", "2.9") + def list(self): + """ + Get a list of keypairs. + """ + return self._list('/%s' % self.keypair_prefix, 'keypairs') + + @api_versions.wraps("2.10", "2.34") + def list(self, user_id=None): + """ + Get a list of keypairs. + + :param user_id: Id of key-pairs owner (Admin only). + """ + 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): + """ + Get a list of keypairs. + + :param user_id: Id of key-pairs owner (Admin only). + :param marker: Begin returning keypairs that appear later in the + 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: + params['user_id'] = user_id + if limit: + params['limit'] = int(limit) + if marker: + params['marker'] = str(marker) + return self._list('/%s' % self.keypair_prefix, 'keypairs', + filters=params) diff --git a/novaclient/v1_1/limits.py b/novaclient/v2/limits.py similarity index 62% rename from novaclient/v1_1/limits.py rename to novaclient/v2/limits.py index 75bf54161..d90d3e9a4 100644 --- a/novaclient/v1_1/limits.py +++ b/novaclient/v2/limits.py @@ -1,10 +1,22 @@ -# Copyright 2011 OpenStack LLC. +# Copyright 2011 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 import base class Limits(base.Resource): - """A collection of RateLimit and AbsoluteLimit objects""" + """A collection of RateLimit and AbsoluteLimit objects.""" def __repr__(self): return "" @@ -26,7 +38,7 @@ def rate(self): class RateLimit(object): - """Data model that represents a flattened view of a single rate limit""" + """Data model that represents a flattened view of a single rate limit.""" def __init__(self, verb, uri, regex, value, remain, unit, next_available): @@ -48,11 +60,11 @@ def __eq__(self, other): and self.next_available == other.next_available def __repr__(self): - return "" % (self.method, self.uri) + return "" % (self.verb, self.uri) class AbsoluteLimit(object): - """Data model that represents a single absolute limit""" + """Data model that represents a single absolute limit.""" def __init__(self, name, value): self.name = name @@ -66,14 +78,19 @@ def __repr__(self): class LimitsManager(base.Manager): - """Manager object used to interact with limits resource""" + """Manager object used to interact with limits resource.""" resource_class = Limits - def get(self): + def get(self, reserved=False, tenant_id=None): """ Get a specific extension. :rtype: :class:`Limits` """ - return self._get("/limits", "limits") + opts = {} + if reserved: + opts['reserved'] = 1 + if tenant_id: + opts['tenant_id'] = tenant_id + return self._get("/limits", "limits", filters=opts) diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py new file mode 100644 index 000000000..1fe764b5e --- /dev/null +++ b/novaclient/v2/migrations.py @@ -0,0 +1,177 @@ +# 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. + +""" +migration interface +""" + +from novaclient import api_versions +from novaclient import base + + +class Migration(base.Resource): + def __repr__(self): + return "" % self.id + + +class MigrationManager(base.ManagerWithFind): + resource_class = Migration + + 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 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 + + return self._list("/os-migrations", "migrations", filters=opts) + + @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) + + @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/networks.py b/novaclient/v2/networks.py new file mode 100644 index 000000000..8c1b923d5 --- /dev/null +++ b/novaclient/v2/networks.py @@ -0,0 +1,65 @@ +# 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. + +""" +Network interface. +""" +from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ + + +class Network(base.Resource): + """ + A network as defined in the Networking (Neutron) API. + """ + HUMAN_ID = True + NAME_ATTR = "name" + + def __repr__(self): + return "" % self.name + + +class NeutronManager(base.Manager): + """A manager for name -> id lookups for neutron networks. + + This uses neutron directly from service catalog. Do not use it + for anything else besides that. You have been warned. + """ + + resource_class = Network + + def find_network(self, name): + """Find a network by name (user provided input).""" + + with self.alternate_service_type( + 'network', allowed_types=('network',)): + + matches = self._list('/v2.0/networks?name=%s' % name, 'networks') + + num_matches = len(matches) + if num_matches == 0: + msg = "No %s matching %s." % ( + self.resource_class.__name__, name) + raise exceptions.NotFound(404, msg) + elif num_matches > 1: + msg = (_("Multiple %(class)s matches found for " + "'%(name)s', use an ID to be more specific.") % + {'class': self.resource_class.__name__.lower(), + 'name': name}) + raise exceptions.NoUniqueMatch(msg) + else: + matches[0].append_request_ids(matches.request_ids) + return matches[0] diff --git a/novaclient/v2/quota_classes.py b/novaclient/v2/quota_classes.py new file mode 100644 index 000000000..917cc9c43 --- /dev/null +++ b/novaclient/v2/quota_classes.py @@ -0,0 +1,110 @@ +# Copyright 2012 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from novaclient import api_versions +from novaclient import base + + +class QuotaClassSet(base.Resource): + + def update(self, *args, **kwargs): + return self.manager.update(self.id, *args, **kwargs) + + +class QuotaClassSetManager(base.Manager): + resource_class = QuotaClassSet + + def get(self, class_name): + return self._get("/os-quota-class-sets/%s" % (class_name), + "quota_class_set") + + def _update_body(self, **kwargs): + return {'quota_class_set': kwargs} + + # NOTE(mriedem): Before 2.50 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.49") + def update(self, class_name, **kwargs): + body = self._update_body(**kwargs) + + for key in list(body['quota_class_set']): + if body['quota_class_set'][key] is None: + body['quota_class_set'].pop(key) + + return self._update('/os-quota-class-sets/%s' % (class_name), + body, + 'quota_class_set') + + # 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", "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, + 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 injected_files is not None: + resources['injected_files'] = injected_files + if injected_file_content_bytes is not None: + resources['injected_file_content_bytes'] = ( + injected_file_content_bytes) + if injected_file_path_bytes is not None: + resources['injected_file_path_bytes'] = injected_file_path_bytes + 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') + + # 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 new file mode 100644 index 000000000..82249f25e --- /dev/null +++ b/novaclient/v2/quotas.py @@ -0,0 +1,111 @@ +# 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 api_versions +from novaclient import base + + +class QuotaSet(base.Resource): + + def update(self, *args, **kwargs): + return self.manager.update(self.id, *args, **kwargs) + + +class QuotaSetManager(base.Manager): + resource_class = QuotaSet + + def get(self, tenant_id, user_id=None, detail=False): + url = '/os-quota-sets/%(tenant_id)s' + if detail: + url += '/detail' + + if user_id: + params = {'tenant_id': tenant_id, 'user_id': user_id} + url += '?user_id=%(user_id)s' + else: + params = {'tenant_id': tenant_id} + + 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) + body = {'quota_set': kwargs} + + for key in list(body['quota_set']): + if body['quota_set'][key] is None: + body['quota_set'].pop(key) + + 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') + + # 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') + + def delete(self, tenant_id, user_id=None): + """ + Delete quota for a tenant or for a user. + + :param tenant_id: A tenant for which quota is to be deleted + :param user_id: A user for which quota is to be deleted + :returns: An instance of novaclient.base.TupleWithMeta + """ + 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._delete(url) diff --git a/novaclient/v2/server_external_events.py b/novaclient/v2/server_external_events.py new file mode 100644 index 000000000..7c2506a2c --- /dev/null +++ b/novaclient/v2/server_external_events.py @@ -0,0 +1,39 @@ +# 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 import base + + +class Event(base.Resource): + def __repr__(self): + return "" % self.name + + +class ServerExternalEventManager(base.Manager): + resource_class = Event + + def create(self, events): + """Create one or more server events. + + :param:events: A list of dictionaries containing 'server_uuid', 'name', + 'status', and 'tag' (which may be absent) + """ + + body = {'events': events} + return self._create('/os-server-external-events', body, 'events', + return_raw=True) diff --git a/novaclient/v2/server_groups.py b/novaclient/v2/server_groups.py new file mode 100644 index 000000000..bc66b01a2 --- /dev/null +++ b/novaclient/v2/server_groups.py @@ -0,0 +1,127 @@ +# Copyright (c) 2014 VMware, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Server group interface. +""" + +from novaclient import api_versions +from novaclient import base +from novaclient import exceptions +from novaclient.i18n import _ + + +class ServerGroup(base.Resource): + """ + A server group. + """ + + def __repr__(self): + return '' % self.id + + def delete(self): + """ + Delete this server group. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self.id) + + +class ServerGroupsManager(base.ManagerWithFind): + """ + Manage :class:`ServerGroup` resources. + """ + resource_class = ServerGroup + + def list(self, all_projects=False, limit=None, offset=None): + """Get a list of all server groups. + + :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) + :returns: list of :class:`ServerGroup`. + """ + qparams = {} + if all_projects: + qparams['all_projects'] = bool(all_projects) + if limit: + qparams['limit'] = int(limit) + if offset: + qparams['offset'] = int(offset) + return self._list('/os-server-groups', 'server_groups', + filters=qparams) + + def get(self, id): + """Get a specific server group. + + :param id: The ID of the :class:`ServerGroup` to get. + :rtype: :class:`ServerGroup` + """ + return self._get('/os-server-groups/%s' % id, + 'server_group') + + def delete(self, id): + """Delete a specific server group. + + :param id: The ID of the :class:`ServerGroup` to delete. + :returns: An instance of novaclient.base.TupleWithMeta + """ + 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. + + :param name: The name of the server group. + :param policies: Policy name or a list of exactly one policy name to + associate with the server group. + :rtype: list of :class:`ServerGroup` + """ + policies = policies if isinstance(policies, list) else [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/server_migrations.py b/novaclient/v2/server_migrations.py new file mode 100644 index 000000000..a85e85f27 --- /dev/null +++ b/novaclient/v2/server_migrations.py @@ -0,0 +1,80 @@ +# Copyright 2016 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 api_versions +from novaclient import base + + +class ServerMigration(base.Resource): + def __repr__(self): + return "" + + +class ServerMigrationsManager(base.ManagerWithFind): + resource_class = ServerMigration + + @api_versions.wraps("2.22") + def live_migrate_force_complete(self, server, migration): + """ + Force on-going live migration to complete + + :param server: The :class:`Server` (or its ID) + :param migration: Migration id that will be forced to complete + :returns: An instance of novaclient.base.TupleWithMeta + """ + body = {'force_complete': None} + resp, body = self.api.client.post( + '/servers/%s/migrations/%s/action' % (base.getid(server), + base.getid(migration)), + body=body) + return self.convert_into_with_meta(body, resp) + + @api_versions.wraps("2.23") + def get(self, server, migration): + """ + Get a migration of a specified server + + :param server: The :class:`Server` (or its ID) + :param migration: Migration id that will be gotten. + :returns: An instance of + novaclient.v2.server_migrations.ServerMigration + """ + return self._get('/servers/%s/migrations/%s' % + (base.getid(server), base.getid(migration)), + 'migration') + + @api_versions.wraps("2.23") + def list(self, server): + """ + Get a migrations list of a specified server + + :param server: The :class:`Server` (or its ID) + :returns: An instance of novaclient.base.ListWithMeta + """ + return self._list( + '/servers/%s/migrations' % (base.getid(server)), "migrations") + + @api_versions.wraps("2.24") + def live_migration_abort(self, server, migration): + """ + Cancel an ongoing live migration + + :param server: The :class:`Server` (or its ID) + :param migration: Migration id that will be cancelled + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete( + '/servers/%s/migrations/%s' % (base.getid(server), + base.getid(migration))) diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py new file mode 100644 index 000000000..81c702dfb --- /dev/null +++ b/novaclient/v2/servers.py @@ -0,0 +1,2445 @@ +# 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. + +""" +Server interface. +""" + +import base64 +import collections +from urllib import parse + +from novaclient import api_versions +from novaclient import base +from novaclient import crypto +from novaclient import exceptions +from novaclient.i18n import _ + +_SENTINEL = object() + +REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD' + +CONSOLE_TYPE_ACTION_MAPPING = { + 'novnc': 'os-getVNCConsole', + 'xvpvnc': 'os-getVNCConsole', + 'spice-html5': 'os-getSPICEConsole', + 'rdp-html5': 'os-getRDPConsole', + 'serial': 'os-getSerialConsole' +} + +CONSOLE_TYPE_PROTOCOL_MAPPING = { + 'novnc': 'vnc', + 'xvpvnc': 'vnc', + 'spice-html5': 'spice', + 'rdp-html5': 'rdp', + 'serial': 'serial', + 'webmks': 'mks' +} + + +class Server(base.Resource): + HUMAN_ID = True + + def __repr__(self): + return '' % getattr(self, 'name', 'unknown-name') + + def delete(self): + """ + Delete (i.e. shut down and delete the image) this server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.delete(self) + + @api_versions.wraps("2.0", "2.18") + def update(self, name=None): + """ + 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", "2.89") + def update(self, name=None, description=None): + """ + 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): + """ + Get text console log output from Server. + + :param length: The number of lines you would like to retrieve (as int) + """ + return self.manager.get_console_output(self, length) + + def get_vnc_console(self, console_type): + """ + Get vnc console for a Server. + + :param console_type: Type of console ('novnc' or 'xvpvnc') + """ + return self.manager.get_vnc_console(self, console_type) + + def get_spice_console(self, console_type): + """ + Get spice console for a Server. + + :param console_type: Type of console ('spice-html5') + """ + return self.manager.get_spice_console(self, console_type) + + def get_rdp_console(self, console_type): + """ + Get rdp console for a Server. + + :param console_type: Type of console ('rdp-html5') + """ + return self.manager.get_rdp_console(self, console_type) + + def get_serial_console(self, console_type): + """ + Get serial console for a Server. + + :param console_type: Type of console ('serial') + """ + return self.manager.get_serial_console(self, console_type) + + def get_mks_console(self): + """ + Get mks console for a Server. + + """ + return self.manager.get_mks_console(self) + + def get_console_url(self, console_type): + """ + Retrieve a console of a particular protocol and console_type + + :param console_type: Type of console + """ + + return self.manager.get_console_url(self, console_type) + + def get_password(self, private_key=None): + """ + Get password for a Server. + + Returns the clear password of an instance if private_key is + provided, returns the ciphered password otherwise. + + :param private_key: Path to private key file for decryption + (optional) + """ + return self.manager.get_password(self, private_key) + + def clear_password(self): + """ + Get password for a Server. + + """ + return self.manager.clear_password(self) + + def stop(self): + """ + Stop -- Stop the running server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.stop(self) + + def force_delete(self): + """ + Force delete -- Force delete a server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.force_delete(self) + + def restore(self): + """ + Restore -- Restore a server in 'soft-deleted' state. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.restore(self) + + def start(self): + """ + Start -- Start the paused server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.start(self) + + def pause(self): + """ + Pause -- Pause the running server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.pause(self) + + def unpause(self): + """ + Unpause -- Unpause the paused server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.unpause(self) + + @api_versions.wraps("2.0", "2.72") + def lock(self): + """ + Lock -- Lock the instance from certain operations. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + 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. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.unlock(self) + + def suspend(self): + """ + Suspend -- Suspend the running server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.suspend(self) + + def resume(self): + """ + Resume -- Resume the suspended server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.resume(self) + + def rescue(self, password=None, image=None): + """ + Rescue -- Rescue the problematic server. + + :param password: The admin password to be set in the rescue instance. + :param image: The :class:`Image` to rescue with. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.rescue(self, password, image) + + def unrescue(self): + """ + Unrescue -- Unrescue the rescued server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.unrescue(self) + + def shelve(self): + """ + Shelve -- Shelve the server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.shelve(self) + + def shelve_offload(self): + """ + Shelve_offload -- Remove a shelved server from the compute node. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.shelve_offload(self) + + @api_versions.wraps("2.0", "2.76") + def unshelve(self): + """ + Unshelve -- Unshelve the server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + 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. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.migrate(self) + + @api_versions.wraps("2.56") + def migrate(self, host=None): + """ + Migrate a server to a new host. + + :param host: (Optional) The target host. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.migrate(self, host=host) + + def change_password(self, password): + """ + Update the admin password for a server. + + :param password: string to set as the admin password on the server + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.change_password(self, password) + + def reboot(self, reboot_type=REBOOT_SOFT): + """ + Reboot the server. + + :param reboot_type: either :data:`REBOOT_SOFT` for a software-level + reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.reboot(self, reboot_type) + + # 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 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, + 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 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 by default. + """ + return self.manager.resize(self, flavor, disk_config=disk_config) + + def create_image(self, image_name, metadata=None): + """ + Create an image based on this server. + + :param image_name: The name to assign the newly create image. + :param metadata: Metadata to assign to the image. + """ + return self.manager.create_image(self, image_name, metadata) + + def backup(self, backup_name, backup_type, rotation): + """ + Backup a server instance. + + :param backup_name: Name of the backup image + :param backup_type: The backup type, like 'daily' or 'weekly' + :param rotation: Int parameter representing how many backups to + keep around. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.backup(self, backup_name, backup_type, rotation) + + def confirm_resize(self): + """ + Confirm that the resize worked, thus removing the original server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.confirm_resize(self) + + def revert_resize(self): + """ + Revert a previous resize, switching back to the old server. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.revert_resize(self) + + @property + 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 = collections.OrderedDict() + try: + # 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 AttributeError: + return {} + + @api_versions.wraps("2.0", "2.24") + def live_migrate(self, host=None, + block_migration=False, + disk_over_commit=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 False and None will be mapped to False + :param disk_over_commit: if True, allow disk over commit, the default + value is None which is mapped to False + :returns: An instance of novaclient.base.TupleWithMeta + """ + if block_migration is None: + block_migration = False + if disk_over_commit is None: + disk_over_commit = False + return self.manager.live_migrate(self, host, + block_migration, + disk_over_commit) + + @api_versions.wraps("2.25", "2.29") + 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) + + @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. + + :param host: destination host name. + :param block_migration: if True, do block_migration, the default + value is None which is mapped to 'auto'. + :param force: force to bypass the scheduler if host is provided. + :returns: An instance of novaclient.base.TupleWithMeta + """ + if block_migration is 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. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.reset_state(self, state) + + def reset_network(self): + """ + Reset network of an instance. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.reset_network(self) + + def add_security_group(self, security_group): + """ + Add a security group to an instance. + + :param security_group: The name of security group to add + :returns: An instance of novaclient.base.DictWithMeta + """ + return self.manager.add_security_group(self, security_group) + + def remove_security_group(self, security_group): + """ + Remove a security group from an instance. + + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.remove_security_group(self, security_group) + + def list_security_group(self): + """ + List security group(s) of an instance. + """ + return self.manager.list_security_group(self) + + @api_versions.wraps("2.0", "2.13") + def evacuate(self, host=None, on_shared_storage=True, password=None): + """ + Evacuate an instance from failed host to specified host. + + :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 admin password on the evacuated + server. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self.manager.evacuate(self, host, on_shared_storage, password) + + @api_versions.wraps("2.14", "2.28") + 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) + + @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. + + :param host: Name of the target host + :param password: string to set as admin 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.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. + """ + return self.manager.interface_list(self) + + @api_versions.wraps("2.0", "2.48") + def interface_attach(self, port_id, net_id, fixed_ip): + """ + Attach a network interface to an instance. + """ + return self.manager.interface_attach(self, port_id, net_id, fixed_ip) + + @api_versions.wraps("2.49") + def interface_attach(self, port_id, net_id, fixed_ip, tag=None): + """ + Attach a network interface to an instance with an optional tag. + """ + return self.manager.interface_attach(self, port_id, net_id, fixed_ip, + tag) + + def interface_detach(self, port_id): + """ + Detach a network interface from an instance. + """ + return self.manager.interface_detach(self, port_id) + + def trigger_crash_dump(self): + """Trigger crash dump in an instance""" + return self.manager.trigger_crash_dump(self) + + @api_versions.wraps('2.26') + def tag_list(self): + """ + Get list of tags from an instance. + """ + return self.manager.tag_list(self) + + @api_versions.wraps('2.26') + def delete_tag(self, tag): + """ + Remove single tag from an instance. + """ + return self.manager.delete_tag(self, tag) + + @api_versions.wraps('2.26') + def delete_all_tags(self): + """ + Remove all tags from an instance. + """ + return self.manager.delete_all_tags(self) + + @api_versions.wraps('2.26') + def set_tags(self, tags): + """ + Set list of tags to an instance. + """ + return self.manager.set_tags(self, tags) + + @api_versions.wraps('2.26') + def add_tag(self, tag): + """ + Add single tag to an instance. + """ + return self.manager.add_tag(self, tag) + + +class NetworkInterface(base.Resource): + @property + def id(self): + return self.port_id + + def __repr__(self): + return '' % self.id + + +class SecurityGroup(base.Resource): + def __str__(self): + return str(self.id) + + +class ServerManager(base.BootingManagerWithFind): + resource_class = Server + + @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. + """ + body = {"server": { + "name": name, + "imageRef": str(base.getid(image)) if image else '', + "flavorRef": str(base.getid(flavor)), + }} + if userdata: + body["server"]["user_data"] = self.transform_userdata(userdata) + if meta: + body["server"]["metadata"] = meta + if reservation_id: + body["server"]["return_reservation_id"] = reservation_id + return_raw = True + if key_name: + body["server"]["key_name"] = key_name + if scheduler_hints: + body['os:scheduler_hints'] = scheduler_hints + if config_drive: + body["server"]["config_drive"] = config_drive + if admin_pass: + body["server"]["adminPass"] = admin_pass + if not min_count: + min_count = 1 + if not max_count: + max_count = min_count + body["server"]["min_count"] = min_count + body["server"]["max_count"] = max_count + + if security_groups: + body["server"]["security_groups"] = [{'name': sg} + for sg in security_groups] + + # Files are a slight bit tricky. They're passed in a "personality" + # list to the POST. Each item is a dict giving a file name and the + # base64-encoded contents of the file. We want to allow passing + # either an open file *or* some contents as files here. + if files: + personality = body['server']['personality'] = [] + 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: + data = file_or_string + + if isinstance(data, str): + data = data.encode('utf-8') + cont = base64.b64encode(data).decode('utf-8') + personality.append({ + 'path': filepath, + 'contents': cont, + }) + + if availability_zone: + 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) + elif block_device_mapping_v2: + # Following logic can't be removed because it will leaves + # a valid boot with both --image and --block-device + # failed , see bug 1433609 for more info + if image: + bdm_dict = {'uuid': base.getid(image), 'source_type': 'image', + 'destination_type': 'local', 'boot_index': 0, + 'delete_on_termination': True} + block_device_mapping_v2.insert(0, bdm_dict) + body['server']['block_device_mapping_v2'] = block_device_mapping_v2 + + 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, str): + all_net_data = nics + else: + # NOTE(tr3buchet): nics can be an empty list + all_net_data = [] + for nic_info in nics: + net_data = {} + # if value is empty string, do not send value in body + if nic_info.get('net-id'): + net_data['uuid'] = nic_info['net-id'] + if (nic_info.get('v4-fixed-ip') and + nic_info.get('v6-fixed-ip')): + raise base.exceptions.CommandError(_( + "Only one of 'v4-fixed-ip' and 'v6-fixed-ip' " + "may be provided.")) + elif nic_info.get('v4-fixed-ip'): + net_data['fixed_ip'] = nic_info['v4-fixed-ip'] + elif nic_info.get('v6-fixed-ip'): + net_data['fixed_ip'] = nic_info['v6-fixed-ip'] + if nic_info.get('port-id'): + net_data['port'] = nic_info['port-id'] + if nic_info.get('tag'): + net_data['tag'] = nic_info['tag'] + all_net_data.append(net_data) + body['server']['networks'] = all_net_data + + if disk_config is not None: + body['server']['OS-DCF:diskConfig'] = disk_config + + if access_ip_v4 is not None: + body['server']['accessIPv4'] = access_ip_v4 + + if access_ip_v6 is not None: + body['server']['accessIPv6'] = access_ip_v6 + + if description: + body['server']['description'] = description + + if tags: + body['server']['tags'] = tags + + 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): + """ + Get a server. + + :param server: ID of the :class:`Server` to get. + :rtype: :class:`Server` + """ + return self._get("/servers/%s" % base.getid(server), "server") + + def list(self, detailed=True, search_opts=None, marker=None, limit=None, + sort_keys=None, sort_dirs=None): + """ + Get a list of servers. + + :param detailed: Whether to return detailed server info (optional). + :param search_opts: Search options to filter out servers which don't + 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://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 + + :rtype: list of :class:`Server` + + Examples: + + client.servers.list() - returns detailed list of servers + + client.servers.list(search_opts={'status': 'ERROR'}) - + returns list of servers in error state. + + client.servers.list(limit=10) - returns only 10 servers + + """ + if search_opts is 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(): + # 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: + detail = "/detail" + + result = base.ListWithMeta([], None) + while True: + if marker: + qparams['marker'] = marker + + if limit and limit != -1: + qparams['limit'] = limit + + # 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 qparams or sort_keys or sort_dirs: + # sort keys and directions are unique since the same parameter + # key is repeated for each associated value + # (ie, &sort_key=key1&sort_key=key2&sort_key=key3) + items = list(qparams.items()) + if sort_keys: + items.extend(('sort_key', sort_key) + for sort_key in sort_keys) + if sort_dirs: + items.extend(('sort_dir', sort_dir) + for sort_dir in sort_dirs) + new_qparams = sorted(items, key=lambda x: x[0]) + query_string = "?%s" % parse.urlencode(new_qparams) + else: + query_string = "" + + servers = self._list("/servers%s%s" % (detail, query_string), + "servers") + result.extend(servers) + result.append_request_ids(servers.request_ids) + + if not servers or limit != -1: + break + marker = result[-1].id + return result + + def get_vnc_console(self, server, console_type): + """ + Get a vnc console for an instance + + :param server: The :class:`Server` (or its ID) to get console for. + :param console_type: Type of vnc console to get ('novnc' or 'xvpvnc') + :returns: An instance of novaclient.base.DictWithMeta + """ + + return self.get_console_url(server, console_type) + + def get_spice_console(self, server, console_type): + """ + Get a spice console for an instance + + :param server: The :class:`Server` (or its ID) to get console for. + :param console_type: Type of spice console to get ('spice-html5') + :returns: An instance of novaclient.base.DictWithMeta + """ + + return self.get_console_url(server, console_type) + + def get_rdp_console(self, server, console_type): + """ + Get a rdp console for an instance + + :param server: The :class:`Server` (or its ID) to get console for. + :param console_type: Type of rdp console to get ('rdp-html5') + :returns: An instance of novaclient.base.DictWithMeta + """ + + return self.get_console_url(server, console_type) + + def get_serial_console(self, server, console_type): + """ + Get a serial console for an instance + + :param server: The :class:`Server` (or its ID) to get console for. + :param console_type: Type of serial console to get ('serial') + :returns: An instance of novaclient.base.DictWithMeta + """ + + return self.get_console_url(server, console_type) + + def _get_protocol(self, console_type): + protocol = CONSOLE_TYPE_PROTOCOL_MAPPING.get(console_type) + if not protocol: + raise exceptions.UnsupportedConsoleType(console_type) + + return protocol + + @api_versions.wraps('2.0', '2.5') + def get_console_url(self, server, console_type): + """ + Retrieve a console url of a server. + + :param server: server to get console url for + :param console_type: type can be novnc, xvpvnc, spice-html5, + rdp-html5 and serial. + """ + + action = CONSOLE_TYPE_ACTION_MAPPING.get(console_type) + if not action: + raise exceptions.UnsupportedConsoleType(console_type) + return self._action(action, server, {'type': console_type}) + + @api_versions.wraps('2.8') + def get_mks_console(self, server): + """ + Get a mks console for an instance + + :param server: The :class:`Server` (or its ID) to get console for. + :returns: An instance of novaclient.base.DictWithMeta + """ + + return self.get_console_url(server, 'webmks') + + @api_versions.wraps('2.6') + def get_console_url(self, server, console_type): + """ + Retrieve a console url of a server. + + :param server: server to get console url for + :param console_type: type can be novnc/xvpvnc for protocol vnc; + spice-html5 for protocol spice; rdp-html5 for + protocol rdp; serial for protocol serial. + webmks for protocol mks (since version 2.8). + """ + + if self.api_version < api_versions.APIVersion('2.8'): + if console_type == 'webmks': + raise exceptions.UnsupportedConsoleType(console_type) + + protocol = self._get_protocol(console_type) + body = {'remote_console': {'protocol': protocol, + 'type': console_type}} + url = '/servers/%s/remote-consoles' % base.getid(server) + resp, body = self.api.client.post(url, body=body) + return self.convert_into_with_meta(body, resp) + + def get_password(self, server, private_key=None): + """ + Get admin password of an instance + + Returns the admin password of an instance in the clear if private_key + is provided, returns the ciphered password otherwise. + + Requires that openssl is installed and in the path + + :param server: The :class:`Server` (or its ID) for which the admin + password is to be returned + :param private_key: The private key to decrypt password + (optional) + :returns: An instance of novaclient.base.StrWithMeta or + novaclient.base.BytesWithMeta or + novaclient.base.UnicodeWithMeta + """ + + resp, body = self.api.client.get("/servers/%s/os-server-password" + % base.getid(server)) + ciphered_pw = body.get('password', '') if body else '' + if private_key and ciphered_pw: + try: + ciphered_pw = crypto.decrypt_password(private_key, ciphered_pw) + except Exception as exc: + ciphered_pw = '%sFailed to decrypt:\n%s' % (exc, ciphered_pw) + return self.convert_into_with_meta(ciphered_pw, resp) + + def clear_password(self, server): + """ + Clear the admin password of an instance + + Remove the admin password for an instance from the metadata server. + + :param server: The :class:`Server` (or its ID) for which the admin + password is to be cleared + """ + + return self._delete("/servers/%s/os-server-password" + % base.getid(server)) + + def stop(self, server): + """ + Stop the server. + + :param server: The :class:`Server` (or its ID) to stop + :returns: An instance of novaclient.base.TupleWithMeta + """ + resp, body = self._action_return_resp_and_body('os-stop', server, None) + return base.TupleWithMeta((resp, body), resp) + + def force_delete(self, server): + """ + Force delete the server. + + :param server: The :class:`Server` (or its ID) to force delete + :returns: An instance of novaclient.base.TupleWithMeta + """ + resp, body = self._action_return_resp_and_body('forceDelete', server, + None) + return base.TupleWithMeta((resp, body), resp) + + def restore(self, server): + """ + Restore soft-deleted server. + + :param server: The :class:`Server` (or its ID) to restore + :returns: An instance of novaclient.base.TupleWithMeta + """ + resp, body = self._action_return_resp_and_body('restore', server, None) + return base.TupleWithMeta((resp, body), resp) + + def start(self, server): + """ + Start the server. + + :param server: The :class:`Server` (or its ID) to start + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('os-start', server, None) + + def pause(self, server): + """ + Pause the server. + + :param server: The :class:`Server` (or its ID) to pause + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('pause', server, None) + + def unpause(self, server): + """ + Unpause the server. + + :param server: The :class:`Server` (or its ID) to unpause + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('unpause', server, None) + + @api_versions.wraps("2.0", "2.72") + def lock(self, server): + """ + Lock the server. + + :param server: The :class:`Server` (or its ID) to lock + :returns: An instance of novaclient.base.TupleWithMeta + """ + 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. + + :param server: The :class:`Server` (or its ID) to unlock + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('unlock', server, None) + + def suspend(self, server): + """ + Suspend the server. + + :param server: The :class:`Server` (or its ID) to suspend + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('suspend', server, None) + + def resume(self, server): + """ + Resume the server. + + :param server: The :class:`Server` (or its ID) to resume + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('resume', server, None) + + def rescue(self, server, password=None, image=None): + """ + Rescue the server. + + :param server: The :class:`Server` to rescue. + :param password: The admin password to be set in the rescue instance. + :param image: The :class:`Image` to rescue with. + :returns: An instance of novaclient.base.TupleWithMeta + """ + info = {} + if password: + info['adminPass'] = password + if image: + info['rescue_image_ref'] = base.getid(image) + resp, body = self._action_return_resp_and_body('rescue', server, + info or None) + return base.TupleWithMeta((resp, body), resp) + + def unrescue(self, server): + """ + Unrescue the server. + + :param server: The :class:`Server` (or its ID) to unrescue + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('unrescue', server, None) + + def shelve(self, server): + """ + Shelve the server. + + :param server: The :class:`Server` (or its ID) to shelve + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('shelve', server, None) + + def shelve_offload(self, server): + """ + Remove a shelved instance from the compute node. + + :param server: The :class:`Server` (or its ID) to shelve offload + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('shelveOffload', server, None) + + @api_versions.wraps("2.0", "2.76") + def unshelve(self, server): + """ + Unshelve the server. + + :param server: The :class:`Server` (or its ID) to unshelve + :returns: An instance of novaclient.base.TupleWithMeta + """ + 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. + + Often a cheaper call then getting all the details for a server. + + :param server: The :class:`Server` (or its ID) for which + the IP addresses are to be returned + :returns: An instance of novaclient.base.DictWithMeta + """ + resp, body = self.api.client.get("/servers/%s/ips" % + base.getid(server)) + return base.DictWithMeta(body['addresses'], resp) + + def diagnostics(self, server): + """ + Retrieve server diagnostics. + + :param server: The :class:`Server` (or its ID) for which + diagnostics to be returned + :returns: An instance of novaclient.base.TupleWithMeta + """ + resp, body = self.api.client.get("/servers/%s/diagnostics" % + 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'): + if not nics: + raise ValueError('nics are required after microversion 2.36') + elif nics and not isinstance(nics, (list, tuple)): + raise ValueError('nics must be a list or a tuple, not %s' % + type(nics)) + + # 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. + :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 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: 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: 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 + if not max_count: + max_count = min_count + if min_count > max_count: + min_count = max_count + + boot_args = [name, image, flavor] + + if description and self.api_version < api_versions.APIVersion("2.19"): + raise exceptions.UnsupportedAttribute("description", "2.19") + + self._validate_create_nics(nics) + + tags_microversion = api_versions.APIVersion("2.32") + if self.api_version < tags_microversion: + if nics: + for nic_info in nics: + if nic_info.get("tag"): + raise ValueError("Setting interface tags is " + "unsupported before microversion " + "2.32") + + if block_device_mapping_v2: + for bdm in block_device_mapping_v2: + if bdm.get("tag"): + raise ValueError("Setting block device tags is " + "unsupported before microversion " + "2.32") + + 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, + max_count=max_count, security_groups=security_groups, + 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, + description=description, tags=tags, + trusted_image_certificates=trusted_image_certificates, + host=host, hypervisor_hostname=hypervisor_hostname, + hostname=hostname) + + if block_device_mapping: + boot_kwargs['block_device_mapping'] = block_device_mapping + elif block_device_mapping_v2: + boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2 + + if nics: + boot_kwargs['nics'] = nics + + response_key = "server" if not reservation_id else "reservation_id" + return self._boot(response_key, *boot_args, **boot_kwargs) + + @api_versions.wraps("2.0", "2.18") + def update(self, server, name=None): + """ + 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 + + body = { + "server": { + "name": name, + }, + } + + return self._update("/servers/%s" % base.getid(server), body, "server") + + @api_versions.wraps("2.19", "2.89") + def update(self, server, name=None, description=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. + :returns: :class:`Server` + """ + if name is None and description is None: + return + + body = {"server": {}} + if name: + body["server"]["name"] = name + if description == "": + body["server"]["description"] = None + elif description: + body["server"]["description"] = description + + 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. + + :param server: The :class:`Server` (or its ID) for which the admin + password is to be changed + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action("changePassword", server, {"adminPass": password}) + + def delete(self, server): + """ + Delete (i.e. shut down and delete the image) this server. + + :param server: The :class:`Server` (or its ID) to delete + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete("/servers/%s" % base.getid(server)) + + def reboot(self, server, reboot_type=REBOOT_SOFT): + """ + Reboot a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param reboot_type: either :data:`REBOOT_SOFT` for a software-level + reboot, or `REBOOT_HARD` for a virtual power cycle hard reboot. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('reboot', server, {'type': reboot_type}) + + # 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 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. + :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` + """ + # 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 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], + ): + if hasattr(file_or_string, 'read'): + data = file_or_string.read() + else: + data = file_or_string + + cont = base64.b64encode(data.encode('utf-8')).decode('utf-8') + personality.append({ + 'path': filepath, + 'contents': cont, + }) + + 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. + + :param server: The :class:`Server` (or its ID). + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('migrate', server) + + @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'. + :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 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) + + def confirm_resize(self, server): + """ + Confirm that the resize worked, thus removing the original server. + + :param server: The :class:`Server` (or its ID) to share onto. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('confirmResize', server) + + def revert_resize(self, server): + """ + Revert a previous resize, switching back to the old server. + + :param server: The :class:`Server` (or its ID) to share onto. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('revertResize', server) + + def create_image(self, server, image_name, metadata=None): + """ + Snapshot a server. + + :param server: The :class:`Server` (or its ID) to share onto. + :param image_name: Name to give the snapshot image + :param metadata: Metadata to give newly-created image entity + :returns: An instance of novaclient.base.StrWithMeta + (The snapshot image's UUID) + """ + body = {'name': image_name, 'metadata': metadata or {}} + resp, body = self._action_return_resp_and_body('createImage', server, + body) + # The 2.45 microversion returns the image_id in the response body, + # not as a location header. + if self.api_version >= api_versions.APIVersion('2.45'): + image_uuid = body['image_id'] + else: + location = resp.headers['location'] + image_uuid = location.split('/')[-1] + return base.StrWithMeta(image_uuid, resp) + + def backup(self, server, backup_name, backup_type, rotation): + """ + Backup a server instance. + + :param server: The :class:`Server` (or its ID) to share onto. + :param backup_name: Name of the backup image + :param backup_type: The backup type, like 'daily' or 'weekly' + :param rotation: Int parameter representing how many backups to + keep around. + :returns: An instance of novaclient.base.TupleWithMeta if the request + microversion is < 2.45, otherwise novaclient.base.DictWithMeta. + """ + body = {'name': backup_name, + 'backup_type': backup_type, + 'rotation': rotation} + return self._action('createBackup', server, body) + + def set_meta(self, server, metadata): + """ + Set a server's metadata + :param server: The :class:`Server` to add metadata to + :param metadata: A dict of metadata to be added to the server + """ + body = {'metadata': metadata} + return self._create("/servers/%s/metadata" % base.getid(server), + body, "metadata") + + def set_meta_item(self, server, key, value): + """ + Updates an item of server metadata + :param server: The :class:`Server` to add metadata to + :param key: metadata key to update + :param value: string value + """ + body = {'meta': {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. + + :param server: The :class:`Server` (or its ID) whose console output + you would like to retrieve. + :param length: The number of tail loglines you would like to retrieve. + :returns: An instance of novaclient.base.StrWithMeta or + novaclient.base.UnicodeWithMeta + """ + resp, body = self._action_return_resp_and_body('os-getConsoleOutput', + server, + {'length': length}) + return self.convert_into_with_meta(body['output'], resp) + + def delete_meta(self, server, keys): + """ + Delete metadata from a server + + :param server: The :class:`Server` to add metadata to + :param keys: A list of metadata keys to delete from the server + :returns: An instance of novaclient.base.TupleWithMeta + """ + result = base.TupleWithMeta((), None) + for k in keys: + ret = self._delete("/servers/%s/metadata/%s" % + (base.getid(server), k)) + result.append_request_ids(ret.request_ids) + + 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): + """ + 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. + :param disk_over_commit: if True, allow disk overcommit. + :returns: An instance of novaclient.base.TupleWithMeta + """ + 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): + """ + 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) + + @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. + + :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' + :param force: forces to bypass the scheduler if host is provided. + :returns: An instance of novaclient.base.TupleWithMeta + """ + 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'): + """ + Reset the state of an instance to active or error. + + :param server: ID of the instance to reset the state of. + :param state: Desired state; either 'active' or 'error'. + Defaults to 'error'. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('os-resetState', server, dict(state=state)) + + def reset_network(self, server): + """ + Reset network of an instance. + + :param server: The :class:`Server` for network is to be reset + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('resetNetwork', server) + + def add_security_group(self, server, security_group): + """ + Add a Security Group to an instance + + :param server: ID of the instance. + :param security_group: The name of security group to add. + :returns: An instance of novaclient.base.DictWithMeta + """ + return self._action('addSecurityGroup', server, + {'name': security_group}) + + def remove_security_group(self, server, security_group): + """ + Remove a Security Group to an instance + + :param server: ID of the instance. + :param security_group: The name of security group to remove. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._action('removeSecurityGroup', server, + {'name': security_group}) + + def list_security_group(self, server): + """ + List Security Group(s) of an instance + + :param server: ID of the instance. + + """ + return self._list('/servers/%s/os-security-groups' % + 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 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 + """ + 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 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) + + @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 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) + + @api_versions.wraps("2.68") + def evacuate(self, server, host=None, password=None): + """ + Evacuate a server instance. + + :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): + """ + List attached network interfaces + + :param server: The :class:`Server` (or its ID) to query. + """ + return self._list('/servers/%s/os-interface' % base.getid(server), + 'interfaceAttachments', obj_class=NetworkInterface) + + @api_versions.wraps("2.0", "2.48") + def interface_attach(self, server, port_id, net_id, fixed_ip): + """ + Attach a network_interface to an instance. + + :param server: The :class:`Server` (or its ID) to attach to. + :param port_id: The port to attach. + """ + + body = {'interfaceAttachment': {}} + if port_id: + body['interfaceAttachment']['port_id'] = port_id + if net_id: + body['interfaceAttachment']['net_id'] = net_id + if fixed_ip: + body['interfaceAttachment']['fixed_ips'] = [ + {'ip_address': fixed_ip}] + + return self._create('/servers/%s/os-interface' % base.getid(server), + body, 'interfaceAttachment', + obj_class=NetworkInterface) + + @api_versions.wraps("2.49") + def interface_attach(self, server, port_id, net_id, fixed_ip, tag=None): + """ + Attach a network_interface to an instance. + + :param server: The :class:`Server` (or its ID) to attach to. + :param port_id: The port to attach. + The port_id and net_id parameters are mutually + exclusive. + :param net_id: The ID of the network to attach. + :param fixed_ip: The fixed IP addresses. If the fixed_ip is specified, + the net_id has to be specified at the same time. + :param tag: The tag. + """ + + body = {'interfaceAttachment': {}} + if port_id: + body['interfaceAttachment']['port_id'] = port_id + if net_id: + body['interfaceAttachment']['net_id'] = net_id + if fixed_ip: + body['interfaceAttachment']['fixed_ips'] = [ + {'ip_address': fixed_ip}] + if tag: + body['interfaceAttachment']['tag'] = tag + + return self._create('/servers/%s/os-interface' % base.getid(server), + body, 'interfaceAttachment', + obj_class=NetworkInterface) + + def interface_detach(self, server, port_id): + """ + Detach a network_interface from an instance. + + :param server: The :class:`Server` (or its ID) to detach from. + :param port_id: The port to detach. + :returns: An instance of novaclient.base.TupleWithMeta + """ + return self._delete('/servers/%s/os-interface/%s' % + (base.getid(server), port_id)) + + @api_versions.wraps("2.17") + def trigger_crash_dump(self, server): + """Trigger crash dump in an instance""" + return self._action("trigger_crash_dump", server) + + def _action(self, action, server, info=None, **kwargs): + """ + Perform a server "action" -- reboot/rebuild/resize/etc. + """ + resp, body = self._action_return_resp_and_body(action, server, + info=info, **kwargs) + return self.convert_into_with_meta(body, resp) + + def _action_return_resp_and_body(self, action, server, info=None, + **kwargs): + """ + Perform a server "action" and return response headers and body + """ + body = {action: info} + self.run_hooks('modify_body_for_action', body, **kwargs) + url = '/servers/%s/action' % base.getid(server) + return self.api.client.post(url, body=body) + + @api_versions.wraps('2.26') + def tag_list(self, server): + """ + Get list of tags from an instance. + """ + resp, body = self.api.client.get( + "/servers/%s/tags" % base.getid(server)) + return base.ListWithMeta(body['tags'], resp) + + @api_versions.wraps('2.26') + def delete_tag(self, server, tag): + """ + Remove single tag from an instance. + """ + return self._delete("/servers/%s/tags/%s" % (base.getid(server), tag)) + + @api_versions.wraps('2.26') + def delete_all_tags(self, server): + """ + Remove all tags from an instance. + """ + return self._delete("/servers/%s/tags" % base.getid(server)) + + @api_versions.wraps('2.26') + def set_tags(self, server, tags): + """ + Set list of tags to an instance. + """ + body = {"tags": tags} + return self._update("/servers/%s/tags" % base.getid(server), body) + + @api_versions.wraps('2.26') + def add_tag(self, server, tag): + """ + Add single tag to an instance. + """ + return self._update( + "/servers/%s/tags/%s" % (base.getid(server), tag), None) diff --git a/novaclient/v2/services.py b/novaclient/v2/services.py new file mode 100644 index 000000000..7adbf1376 --- /dev/null +++ b/novaclient/v2/services.py @@ -0,0 +1,150 @@ +# Copyright 2012 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Service interface. +""" + +from urllib import parse + +from novaclient import api_versions +from novaclient import base + + +class Service(base.Resource): + def __repr__(self): + return "" % self.id + + def _add_details(self, info): + dico = 'resource' in info and info['resource'] or info + for (k, v) in dico.items(): + setattr(self, k, v) + + +class ServiceManager(base.ManagerWithFind): + resource_class = Service + + def list(self, host=None, binary=None): + """ + Get a list of services. + + :param host: destination host name. + """ + url = "/os-services" + filters = [] + if host: + filters.append(("host", host)) + if binary: + filters.append(("binary", binary)) + if filters: + url = "%s?%s" % (url, parse.urlencode(filters)) + return self._list(url, "services") + + @api_versions.wraps("2.0", "2.10") + def _update_body(self, host, binary, disabled_reason=None): + body = {"host": host, + "binary": binary} + if disabled_reason is not None: + body["disabled_reason"] = disabled_reason + return body + + @api_versions.wraps("2.11") + def _update_body(self, host, binary, disabled_reason=None, + force_down=None): + body = {"host": host, + "binary": binary} + if disabled_reason is not None: + body["disabled_reason"] = disabled_reason + if force_down is not None: + body["forced_down"] = force_down + return body + + @api_versions.wraps('2.0', '2.52') + def enable(self, host, binary): + """Enable the service specified by hostname and binary.""" + body = self._update_body(host, binary) + return self._update("/os-services/enable", body, "service") + + @api_versions.wraps('2.53') + def enable(self, service_uuid): + """Enable the service specified by the service UUID ID. + + :param service_uuid: The UUID ID of the service to enable. + """ + return self._update( + "/os-services/%s" % service_uuid, {'status': 'enabled'}, "service") + + @api_versions.wraps('2.0', '2.52') + def disable(self, host, binary): + """Disable the service specified by hostname and binary.""" + body = self._update_body(host, binary) + return self._update("/os-services/disable", body, "service") + + @api_versions.wraps('2.53') + def disable(self, service_uuid): + """Disable the service specified by the service UUID ID. + + :param service_uuid: The UUID ID of the service to disable. + """ + return self._update("/os-services/%s" % service_uuid, + {'status': 'disabled'}, "service") + + @api_versions.wraps('2.0', '2.52') + def disable_log_reason(self, host, binary, reason): + """Disable the service with reason.""" + body = self._update_body(host, binary, reason) + return self._update("/os-services/disable-log-reason", body, "service") + + @api_versions.wraps('2.53') + def disable_log_reason(self, service_uuid, reason): + """Disable the service with a reason. + + :param service_uuid: The UUID ID of the service to disable. + :param reason: The reason for disabling a service. The minimum length + is 1 and the maximum length is 255. + """ + body = { + 'status': 'disabled', + 'disabled_reason': reason + } + return self._update("/os-services/%s" % service_uuid, body, "service") + + def delete(self, service_id): + """Delete a service. + + :param service_id: Before microversion 2.53, this must be an integer id + and may not uniquely the service in a multi-cell deployment. + Starting with microversion 2.53 this must be a UUID. + """ + return self._delete("/os-services/%s" % service_id) + + @api_versions.wraps("2.11", "2.52") + def force_down(self, host, binary, force_down=None): + """Force service state to down specified by hostname and binary.""" + body = self._update_body(host, binary, force_down=force_down) + return self._update("/os-services/force-down", body, "service") + + @api_versions.wraps("2.53") + def force_down(self, service_uuid, force_down): + """Update the service's ``forced_down`` field specified by the + service UUID ID. + + :param service_uuid: The UUID ID of the service. + :param force_down: Whether or not this service was forced down manually + by an administrator. This value is useful to know that some 3rd + party has verified the service should be marked down. + """ + return self._update("/os-services/%s" % service_uuid, + {'forced_down': force_down}, "service") diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py new file mode 100644 index 000000000..7242dbc13 --- /dev/null +++ b/novaclient/v2/shell.py @@ -0,0 +1,5809 @@ +# Copyright 2010 Jacob Kaplan-Moss + +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import collections +import datetime +import getpass +import logging +import os +import pprint +import sys +import time + +from oslo_utils import netutils +from oslo_utils import strutils +from oslo_utils import timeutils + +import novaclient +from novaclient import api_versions +from novaclient import base +from novaclient import client +from novaclient import exceptions +from novaclient.i18n import _ +from novaclient import shell +from novaclient import utils +from novaclient.v2 import availability_zones +from novaclient.v2 import quotas +from novaclient.v2 import servers + + +logger = logging.getLogger(__name__) + + +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 = { + 'id': 'uuid', + 'source': 'source_type', + 'dest': 'destination_type', + 'bus': 'disk_bus', + 'device': 'device_name', + 'size': 'volume_size', + 'format': 'guest_format', + 'bootindex': 'boot_index', + 'type': 'device_type', + 'shutdown': 'delete_on_termination', + 'tag': 'tag', + 'volume_type': 'volume_type' # added in 2.67 +} + + +def _key_value_pairing(text): + try: + (k, v) = text.split('=', 1) + return (k, v) + except ValueError: + msg = _("'%s' is not in the format of 'key=value'") % text + raise argparse.ArgumentTypeError(msg) + + +def _meta_parsing(metadata): + try: + return dict(v.split('=', 1) for v in metadata) + except ValueError: + msg = _("'%s' is not in the format of 'key=value'") % metadata + raise argparse.ArgumentTypeError(msg) + + +def _match_image(cs, wanted_properties): + image_list = cs.glance.list() + images_matched = [] + match = set(wanted_properties) + for img in image_list: + img_dict = {} + # exclude any unhashable entries + for key, value in img.to_dict().items(): + try: + set([key, value]) + except TypeError: + pass + else: + img_dict[key] = value + if match == match.intersection(set(img_dict.items())): + images_matched.append(img) + return images_matched + + +def _supports_block_device_tags(cs): + if (cs.api_version == api_versions.APIVersion('2.32') or + cs.api_version >= api_versions.APIVersion('2.42')): + return True + else: + return False + + +def _parse_device_spec(device_spec): + spec_dict = {} + for arg in device_spec.split(','): + if '=' in arg: + spec_dict.update([arg.split('=')]) + else: + raise argparse.ArgumentTypeError( + _("Expected a comma-separated list of key=value pairs. '%s' " + "is not a key=value pair.") % arg) + return spec_dict + + +def _parse_block_device_mapping_v2(cs, args, image): + bdm = [] + + if args.boot_volume: + bdm_dict = {'uuid': args.boot_volume, 'source_type': 'volume', + 'destination_type': 'volume', 'boot_index': 0, + 'delete_on_termination': False} + bdm.append(bdm_dict) + + if args.snapshot: + bdm_dict = {'uuid': args.snapshot, 'source_type': 'snapshot', + 'destination_type': 'volume', 'boot_index': 0, + '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 = {} + + if ('tag' in spec_dict and not _supports_block_device_tags(cs)): + raise exceptions.CommandError( + _("'tag' in block device mapping is not supported " + "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 + + source_type = bdm_dict.get('source_type') + if not source_type: + bdm_dict['source_type'] = 'blank' + elif source_type not in ( + 'volume', 'image', 'snapshot', 'blank'): + raise exceptions.CommandError( + _("The value of source_type key of --block-device " + "should be one of 'volume', 'image', 'snapshot' " + "or 'blank' but it was '%(action)s'") + % {'action': source_type}) + + destination_type = bdm_dict.get('destination_type') + if not destination_type: + source_type = bdm_dict['source_type'] + if source_type in ('image', 'blank'): + bdm_dict['destination_type'] = 'local' + if source_type in ('snapshot', 'volume'): + bdm_dict['destination_type'] = 'volume' + elif destination_type not in ('local', 'volume'): + raise exceptions.CommandError( + _("The value of destination_type key of --block-device " + "should be either 'local' or 'volume' but it " + "was '%(action)s'") + % {'action': destination_type}) + + # Convert the delete_on_termination to a boolean or set it to true by + # default for local block devices when not specified. + if 'delete_on_termination' in bdm_dict: + action = bdm_dict['delete_on_termination'] + if action not in ['remove', 'preserve']: + raise exceptions.CommandError( + _("The value of shutdown key of --block-device shall be " + "either 'remove' or 'preserve' but it was '%(action)s'") + % {'action': action}) + + bdm_dict['delete_on_termination'] = (action == 'remove') + elif bdm_dict.get('destination_type') == 'local': + bdm_dict['delete_on_termination'] = True + + bdm.append(bdm_dict) + + for ephemeral_spec in args.ephemeral: + bdm_dict = {'source_type': 'blank', 'destination_type': 'local', + 'boot_index': -1, 'delete_on_termination': True} + try: + eph_dict = _parse_device_spec(ephemeral_spec) + except ValueError: + err_msg = (_("Invalid ephemeral argument '%s'.") % args.ephemeral) + raise argparse.ArgumentTypeError(err_msg) + if 'size' in eph_dict: + bdm_dict['volume_size'] = eph_dict['size'] + if 'format' in eph_dict: + bdm_dict['guest_format'] = eph_dict['format'] + + bdm.append(bdm_dict) + + if args.swap: + bdm_dict = {'source_type': 'blank', 'destination_type': 'local', + 'boot_index': -1, 'delete_on_termination': True, + 'guest_format': 'swap', 'volume_size': args.swap} + bdm.append(bdm_dict) + + return bdm + + +def _supports_nic_tags(cs): + if ((cs.api_version >= api_versions.APIVersion('2.32') and + cs.api_version <= api_versions.APIVersion('2.36')) or + cs.api_version >= api_versions.APIVersion('2.42')): + return True + else: + return False + + +def _parse_nics(cs, args): + supports_auto_alloc = cs.api_version >= api_versions.APIVersion('2.37') + supports_nic_tags = _supports_nic_tags(cs) + + nic_keys = {'net-id', 'v4-fixed-ip', 'v6-fixed-ip', 'port-id', 'net-name'} + + if supports_auto_alloc and supports_nic_tags: + # API version >= 2.42 + nic_keys.add('tag') + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , " + "with only one of net-id, net-name or port-id " + "specified. Specifying a --nic of auto or none cannot " + "be used with any other --nic value.")) + elif supports_auto_alloc and not supports_nic_tags: + # 2.41 >= API version >= 2.37 + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , " + "with only one of net-id, net-name or port-id " + "specified. Specifying a --nic of auto or none cannot " + "be used with any other --nic value.")) + elif not supports_auto_alloc and supports_nic_tags: + # 2.36 >= API version >= 2.32 + nic_keys.add('tag') + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , " + "with only one of net-id, net-name or port-id " + "specified.")) + else: + # API version <= 2.31 + err_msg = (_("Invalid nic argument '%s'. Nic arguments must be of " + "the form --nic , " + "with only one of net-id, net-name or port-id " + "specified.")) + auto_or_none = False + nics = [] + for nic_str in args.nics: + nic_info = {} + nic_info_set = False + for kv_str in nic_str.split(","): + if auto_or_none: + # Since we start with auto_or_none being False, it being true + # means we've parsed an auto or none argument, then continued + # after the comma to another key=value pair. Since auto or none + # can only be given by themselves, raise. + raise exceptions.CommandError(_("'auto' or 'none' cannot be " + "used with any other nic " + "arguments")) + try: + # handle the special auto/none cases + if kv_str in ('auto', 'none'): + if not supports_auto_alloc: + raise exceptions.CommandError(err_msg % nic_str) + if nic_info_set: + # Since we start with nic_info_set being False, it + # being true means we've parsed a key=value pair, then + # landed on a auto or none argument after the comma. + # Since auto or none can only be given by themselves, + # raise. + raise exceptions.CommandError( + _("'auto' or 'none' cannot be used with any " + "other nic arguments")) + nics.append(kv_str) + auto_or_none = True + continue + k, v = kv_str.split("=", 1) + except ValueError: + raise exceptions.CommandError(err_msg % nic_str) + + if k in nic_keys: + # if user has given a net-name resolve it to network ID + if k == 'net-name': + k = 'net-id' + v = _find_network_id(cs, v) + # if some argument was given multiple times + if k in nic_info: + raise exceptions.CommandError(err_msg % nic_str) + nic_info[k] = v + nic_info_set = True + else: + raise exceptions.CommandError(err_msg % nic_str) + + if auto_or_none: + continue + + if 'v4-fixed-ip' in nic_info and not netutils.is_valid_ipv4( + nic_info['v4-fixed-ip']): + raise exceptions.CommandError(_("Invalid ipv4 address.")) + + if 'v6-fixed-ip' in nic_info and not netutils.is_valid_ipv6( + nic_info['v6-fixed-ip']): + raise exceptions.CommandError(_("Invalid ipv6 address.")) + + if bool(nic_info.get('net-id')) == bool(nic_info.get('port-id')): + raise exceptions.CommandError(err_msg % nic_str) + + nics.append(nic_info) + + if nics: + if auto_or_none: + if len(nics) > 1: + raise exceptions.CommandError(err_msg % nic_str) + # change the single list entry to a string + nics = nics[0] + else: + # Default to 'auto' if API version >= 2.37 and nothing was specified + if supports_auto_alloc: + nics = 'auto' + + return nics + + +def _boot(cs, args): + """Boot a new server.""" + if not args.flavor: + raise exceptions.CommandError(_("you need to specify a Flavor ID.")) + + if args.image: + image = _find_image(cs, args.image) + else: + image = None + + 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: + image = images[0] + else: + raise exceptions.CommandError(_("No images match the property " + "expected by --image-with")) + + min_count = 1 + max_count = 1 + if args.min_count is not None: + if args.min_count < 1: + raise exceptions.CommandError(_("min_count should be >= 1")) + min_count = args.min_count + max_count = min_count + if args.max_count is not None: + if args.max_count < 1: + raise exceptions.CommandError(_("max_count should be >= 1")) + max_count = args.max_count + if (args.min_count is not None and + args.max_count is not None and + args.min_count > args.max_count): + raise exceptions.CommandError(_("min_count should be <= max_count")) + + flavor = _find_flavor(cs, args.flavor) + + meta = _meta_parsing(args.meta) + + 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 + if args.key_name is not None: + key_name = args.key_name + + if args.user_data: + try: + 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") % + {'user_data': args.user_data, + 'exc': e}) + else: + userdata = None + + if args.availability_zone: + availability_zone = args.availability_zone + else: + availability_zone = None + + if args.security_groups: + security_groups = args.security_groups.split(',') + else: + security_groups = None + + block_device_mapping = {} + for bdm in args.block_device_mapping: + device_name, mapping = bdm.split('=', 1) + block_device_mapping[device_name] = mapping + + block_device_mapping_v2 = _parse_block_device_mapping_v2(cs, args, image) + + n_boot_args = len(list(filter( + bool, (image, args.boot_volume, args.snapshot)))) + have_bdm = block_device_mapping_v2 or block_device_mapping + + # Fail if more than one boot devices are present + # or if there is no device to boot from. + if n_boot_args > 1 or n_boot_args == 0 and not have_bdm: + raise exceptions.CommandError( + _("you need to specify at least one source ID (Image, Snapshot, " + "or Volume), a block device mapping or provide a set of " + "properties to match against an image")) + + if block_device_mapping and block_device_mapping_v2: + raise exceptions.CommandError( + _("you can't mix old block devices (--block-device-mapping) " + "with the new ones (--block-device, --boot-volume, --snapshot, " + "--ephemeral, --swap)")) + + nics = _parse_nics(cs, args) + + hints = {} + if args.scheduler_hints: + 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], str): + hints[key] = [hints[key]] + hints[key] += [value] + else: + hints[key] = value + boot_args = [args.name, image, flavor] + + if str(args.config_drive).lower() in ("true", "1"): + config_drive = True + elif str(args.config_drive).lower() in ("false", "0", "", "none"): + config_drive = None + else: + raise exceptions.CommandError( + _("The value of the '--config-drive' option must be " + "a boolean value.")) + + boot_kwargs = dict( + meta=meta, + key_name=key_name, + min_count=min_count, + max_count=max_count, + userdata=userdata, + availability_zone=availability_zone, + security_groups=security_groups, + block_device_mapping=block_device_mapping, + block_device_mapping_v2=block_device_mapping_v2, + nics=nics, + scheduler_hints=hints, + config_drive=config_drive, + admin_pass=args.admin_pass, + access_ip_v4=args.access_ip_v4, + access_ip_v6=args.access_ip_v6, + reservation_id=args.return_reservation_id) + + if 'description' in args: + boot_kwargs["description"] = args.description + + 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 + + +@utils.arg( + '--flavor', + default=None, + metavar='', + help=_("Name or ID of flavor (see 'nova flavor-list').")) +@utils.arg( + '--image', + default=None, + metavar='', + help=_("Name or ID of image (see 'glance image-list'). ")) +@utils.arg( + '--image-with', + default=[], + type=_key_value_pairing, + action='append', + metavar='', + help=_("Image metadata property (see 'glance image-show'). ")) +@utils.arg( + '--boot-volume', + default=None, + metavar="", + help=_("Volume ID to boot from.")) +@utils.arg( + '--snapshot', + default=None, + metavar="", + help=_("Snapshot ID to boot from (will create a volume).")) +@utils.arg( + '--min-count', + default=None, + type=int, + metavar='', + help=_("Boot at least servers (limited by quota).")) +@utils.arg( + '--max-count', + default=None, + type=int, + metavar='', + help=_("Boot up to servers (limited by quota).")) +@utils.arg( + '--meta', + metavar="", + action='append', + default=[], + help=_("Record arbitrary key/value metadata to /meta_data.json " + "on the metadata server. Can be specified multiple times.")) +@utils.arg( + '--file', + metavar="", + action='append', + dest='files', + default=[], + help=_("Store arbitrary files from locally to " + "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'), + metavar='', + help=_("Key name of keypair that should be created earlier with \ + the command keypair-add.")) +@utils.arg('name', metavar='', help=_('Name for the new server.')) +@utils.arg( + '--user-data', + default=None, + metavar='', + help=_("user data file to pass to be exposed by the metadata server.")) +@utils.arg( + '--availability-zone', + default=None, + metavar='', + help=_("The availability zone for server placement.")) +@utils.arg( + '--security-groups', + default=None, + metavar='', + help=_("Comma separated list of security group names.")) +@utils.arg( + '--block-device-mapping', + metavar="", + action='append', + default=[], + help=_("Block device mapping in the format " + "=:::.")) +@utils.arg( + '--block-device', + metavar="key1=value1[,key2=value2...]", + action='append', + default=[], + start_version='2.0', + end_version='2.31', + 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) and " + "shutdown=shutdown behaviour (either preserve or remove, " + "for local destination set to remove).")) +@utils.arg( + '--block-device', + metavar="key1=value1[,key2=value2...]", + action='append', + default=[], + start_version='2.32', + end_version='2.32', + 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, ...; " + "tag=device metadata tag (optional) " + "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) and " + "shutdown=shutdown behaviour (either preserve or remove, " + "for local destination set to remove).")) +@utils.arg( + '--block-device', + metavar="key1=value1[,key2=value2...]", + action='append', + default=[], + start_version='2.33', + end_version='2.41', + 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) and " + "shutdown=shutdown behaviour (either preserve or remove, " + "for local destination set to remove).")) +@utils.arg( + '--block-device', + metavar="key1=value1[,key2=value2...]", + 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) " + "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).")) +@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 MiB.")) +@utils.arg( + '--ephemeral', + metavar="size=[,format=]", + action='append', + default=[], + 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 " + "use.")) +@utils.arg( + '--nic', + metavar="", + action='append', + dest='nics', + default=[], + start_version='2.0', + end_version='2.31', + help=_("Create a NIC on the server. " + "Specify option multiple times to create multiple NICs. " + "net-id: attach NIC to network with this 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 " + "(either port-id or net-id must be provided).")) +@utils.arg( + '--nic', + metavar="", + action='append', + dest='nics', + default=[], + start_version='2.32', + end_version='2.36', + help=_("Create a NIC on the server. " + "Specify option multiple times to create multiple nics. " + "net-id: attach NIC to network with this 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).")) +@utils.arg( + '--nic', + metavar="", + action='append', + dest='nics', + default=[], + start_version='2.37', + end_version='2.41', + help=_("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 " + "(either port-id or net-id must be provided).")) +@utils.arg( + '--nic', + metavar="", + action='append', + dest='nics', + default=[], + start_version='2.42', + help=_("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).")) +@utils.arg( + '--config-drive', + metavar="", + dest='config_drive', + default=False, + help=_("Enable config drive. The value must be a boolean value.")) +@utils.arg( + '--poll', + dest='poll', + action="store_true", + default=False, + help=_('Report the new server boot progress until it completes.')) +@utils.arg( + '--admin-pass', + dest='admin_pass', + metavar='', + default=None, + help=_('Admin password for the instance.')) +@utils.arg( + '--access-ip-v4', + dest='access_ip_v4', + metavar='', + default=None, + help=_('Alternative access IPv4 of the instance.')) +@utils.arg( + '--access-ip-v6', + dest='access_ip_v6', + metavar='', + default=None, + help=_('Alternative access IPv6 of the instance.')) +@utils.arg( + '--description', + metavar='', + dest='description', + default=None, + help=_('Description for the server.'), + start_version="2.19") +@utils.arg( + '--tags', + metavar='', + default=None, + help=_('Tags for the server.' + 'Tags must be separated by commas: --tags '), + start_version="2.52") +@utils.arg( + '--return-reservation-id', + dest='return_reservation_id', + 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) + + server = cs.servers.create(*boot_args, **boot_kwargs) + if boot_kwargs['reservation_id']: + new_server = {'reservation_id': server} + utils.print_dict(new_server) + return + else: + _print_server(cs, args, server) + + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'building', ['active']) + + +def _poll_for_status(poll_fn, obj_id, action, final_ok_states, + poll_period=5, show_progress=True, + status_field="status", silent=False): + """Block while an action is being performed, periodically printing + progress. + """ + def print_progress(progress): + if show_progress: + msg = (_('\rServer %(action)s... %(progress)s%% complete') + % dict(action=action, progress=progress)) + else: + msg = _('\rServer %(action)s...') % dict(action=action) + + sys.stdout.write(msg) + sys.stdout.flush() + + if not silent: + print() + + while True: + obj = poll_fn(obj_id) + + status = getattr(obj, status_field) + + if status: + status = status.lower() + + progress = getattr(obj, 'progress', None) or 0 + if status in final_ok_states: + if not silent: + print_progress(100) + print(_("\nFinished")) + break + elif status == "error": + if not silent: + print(_("\nError %s server") % action) + raise exceptions.ResourceInErrorState(obj) + elif status == "deleted": + if not silent: + print(_("\nDeleted %s server") % action) + raise exceptions.InstanceInDeletedState(obj.fault["message"]) + + if not silent: + print_progress(progress) + + time.sleep(poll_period) + + +def _expand_dict_attr(collection, attr): + """Expand item attribute whose value is a dict. + + Take a collection of items where the named attribute is known to have a + dictionary value and replace the named attribute with multiple attributes + whose names are the keys of the dictionary namespaced with the original + attribute name. + """ + for item in collection: + field = getattr(item, 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): + for item in collection: + keys = item.__dict__.keys() + item_dict = item.to_dict() + 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): + power_states = [ + 'NOSTATE', # 0x00 + 'Running', # 0x01 + '', # 0x02 + 'Paused', # 0x03 + 'Shutdown', # 0x04 + '', # 0x05 + 'Crashed', # 0x06 + 'Suspended' # 0x07 + ] + + for item in collection: + try: + setattr(item, 'power_state', + power_states[getattr(item, 'power_state')]) + except AttributeError: + setattr(item, 'power_state', "N/A") + try: + 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_mib')]) + + +def _print_flavor_extra_specs(flavor): + try: + return flavor.get_keys() + except exceptions.NotFound: + return "N/A" + + +def _print_flavor_list(cs, flavors, show_extra_specs=False): + _translate_flavor_keys(flavors) + + headers = [ + 'ID', + 'Name', + 'Memory_MiB', + 'Disk', + 'Ephemeral', + 'Swap', + 'VCPUs', + 'RXTX_Factor', + 'Is_Public', + ] + + formatters = {} + if show_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') + + if cs.api_version >= api_versions.APIVersion('2.55'): + headers.append('Description') + + utils.print_list(flavors, headers, formatters) + + +@utils.arg( + '--extra-specs', + dest='extra_specs', + action='store_true', + default=False, + help=_('Get extra-specs of each flavor.')) +@utils.arg( + '--all', + dest='all', + action='store_true', + default=False, + help=_('Display all flavors (Admin only).')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last flavor ID of the previous page; displays list of flavors' + ' after "marker".')) +@utils.arg( + '--min-disk', + dest='min_disk', + metavar='', + default=None, + help=_('Filters the flavors by a minimum disk space, in GiB.')) +@utils.arg( + '--min-ram', + dest='min_ram', + metavar='', + default=None, + help=_('Filters the flavors by a minimum RAM, in MiB.')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_("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.")) +@utils.arg( + '--sort-key', + dest='sort_key', + metavar='', + default=None, + help=_('Flavors list sort key.')) +@utils.arg( + '--sort-dir', + dest='sort_dir', + metavar='', + default=None, + help=_('Flavors list sort direction.')) +def do_flavor_list(cs, args): + """Print a list of available 'flavors' (sizes of servers).""" + if args.all: + flavors = cs.flavors.list(is_public=None, min_disk=args.min_disk, + min_ram=args.min_ram, sort_key=args.sort_key, + sort_dir=args.sort_dir) + else: + 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(cs, flavors, args.extra_specs) + + +@utils.arg( + 'flavor', + metavar='', + help=_("Name or ID of the flavor to delete.")) +def do_flavor_delete(cs, args): + """Delete a specific flavor""" + flavor = _find_flavor(cs, args.flavor) + cs.flavors.delete(flavor) + + +@utils.arg( + 'flavor', + metavar='', + help=_("Name or ID of flavor.")) +def do_flavor_show(cs, args): + """Show details about the given flavor.""" + flavor = _find_flavor(cs, args.flavor) + _print_flavor(flavor) + + +@utils.arg( + 'name', + metavar='', + help=_("Unique name of the new flavor.")) +@utils.arg( + 'id', + metavar='', + help=_("Unique ID of the new flavor." + " Specifying 'auto' will generated a UUID for the ID.")) +@utils.arg( + 'ram', + metavar='', + help=_("Memory size in MiB.")) +@utils.arg( + 'disk', + metavar='', + help=_("Disk size in GiB.")) +@utils.arg( + '--ephemeral', + metavar='', + help=_("Ephemeral space size in GiB (default 0)."), + default=0) +@utils.arg( + 'vcpus', + metavar='', + help=_("Number of vcpus")) +@utils.arg( + '--swap', + metavar='', + help=_("Additional swap space size in MiB (default 0)."), + default=0) +@utils.arg( + '--rxtx-factor', + metavar='', + help=_("RX/TX factor (default 1)."), + default=1.0) +@utils.arg( + '--is-public', + metavar='', + 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, 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( + 'flavor', + metavar='', + help=_("Name or ID of flavor.")) +@utils.arg( + 'action', + metavar='', + choices=['set', 'unset'], + help=_("Actions: 'set' or 'unset'.")) +@utils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Extra_specs to set/unset (only key is necessary on unset).')) +def do_flavor_key(cs, args): + """Set or unset extra_spec for a flavor.""" + flavor = _find_flavor(cs, args.flavor) + keypair = _extract_metadata(args) + + if args.action == 'set': + flavor.set_keys(keypair) + elif args.action == 'unset': + flavor.unset_keys(keypair.keys()) + + +@utils.arg( + '--flavor', + metavar='', + help=_("Filter results by flavor name or ID.")) +def do_flavor_access_list(cs, args): + """Print access information about the given flavor.""" + if args.flavor: + flavor = _find_flavor(cs, args.flavor) + if flavor.is_public: + raise exceptions.CommandError(_("Access list not available " + "for public flavors.")) + kwargs = {'flavor': flavor} + else: + raise exceptions.CommandError(_("Unable to get all access lists. " + "Specify --flavor")) + + try: + access_list = cs.flavor_access.list(**kwargs) + except NotImplementedError as e: + raise exceptions.CommandError("%s" % str(e)) + + columns = ['Flavor_ID', 'Tenant_ID'] + utils.print_list(access_list, columns) + + +@utils.arg( + 'flavor', + metavar='', + help=_("Flavor name or ID to add access for the given tenant.")) +@utils.arg( + 'tenant', metavar='', + help=_('Tenant ID to add flavor access for.')) +def do_flavor_access_add(cs, args): + """Add flavor access for the given tenant.""" + flavor = _find_flavor(cs, args.flavor) + access_list = cs.flavor_access.add_tenant_access(flavor, args.tenant) + columns = ['Flavor_ID', 'Tenant_ID'] + utils.print_list(access_list, columns) + + +@utils.arg( + 'flavor', + metavar='', + help=_("Flavor name or ID to remove access for the given tenant.")) +@utils.arg( + 'tenant', metavar='', + help=_('Tenant ID to remove flavor access for.')) +def do_flavor_access_remove(cs, args): + """Remove flavor access for the given tenant.""" + flavor = _find_flavor(cs, args.flavor) + access_list = cs.flavor_access.remove_tenant_access(flavor, args.tenant) + columns = ['Flavor_ID', 'Tenant_ID'] + utils.print_list(access_list, columns) + + +def _extract_metadata(args): + metadata = {} + for metadatum in args.metadata[0]: + # Can only pass the key in on 'delete' + # So this doesn't have to have '=' + if metadatum.find('=') > -1: + (key, value) = metadatum.split('=', 1) + else: + key = metadatum + value = None + + metadata[key] = value + return metadata + + +def _print_image(image): + info = image.to_dict() + + # ignore links, we don't need to present those + info.pop('links', None) + + # try to replace a server entity to just an id + server = info.pop('server', None) + try: + info['server'] = server['id'] + except (KeyError, TypeError): + pass + + # break up metadata and display each on its own row + metadata = info.pop('metadata', {}) + try: + for key, value in metadata.items(): + _key = 'metadata %s' % key + info[_key] = value + except AttributeError: + pass + + utils.print_dict(info) + + +def _print_flavor(flavor): + info = flavor.to_dict() + # ignore links, we don't need to present those + info.pop('links') + # 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) + + +@utils.arg( + '--reservation-id', + dest='reservation_id', + metavar='', + default=None, + help=_('Only return servers that match reservation-id.')) +@utils.arg( + '--ip', + dest='ip', + metavar='', + default=None, + help=_('Search with regular expression match by IP address.')) +@utils.arg( + '--ip6', + dest='ip6', + metavar='', + default=None, + help=_('Search with regular expression match by IPv6 address.')) +@utils.arg( + '--name', + dest='name', + metavar='', + default=None, + help=_('Search with regular expression match by name.')) +@utils.arg( + '--status', + dest='status', + metavar='', + default=None, + help=_('Search by server status.')) +@utils.arg( + '--flavor', + dest='flavor', + metavar='', + default=None, + help=_('Search by flavor name or ID.')) +@utils.arg( + '--image', + dest='image', + metavar='', + default=None, + help=_('Search by image name or ID.')) +@utils.arg( + '--host', + dest='host', + metavar='', + default=None, + help=_('Search servers by hostname to which they are assigned (Admin ' + 'only).')) +@utils.arg( + '--all-tenants', + dest='all_tenants', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=int(strutils.bool_from_string( + os.environ.get("ALL_TENANTS", 'false'), True)), + help=_('Display information from all tenants (Admin only).')) +@utils.arg( + '--tenant', + # nova db searches by project_id + dest='tenant', + metavar='', + nargs='?', + help=_('Display information from single tenant (Admin only).')) +@utils.arg( + '--user', + dest='user', + metavar='', + nargs='?', + help=_('Display information from single user (Admin only until ' + 'microversion 2.82).')) +@utils.arg( + '--deleted', + dest='deleted', + action="store_true", + default=False, + help=_('Only display deleted servers (Admin only).')) +@utils.arg( + '--fields', + default=None, + metavar='', + help=_('Comma-separated list of fields to display. ' + 'Use the show command to see which fields are available.')) +@utils.arg( + '--minimal', + dest='minimal', + action="store_true", + default=False, + help=_('Get only UUID and name.')) +@utils.arg( + '--sort', + dest='sort', + metavar='[:]', + help=_('Comma-separated list of sort keys and directions in the form ' + 'of [:]. The direction defaults to descending if ' + 'not specified.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last server UUID of the previous page; displays list of ' + 'servers after "marker".')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_("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.")) +@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 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', + metavar='', + default=None, + help=_("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 "), + start_version="2.26") +@utils.arg( + '--tags-any', + dest='tags-any', + metavar='', + default=None, + help=_("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 "), + start_version="2.26") +@utils.arg( + '--not-tags', + dest='not-tags', + metavar='', + default=None, + help=_("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 "), + start_version="2.26") +@utils.arg( + '--not-tags-any', + dest='not-tags-any', + metavar='', + default=None, + help=_("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 "), + 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 + flavorid = None + if args.image: + imageid = _find_image(cs, args.image).id + if args.flavor: + flavorid = _find_flavor(cs, args.flavor).id + # search by tenant or user only works with all_tenants + if args.tenant: + args.all_tenants = 1 + + search_opts = { + 'all_tenants': args.all_tenants, + 'reservation_id': args.reservation_id, + 'ip': args.ip, + 'ip6': args.ip6, + 'name': args.name, + 'image': imageid, + 'flavor': flavorid, + 'status': args.status, + 'tenant_id': args.tenant, + 'user_id': args.user, + 'host': args.host, + 'deleted': args.deleted, + '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: + search_opts[arg] = getattr(args, arg) + + filters = {'security_groups': utils.format_security_groups} + + # In microversion 2.47 we started embedding flavor info in server details. + have_embedded_flavor_info = ( + cs.api_version >= api_versions.APIVersion('2.47')) + # If we don't have embedded flavor info then we only report the flavor id + # rather than looking up the rest of the information. + if not have_embedded_flavor_info: + filters['flavor'] = lambda f: f['id'] + + id_col = 'ID' + + detailed = not args.minimal + + sort_keys = [] + sort_dirs = [] + if args.sort: + for sort in args.sort.split(','): + sort_key, _sep, sort_dir = sort.partition(':') + if not sort_dir: + sort_dir = 'desc' + elif sort_dir not in ('asc', 'desc'): + raise exceptions.CommandError(_( + 'Unknown sort direction: %s') % sort_dir) + sort_keys.append(sort_key) + sort_dirs.append(sort_dir) + + if search_opts['changes-since']: + try: + timeutils.parse_isotime(search_opts['changes-since']) + except ValueError: + 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, + sort_dirs=sort_dirs, + marker=args.marker, + limit=args.limit) + convert = [('OS-EXT-SRV-ATTR:host', 'host'), + ('OS-EXT-STS:task_state', 'task_state'), + ('OS-EXT-SRV-ATTR:instance_name', 'instance_name'), + ('OS-EXT-STS:power_state', 'power_state'), + ('hostId', 'host_id')] + _translate_keys(servers, convert) + _translate_extended_states(servers) + + formatters = {} + cols = [] + fmts = {} + + # 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: + 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( + args.fields, servers, exclude_fields=('id',), filters=filters) + + if args.minimal: + columns = [ + id_col, + 'Name'] + elif cols: + columns = [id_col] + cols + formatters.update(fmts) + else: + columns = [ + id_col, + 'Name', + 'Status', + 'Task State', + 'Power State', + 'Networks' + ] + # If getting the data for all tenants, print + # Tenant ID as well + if search_opts['all_tenants']: + columns.insert(2, 'Tenant ID') + if search_opts['changes-since'] or search_opts.get('changes-before'): + columns.append('Updated') + formatters['Networks'] = utils.format_servers_list_networks + sortby_index = 1 + if args.sort: + sortby_index = None + utils.print_list(servers, columns, + formatters, sortby_index=sortby_index) + + +def _get_list_table_columns_and_formatters(fields, objs, exclude_fields=(), + filters=None): + """Check and add fields to output columns. + + If there is any value in fields that not an attribute of obj, + CommandError will be raised. + + If fields has duplicate values (case sensitive), we will make them unique + and ignore duplicate ones. + + If exclude_fields is specified, any field both in fields and + exclude_fields will be ignored. + + :param fields: A list of string contains the fields to be printed. + :param objs: An list of object which will be used to check if field is + valid or not. Note, we don't check fields if obj is None or + empty. + :param exclude_fields: A tuple of string which contains the fields to be + excluded. + :param filters: A dictionary defines how to get value from fields, this + is useful when field's value is a complex object such as + dictionary. + + :return: columns, formatters. + columns is a list of string which will be used as table header. + formatters is a dictionary specifies how to display the value + of the field. + They can be [], {}. + :raise: novaclient.exceptions.CommandError + """ + if not fields: + return [], {} + + if not objs: + obj = None + elif isinstance(objs, list): + obj = objs[0] + else: + obj = objs + + 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 field not in existing_fields: + non_existent_fields.append(field) + continue + if field in exclude_fields: + continue + field_title, formatter = utils.make_field_formatter(field, + filters) + columns.append(field_title) + formatters[field_title] = formatter + exclude_fields.add(field) + + if non_existent_fields: + raise exceptions.CommandError( + _("Non-existent fields are specified: %s") % non_existent_fields) + + return columns, formatters + + +@utils.arg( + '--hard', + dest='reboot_type', + action='store_const', + const=servers.REBOOT_HARD, + default=servers.REBOOT_SOFT, + help=_('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.')) +@utils.arg( + 'server', + metavar='', nargs='+', + help=_('Name or ID of server(s).')) +@utils.arg( + '--poll', + dest='poll', + action="store_true", + default=False, + help=_('Poll until reboot is complete.')) +def do_reboot(cs, args): + """Reboot a server.""" + servers = [_find_server(cs, s) for s in args.server] + utils.do_action_on_many( + lambda s: s.reboot(args.reboot_type), + servers, + _("Request to reboot server %s has been accepted."), + _("Unable to reboot the specified server(s).")) + + if args.poll: + utils.do_action_on_many( + lambda s: _poll_for_status(cs.servers.get, s.id, 'rebooting', + ['active'], show_progress=False), + servers, + _("Wait for server %s reboot."), + _("Wait for specified server(s) failed.")) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('image', metavar='', help=_("Name or ID of new image.")) +@utils.arg( + '--rebuild-password', + dest='rebuild_password', + metavar='', + default=False, + help=_("Set the provided admin password on the rebuilt server.")) +@utils.arg( + '--poll', + dest='poll', + action="store_true", + default=False, + help=_('Report the server rebuild progress until it completes.')) +@utils.arg( + '--minimal', + dest='minimal', + action="store_true", + default=False, + help=_('Skips flavor/image lookups when showing servers.')) +@utils.arg( + '--preserve-ephemeral', + action="store_true", + default=False, + help=_('Preserve the default ephemeral storage partition on rebuild.')) +@utils.arg( + '--name', + metavar='', + default=None, + help=_('Name for the new server.')) +@utils.arg( + '--description', + metavar='', + dest='description', + default=None, + help=_('New description for the server.'), + start_version="2.19") +@utils.arg( + '--meta', + metavar="", + action='append', + default=[], + help=_("Record arbitrary key/value metadata to /meta_data.json " + "on the metadata server. Can be specified multiple times.")) +@utils.arg( + '--file', + metavar="", + action='append', + dest='files', + default=[], + help=_("Store arbitrary files from locally to " + "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: + kwargs['password'] = args.rebuild_password + else: + kwargs['password'] = None + + if 'description' in args: + kwargs['description'] = args.description + + # 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: + _poll_for_status(cs.servers.get, server.id, 'rebuilding', ['active']) + + +@utils.arg( + 'server', metavar='', + help=_('Name (old name) or ID of server.')) +@utils.arg( + '--name', + metavar='', + dest='name', + default=None, + help=_('New name for the server.')) +@utils.arg( + '--description', + metavar='', + dest='description', + default=None, + 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 + 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) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + 'flavor', + metavar='', + help=_("Name or ID of new flavor.")) +@utils.arg( + '--poll', + dest='poll', + action="store_true", + default=False, + help=_('Report the server resize progress until it completes.')) +def do_resize(cs, args): + """Resize a server.""" + server = _find_server(cs, args.server) + flavor = _find_flavor(cs, args.flavor) + server.resize(flavor) + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'resizing', + ['active', 'verify_resize']) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_resize_confirm(cs, args): + """Confirm a previous resize.""" + _find_server(cs, args.server).confirm_resize() + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_resize_revert(cs, args): + """Revert a previous resize (and return to the previous VM).""" + _find_server(cs, args.server).revert_resize() + + +@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', + action="store_true", + default=False, + help=_('Report the server migration progress until it completes.')) +def do_migrate(cs, args): + """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(**update_kwargs) + + if args.poll: + _poll_for_status(cs.servers.get, server.id, 'migrating', + ['active', 'verify_resize']) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_pause(cs, args): + """Pause a server.""" + _find_server(cs, args.server).pause() + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_unpause(cs, args): + """Unpause a server.""" + _find_server(cs, args.server).unpause() + + +@utils.arg( + '--all-tenants', + action='store_const', + const=1, + default=0, + help=_('Stop server(s) in another tenant by name (Admin only).')) +@utils.arg( + 'server', + metavar='', nargs='+', + help=_('Name or ID of server(s).')) +def do_stop(cs, args): + """Stop the server(s).""" + find_args = {'all_tenants': args.all_tenants} + utils.do_action_on_many( + lambda s: _find_server(cs, s, **find_args).stop(), + args.server, + _("Request to stop server %s has been accepted."), + _("Unable to stop the specified server(s).")) + + +@utils.arg( + '--all-tenants', + action='store_const', + const=1, + default=0, + help=_('Start server(s) in another tenant by name (Admin only).')) +@utils.arg( + 'server', + metavar='', nargs='+', + help=_('Name or ID of server(s).')) +def do_start(cs, args): + """Start the server(s).""" + find_args = {'all_tenants': args.all_tenants} + utils.do_action_on_many( + lambda s: _find_server(cs, s, **find_args).start(), + args.server, + _("Request to start server %s has been accepted."), + _("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. + """ + 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.')) +def do_unlock(cs, args): + """Unlock a server.""" + _find_server(cs, args.server).unlock() + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_suspend(cs, args): + """Suspend a server.""" + _find_server(cs, args.server).suspend() + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_resume(cs, args): + """Resume a server.""" + _find_server(cs, args.server).resume() + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--password', + metavar='', + dest='password', + help=_('The admin password to be set in the rescue environment.')) +@utils.arg( + '--image', + metavar='', + dest='image', + help=_('The image to rescue with.')) +def do_rescue(cs, args): + """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. + """ + kwargs = {} + if args.image: + kwargs['image'] = _find_image(cs, args.image) + if args.password: + kwargs['password'] = args.password + utils.print_dict(_find_server(cs, args.server).rescue(**kwargs)[1]) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_unrescue(cs, args): + """Restart the server from normal boot disk again.""" + _find_server(cs, args.server).unrescue() + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_shelve(cs, args): + """Shelve a server.""" + _find_server(cs, args.server).shelve() + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_shelve_offload(cs, args): + """Remove a shelved server from the compute node.""" + _find_server(cs, args.server).shelve_offload() + + +@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.""" + 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.')) +def do_diagnostics(cs, args): + """Retrieve server diagnostics.""" + server = _find_server(cs, args.server) + 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 ' + 'be refreshed from neutron (Admin only).')) +def do_refresh_network(cs, args): + """Refresh server network information.""" + server = _find_server(cs, args.server) + cs.server_external_events.create([{'server_uuid': server.id, + 'name': 'network-changed'}]) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_set_password(cs, args): + """ + Change the admin password for a server. + """ + server = _find_server(cs, args.server) + p1 = getpass.getpass('New password: ') + p2 = getpass.getpass('Again: ') + if p1 != p2: + raise exceptions.CommandError(_("Passwords do not match.")) + server.change_password(p1) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('name', metavar='', help=_('Name of snapshot.')) +@utils.arg( + '--metadata', + metavar="", + action='append', + default=[], + help=_("Record arbitrary key/value metadata to /meta_data.json " + "on the metadata server. Can be specified multiple times.")) +@utils.arg( + '--show', + dest='show', + action="store_true", + default=False, + help=_('Print image info.')) +@utils.arg( + '--poll', + dest='poll', + action="store_true", + default=False, + help=_('Report the snapshot progress and poll until image creation is ' + 'complete.')) +def do_image_create(cs, args): + """Create a new image by taking a snapshot of a running server.""" + server = _find_server(cs, args.server) + meta = _meta_parsing(args.metadata) or None + image_uuid = cs.servers.create_image(server, args.name, meta) + + if args.poll: + _poll_for_status(cs.glance.find_image, image_uuid, 'snapshotting', + ['active']) + + # NOTE(sirp): A race-condition exists between when the image finishes + # uploading and when the servers's `task_state` is cleared. To account + # for this, we need to poll a second time to ensure the `task_state` is + # cleared before returning, ensuring that a snapshot taken immediately + # after this function returns will succeed. + # + # A better long-term solution will be to separate 'snapshotting' and + # 'image-uploading' in Nova and clear the task-state once the VM + # snapshot is complete but before the upload begins. + task_state_field = "OS-EXT-STS:task_state" + if hasattr(server, task_state_field): + _poll_for_status(cs.servers.get, server.id, 'image_snapshot', + [None], status_field=task_state_field, + show_progress=False, silent=True) + + if args.show: + _print_image(_find_image(cs, image_uuid)) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('name', metavar='', help=_('Name of the backup image.')) +@utils.arg( + 'backup_type', metavar='', + help=_('The backup type, like "daily" or "weekly".')) +@utils.arg( + 'rotation', metavar='', + help=_('Int parameter representing how many backups to keep ' + 'around.')) +def do_backup(cs, args): + """Backup a server by creating a 'backup' type snapshot.""" + result = _find_server(cs, args.server).backup(args.name, + args.backup_type, + args.rotation) + # Microversion >= 2.45 will return a DictWithMeta that has the image_id + # in it for the backup snapshot image. + if cs.api_version >= api_versions.APIVersion('2.45'): + _print_image(_find_image(cs, result['image_id'])) + + +@utils.arg( + 'server', + metavar='', + help=_("Name or ID of server.")) +@utils.arg( + 'action', + metavar='', + choices=['set', 'delete'], + help=_("Actions: 'set' or 'delete'.")) +@utils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Metadata to set or delete (only key is necessary on delete).')) +def do_meta(cs, args): + """Set or delete metadata on a server.""" + server = _find_server(cs, args.server) + metadata = _extract_metadata(args) + + if args.action == 'set': + cs.servers.set_meta(server, metadata) + elif args.action == 'delete': + cs.servers.delete_meta(server, sorted(metadata.keys(), reverse=True)) + + +def _print_server(cs, args, server=None, wrap=0): + # By default when searching via name we will do a + # findall(name=blah) and due a REST /details which is not the same + # as a .get() and doesn't get the information about flavors and + # images. This fix it as we redo the call with the id which does a + # .get() to get all information. + if not server: + server = _find_server(cs, args.server) + + minimal = getattr(args, "minimal", False) + + 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) + + flavor = info.get('flavor', {}) + if cs.api_version >= api_versions.APIVersion('2.47'): + # The "flavor" field is a JSON representation of a dict containing the + # flavor information used at boot. + if minimal: + # To retain something similar to the previous behaviour, keep the + # 'flavor' field name but just output the original name. + info['flavor'] = flavor['original_name'] + else: + # Replace the "flavor" field with individual namespaced fields. + del info['flavor'] + for key in flavor.keys(): + info['flavor:' + key] = flavor[key] + else: + # Prior to microversion 2.47 we just have the ID of the flavor so we + # need to retrieve the flavor information (which may have changed + # since the instance was booted). + flavor_id = flavor.get('id', '') + if minimal: + info['flavor'] = flavor_id + else: + try: + info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, + flavor_id) + except Exception: + info['flavor'] = '%s (%s)' % (_("Flavor not found"), flavor_id) + + if 'security_groups' in info: + # when we have multiple nics the info will include the + # security groups N times where N == number of nics. Be nice + # and only display it once. + info['security_groups'] = ', '.join( + sorted(set(group['name'] for group in info['security_groups']))) + + image = info.get('image', {}) + if image: + image_id = image.get('id', '') + if minimal: + info['image'] = image_id + else: + try: + info['image'] = '%s (%s)' % (_find_image(cs, image_id).name, + image_id) + except Exception: + info['image'] = '%s (%s)' % (_("Image not found"), image_id) + else: # Booted from volume + info['image'] = _("Attempt to boot from volume - no image supplied") + + info.pop('links', None) + info.pop('addresses', None) + info.pop('OS-EXT-SRV-ATTR:user_data', None) + + utils.print_dict(info, wrap=wrap) + + +@utils.arg( + '--minimal', + dest='minimal', + action="store_true", + default=False, + help=_('Skips flavor/image lookups when showing servers.')) +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--wrap', dest='wrap', metavar='', type=int, default=0, + help=_('Wrap the output to a specified length, or 0 to disable.')) +def do_show(cs, args): + """Show details about the given server.""" + _print_server(cs, args, wrap=args.wrap) + + +@utils.arg( + '--all-tenants', + action='store_const', + const=1, + default=0, + help=_('Delete server(s) in another tenant by name (Admin only).')) +@utils.arg( + 'server', metavar='', nargs='+', + help=_('Name or ID of server(s).')) +def do_delete(cs, args): + """Immediately shut down and delete specified server(s).""" + find_args = {'all_tenants': args.all_tenants} + utils.do_action_on_many( + lambda s: _find_server(cs, s, **find_args).delete(), + args.server, + _("Request to delete server %s has been accepted."), + _("Unable to delete the specified server(s).")) + + +def _find_server(cs, server, raise_if_notfound=True, **find_args): + """Get a server by name or ID. + + :param cs: NovaClient's instance + :param server: identifier of server + :param raise_if_notfound: raise an exception if server is not found + :param find_args: argument to search server + """ + if raise_if_notfound: + return utils.find_resource(cs.servers, server, **find_args) + else: + try: + return utils.find_resource(cs.servers, server, + wrap_exception=False) + except exceptions.NoUniqueMatch as e: + raise exceptions.CommandError(str(e)) + except exceptions.NotFound: + # The server can be deleted + return server + + +def _find_image(cs, image): + """Get an image by name or ID.""" + try: + return cs.glance.find_image(image) + except (exceptions.NotFound, exceptions.NoUniqueMatch) as 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): + """Get a flavor by name, ID, or RAM size.""" + try: + return utils.find_resource(cs.flavors, flavor, is_public=None) + except exceptions.NotFound: + return cs.flavors.find(ram=flavor) + + +def _find_network_id(cs, net_name): + """Get unique network ID from network name from neutron""" + try: + return cs.neutron.find_network(net_name).id + except (exceptions.NotFound, exceptions.NoUniqueMatch) as e: + raise exceptions.CommandError(str(e)) + + +def _print_volume(volume): + utils.print_dict(volume.to_dict()) + + +def _translate_availability_zone_keys(collection): + _translate_keys(collection, + [('zoneName', 'name'), ('zoneState', 'status')]) + + +def _translate_volume_attachments_keys(collection): + _translate_keys(collection, + [('serverId', 'server_id'), + ('volumeId', 'volume_id')]) + + +@utils.arg( + 'server', + metavar='', + help=_('Name or ID of server.')) +@utils.arg( + 'volume', + metavar='', + help=_('ID of the volume to attach.')) +@utils.arg( + 'device', metavar='', default=None, nargs='?', + help=_('Name of the device e.g. /dev/vdb. ' + 'Use "auto" for autoassign (if supported). ' + 'Libvirt driver will use default device name.')) +@utils.arg( + '--tag', + metavar='', + 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': + args.device = None + + update_kwargs = {} + 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, + **update_kwargs) + _print_volume(volume) + + +@utils.arg( + 'server', + metavar='', + help=_('Name or ID of server.')) +@utils.arg( + 'src_volume', + metavar='', + help=_('ID of the source (original) volume.')) +@utils.arg( + 'dest_volume', + 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. + + 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, + **kwargs) + + +@utils.arg( + 'server', + metavar='', + help=_('Name or ID of server.')) +@utils.arg( + 'attachment_id', + metavar='', + help=_('ID of the volume to detach.')) +def do_volume_detach(cs, args): + """Detach a volume from a server.""" + cs.volumes.delete_server_volume(_find_server(cs, args.server).id, + args.attachment_id) + + +@utils.arg( + 'server', + metavar='', + help=_('Name or ID of server.')) +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) + # 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') +def console_dict_accessor(cs, data): + return data['console'] + + +@api_versions.wraps('2.6') +def console_dict_accessor(cs, data): + return data['remote_console'] + + +class Console(object): + def __init__(self, console_dict): + self.type = console_dict['type'] + self.url = console_dict['url'] + + +def print_console(cs, data): + utils.print_list([Console(console_dict_accessor(cs, data))], + ['Type', 'Url']) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + 'console_type', + metavar='', + help=_('Type of vnc console ("novnc" or "xvpvnc").')) +def do_get_vnc_console(cs, args): + """Get a vnc console to a server.""" + server = _find_server(cs, args.server) + data = server.get_vnc_console(args.console_type) + + print_console(cs, data) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + 'console_type', + metavar='', + help=_('Type of spice console ("spice-html5").')) +def do_get_spice_console(cs, args): + """Get a spice console to a server.""" + server = _find_server(cs, args.server) + data = server.get_spice_console(args.console_type) + + print_console(cs, data) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + 'console_type', + metavar='', + help=_('Type of rdp console ("rdp-html5").')) +def do_get_rdp_console(cs, args): + """Get a rdp console to a server.""" + server = _find_server(cs, args.server) + data = server.get_rdp_console(args.console_type) + + print_console(cs, data) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--console-type', + default='serial', + help=_('Type of serial console, default="serial".')) +def do_get_serial_console(cs, args): + """Get a serial console to a server.""" + if args.console_type not in ('serial',): + raise exceptions.CommandError( + _("Invalid parameter value for 'console_type', " + "currently supported 'serial'.")) + + server = _find_server(cs, args.server) + data = server.get_serial_console(args.console_type) + + print_console(cs, data) + + +@api_versions.wraps('2.8') +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_get_mks_console(cs, args): + """Get an MKS console to a server.""" + server = _find_server(cs, args.server) + data = server.get_mks_console() + + print_console(cs, data) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + 'private_key', + metavar='', + help=_('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.'), + nargs='?', + default=None) +def do_get_password(cs, args): + """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. + """ + server = _find_server(cs, args.server) + data = server.get_password(args.private_key) + print(data) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_clear_password(cs, args): + """Clear the admin password for a server from the metadata server. + This action does not actually change the instance server password. + """ + server = _find_server(cs, args.server) + server.clear_password() + + +def _print_floating_ip_list(floating_ips): + convert = [('instance_id', 'server_id')] + _translate_keys(floating_ips, convert) + + utils.print_list(floating_ips, + ['Id', 'IP', 'Server Id', 'Fixed IP', 'Pool']) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + '--length', + metavar='', + default=None, + help=_('Length in lines to tail.')) +def do_console_log(cs, args): + """Get console log output of a server.""" + server = _find_server(cs, args.server) + data = server.get_console_output(length=args.length) + print(data) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + 'secgroup', + metavar='', + help=_('Name or ID of Security Group.')) +def do_add_secgroup(cs, args): + """Add a Security Group to a server.""" + server = _find_server(cs, args.server) + server.add_security_group(args.secgroup) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + 'secgroup', + metavar='', + help=_('Name of Security Group.')) +def do_remove_secgroup(cs, args): + """Remove a Security Group from a server.""" + server = _find_server(cs, args.server) + server.remove_security_group(args.secgroup) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_list_secgroup(cs, args): + """List Security Group(s) of a server.""" + server = _find_server(cs, args.server) + groups = server.list_security_group() + _print_secgroups(groups) + + +def _print_secgroups(secgroups): + utils.print_list(secgroups, ['Id', 'Name', 'Description']) + + +@api_versions.wraps("2.0", "2.1") +def _keypair_create(cs, args, name, pub_key): + return cs.keypairs.create(name, pub_key) + + +@api_versions.wraps("2.2", "2.9") +def _keypair_create(cs, args, name, pub_key): + return cs.keypairs.create(name, pub_key, key_type=args.key_type) + + +@api_versions.wraps("2.10") +def _keypair_create(cs, args, name, pub_key): + return cs.keypairs.create(name, pub_key, key_type=args.key_type, + user_id=args.user) + + +@utils.arg('name', metavar='', help=_('Name of key.')) +@utils.arg( + '--pub-key', + metavar='', + default=None, + help=_('Path to a public ssh key.')) +@utils.arg( + '--key-type', + metavar='', + default='ssh', + help=_('Keypair type. Can be ssh or x509.'), + start_version="2.2") +@utils.arg( + '--user', + metavar='', + default=None, + help=_('ID of user to whom to add key-pair (Admin only).'), + start_version="2.10") +def do_keypair_add(cs, args): + """Create a new key pair for use with servers.""" + name = args.name + pub_key = args.pub_key + if pub_key: + if pub_key == '-': + pub_key = sys.stdin.read() + else: + try: + with open(os.path.expanduser(pub_key)) as f: + pub_key = f.read() + except IOError as e: + raise exceptions.CommandError( + _("Can't open or read '%(key)s': %(exc)s") + % {'key': pub_key, 'exc': e} + ) + + keypair = _keypair_create(cs, args, name, pub_key) + + if not pub_key: + private_key = keypair.private_key + print(private_key) + + +@api_versions.wraps("2.0", "2.9") +@utils.arg('name', metavar='', help=_('Keypair name to delete.')) +def do_keypair_delete(cs, args): + """Delete keypair given by its name.""" + name = _find_keypair(cs, args.name) + cs.keypairs.delete(name) + + +@api_versions.wraps("2.10") +@utils.arg('name', metavar='', help=_('Keypair name to delete.')) +@utils.arg( + '--user', + metavar='', + default=None, + help=_('ID of key-pair owner (Admin only).')) +def do_keypair_delete(cs, args): + """Delete keypair given by its name.""" + cs.keypairs.delete(args.name, args.user) + + +@api_versions.wraps("2.0", "2.1") +def _get_keypairs_list_columns(cs, args): + return ['Name', 'Fingerprint'] + + +@api_versions.wraps("2.2") +def _get_keypairs_list_columns(cs, args): + return ['Name', 'Type', 'Fingerprint'] + + +@api_versions.wraps("2.0", "2.9") +def do_keypair_list(cs, args): + """Print a list of keypairs for a user""" + keypairs = cs.keypairs.list() + columns = _get_keypairs_list_columns(cs, args) + utils.print_list(keypairs, columns) + + +@api_versions.wraps("2.10", "2.34") +@utils.arg( + '--user', + metavar='', + default=None, + help=_('List key-pairs of specified user ID (Admin only).')) +def do_keypair_list(cs, args): + """Print a list of keypairs for a user""" + keypairs = cs.keypairs.list(args.user) + columns = _get_keypairs_list_columns(cs, args) + utils.print_list(keypairs, columns) + + +@api_versions.wraps("2.35") +@utils.arg( + '--user', + metavar='', + default=None, + help=_('List key-pairs of specified user ID (Admin only).')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last keypair of the previous page; displays list of keypairs ' + 'after "marker".')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_("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.")) +def do_keypair_list(cs, args): + """Print a list of keypairs for a user""" + keypairs = cs.keypairs.list(args.user, args.marker, args.limit) + columns = _get_keypairs_list_columns(cs, args) + utils.print_list(keypairs, columns) + + +def _print_keypair(keypair): + kp = keypair.to_dict() + pk = kp.pop('public_key') + utils.print_dict(kp) + print(_("Public key: %s") % pk) + + +@api_versions.wraps("2.0", "2.9") +@utils.arg( + 'keypair', + metavar='', + help=_("Name of keypair.")) +def do_keypair_show(cs, args): + """Show details about the given keypair.""" + keypair = _find_keypair(cs, args.keypair) + _print_keypair(keypair) + + +@api_versions.wraps("2.10") +@utils.arg( + 'keypair', + metavar='', + help=_("Name of keypair.")) +@utils.arg( + '--user', + metavar='', + default=None, + help=_('ID of key-pair owner (Admin only).')) +def do_keypair_show(cs, args): + """Show details about the given keypair.""" + keypair = cs.keypairs.get(args.keypair, args.user) + _print_keypair(keypair) + + +def _find_keypair(cs, keypair): + """Get a keypair by name.""" + return utils.find_resource(cs.keypairs, keypair) + + +def _print_absolute_limits(limits): + """Prints absolute limits.""" + class Limit(object): + def __init__(self, name, used, max, other): + self.name = name + self.used = used + self.max = max + self.other = other + + limit_map = { + 'maxServerMeta': {'name': 'Server Meta', 'type': 'max'}, + 'maxPersonality': {'name': 'Personality', 'type': 'max'}, + 'maxPersonalitySize': {'name': 'Personality Size', 'type': 'max'}, + 'maxImageMeta': {'name': 'ImageMeta', 'type': 'max'}, + 'maxTotalKeypairs': {'name': 'Keypairs', 'type': 'max'}, + 'totalCoresUsed': {'name': 'Cores', 'type': 'used'}, + 'maxTotalCores': {'name': 'Cores', 'type': 'max'}, + 'totalRAMUsed': {'name': 'RAM', 'type': 'used'}, + 'maxTotalRAMSize': {'name': 'RAM', 'type': 'max'}, + 'totalInstancesUsed': {'name': 'Instances', 'type': 'used'}, + 'maxTotalInstances': {'name': 'Instances', 'type': 'max'}, + 'totalFloatingIpsUsed': {'name': 'FloatingIps', 'type': 'used'}, + 'maxTotalFloatingIps': {'name': 'FloatingIps', 'type': 'max'}, + 'totalSecurityGroupsUsed': {'name': 'SecurityGroups', 'type': 'used'}, + 'maxSecurityGroups': {'name': 'SecurityGroups', 'type': 'max'}, + 'maxSecurityGroupRules': {'name': 'SecurityGroupRules', 'type': 'max'}, + 'maxServerGroups': {'name': 'ServerGroups', 'type': 'max'}, + 'totalServerGroupsUsed': {'name': 'ServerGroups', 'type': 'used'}, + 'maxServerGroupMembers': {'name': 'ServerGroupMembers', 'type': 'max'}, + } + + max = {} + used = {} + other = {} + limit_names = [] + columns = ['Name', 'Used', 'Max'] + for limit in limits: + map = limit_map.get(limit.name, {'name': limit.name, 'type': 'other'}) + name = map['name'] + if map['type'] == 'max': + max[name] = limit.value + elif map['type'] == 'used': + used[name] = limit.value + else: + other[name] = limit.value + if 'Other' not in columns: + columns.append('Other') + if name not in limit_names: + limit_names.append(name) + + limit_names.sort() + + limit_list = [] + for name in limit_names: + limit_list.append(Limit( + name, used.get(name, '-'), max.get(name, '-'), + other.get(name, '-'), + )) + + utils.print_list(limit_list, columns) + + +def _print_rate_limits(limits): + """print rate limits.""" + columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] + utils.print_list(limits, columns) + + +@utils.arg( + '--tenant', + # nova db searches by project_id + dest='tenant', + metavar='', + nargs='?', + help=_('Display information from single tenant (Admin only).')) +@utils.arg( + '--reserved', + dest='reserved', + action='store_true', + default=False, + help=_('Include reservations count.')) +def do_limits(cs, args): + """Print rate and absolute limits.""" + limits = cs.limits.get(args.reserved, args.tenant) + _print_rate_limits(limits.rate) + _print_absolute_limits(limits.absolute) + + +def _get_usage_marker(usage): + marker = None + if hasattr(usage, 'server_usages') and usage.server_usages: + marker = usage.server_usages[-1]['instance_id'] + return marker + + +def _get_usage_list_marker(usage_list): + marker = None + if usage_list: + marker = _get_usage_marker(usage_list[-1]) + return marker + + +def _merge_usage(usage, next_usage): + usage.server_usages.extend(next_usage.server_usages) + usage.total_hours += next_usage.total_hours + usage.total_memory_mb_usage += next_usage.total_memory_mb_usage + usage.total_vcpus_usage += next_usage.total_vcpus_usage + usage.total_local_gb_usage += next_usage.total_local_gb_usage + + +def _merge_usage_list(usages, next_usage_list): + for next_usage in next_usage_list: + if next_usage.tenant_id in usages: + _merge_usage(usages[next_usage.tenant_id], next_usage) + else: + usages[next_usage.tenant_id] = next_usage + + +@utils.arg( + '--start', + metavar='', + help=_('Usage range start date ex 2012-01-20. (default: 4 weeks ago)'), + default=None) +@utils.arg( + '--end', + metavar='', + help=_('Usage range end date, ex 2012-01-20. (default: tomorrow)'), + default=None) +def do_usage_list(cs, args): + """List usage data for all tenants.""" + dateformat = "%Y-%m-%d" + rows = ["Tenant ID", "Servers", "RAM MiB-Hours", "CPU Hours", + "Disk GiB-Hours"] + + now = timeutils.utcnow() + + if args.start: + start = datetime.datetime.strptime(args.start, dateformat) + else: + start = now - datetime.timedelta(weeks=4) + + if args.end: + end = datetime.datetime.strptime(args.end, dateformat) + else: + end = now + datetime.timedelta(days=1) + + def simplify_usage(u): + simplerows = [x.lower().replace(" ", "_") for x in rows] + + setattr(u, simplerows[0], u.tenant_id) + setattr(u, simplerows[1], "%d" % len(u.server_usages)) + setattr(u, simplerows[2], "%.2f" % u.total_memory_mb_usage) + setattr(u, simplerows[3], "%.2f" % u.total_vcpus_usage) + setattr(u, simplerows[4], "%.2f" % u.total_local_gb_usage) + + if cs.api_version < api_versions.APIVersion('2.40'): + usage_list = cs.usage.list(start, end, detailed=True) + else: + # If the number of instances used to calculate the usage is greater + # than CONF.api.max_limit, the usage will be split across multiple + # requests and the responses will need to be merged back together. + usages = collections.OrderedDict() + usage_list = cs.usage.list(start, end, detailed=True) + _merge_usage_list(usages, usage_list) + marker = _get_usage_list_marker(usage_list) + while marker: + next_usage_list = cs.usage.list( + start, end, detailed=True, marker=marker) + marker = _get_usage_list_marker(next_usage_list) + if marker: + _merge_usage_list(usages, next_usage_list) + usage_list = list(usages.values()) + + print(_("Usage from %(start)s to %(end)s:") % + {'start': start.strftime(dateformat), + 'end': end.strftime(dateformat)}) + + for usage in usage_list: + simplify_usage(usage) + + utils.print_list(usage_list, rows) + + +@utils.arg( + '--start', + metavar='', + help=_('Usage range start date ex 2012-01-20. (default: 4 weeks ago)'), + default=None) +@utils.arg( + '--end', metavar='', + help=_('Usage range end date, ex 2012-01-20. (default: tomorrow)'), + default=None) +@utils.arg( + '--tenant', + metavar='', + default=None, + help=_('UUID of tenant to get usage for.')) +def do_usage(cs, args): + """Show usage data for a single tenant.""" + dateformat = "%Y-%m-%d" + rows = ["Servers", "RAM MiB-Hours", "CPU Hours", "Disk GiB-Hours"] + + now = timeutils.utcnow() + + if args.start: + start = datetime.datetime.strptime(args.start, dateformat) + else: + start = now - datetime.timedelta(weeks=4) + + if args.end: + end = datetime.datetime.strptime(args.end, dateformat) + else: + end = now + datetime.timedelta(days=1) + + def simplify_usage(u): + simplerows = [x.lower().replace(" ", "_") for x in rows] + + setattr(u, simplerows[0], "%d" % len(u.server_usages)) + setattr(u, simplerows[1], "%.2f" % u.total_memory_mb_usage) + setattr(u, simplerows[2], "%.2f" % u.total_vcpus_usage) + setattr(u, simplerows[3], "%.2f" % u.total_local_gb_usage) + + if args.tenant: + tenant_id = args.tenant + else: + if isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + tenant_id = auth.get_auth_ref(cs.client.session).project_id + else: + tenant_id = cs.client.tenant_id + + if cs.api_version < api_versions.APIVersion('2.40'): + usage = cs.usage.get(tenant_id, start, end) + else: + # If the number of instances used to calculate the usage is greater + # than CONF.api.max_limit, the usage will be split across multiple + # requests and the responses will need to be merged back together. + usage = cs.usage.get(tenant_id, start, end) + marker = _get_usage_marker(usage) + while marker: + next_usage = cs.usage.get(tenant_id, start, end, marker=marker) + marker = _get_usage_marker(next_usage) + if marker: + _merge_usage(usage, next_usage) + + print(_("Usage from %(start)s to %(end)s:") % + {'start': start.strftime(dateformat), + 'end': end.strftime(dateformat)}) + + if getattr(usage, 'total_vcpus_usage', None): + simplify_usage(usage) + utils.print_list([usage], rows) + else: + print(_('None')) + + +@utils.arg( + '--hypervisor', + metavar='', + default=None, + help=_('Type of hypervisor.')) +def do_agent_list(cs, args): + """DEPRECATED List all builds.""" + _emit_agent_deprecation_warning() + result = cs.agents.list(args.hypervisor) + columns = ["Agent_id", "Hypervisor", "OS", "Architecture", "Version", + 'Md5hash', 'Url'] + utils.print_list(result, columns) + + +@utils.arg('os', metavar='', help=_('Type of OS.')) +@utils.arg( + 'architecture', + metavar='', + help=_('Type of architecture.')) +@utils.arg('version', metavar='', help=_('Version.')) +@utils.arg('url', metavar='', help=_('URL.')) +@utils.arg('md5hash', metavar='', help=_('MD5 hash.')) +@utils.arg( + 'hypervisor', + metavar='', + default='xen', + help=_('Type of hypervisor.')) +def do_agent_create(cs, args): + """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) + utils.print_dict(result.to_dict()) + + +@utils.arg('id', metavar='', help=_('ID of the agent-build.')) +def do_agent_delete(cs, args): + """DEPRECATED Delete existing agent build.""" + _emit_agent_deprecation_warning() + cs.agents.delete(args.id) + + +@utils.arg('id', metavar='', help=_('ID of the agent-build.')) +@utils.arg('version', metavar='', help=_('Version.')) +@utils.arg('url', metavar='', help=_('URL')) +@utils.arg('md5hash', metavar='', help=_('MD5 hash.')) +def do_agent_modify(cs, args): + """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()) + + +def _find_aggregate(cs, aggregate): + """Get an aggregate by name or ID.""" + return utils.find_resource(cs.aggregates, aggregate) + + +def do_aggregate_list(cs, args): + """Print a list of all aggregates.""" + aggregates = cs.aggregates.list() + columns = ['Id', 'Name', 'Availability Zone'] + if cs.api_version >= api_versions.APIVersion('2.41'): + columns.append('UUID') + utils.print_list(aggregates, columns) + + +@utils.arg('name', metavar='', help=_('Name of aggregate.')) +@utils.arg( + 'availability_zone', + metavar='', + default=None, + nargs='?', + help=_('The availability zone of the aggregate (optional).')) +def do_aggregate_create(cs, args): + """Create a new aggregate with the specified details.""" + aggregate = cs.aggregates.create(args.name, args.availability_zone) + _print_aggregate_details(cs, aggregate) + + +@utils.arg( + 'aggregate', + metavar='', + help=_('Name or ID of aggregate to delete.')) +def do_aggregate_delete(cs, args): + """Delete the aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + cs.aggregates.delete(aggregate) + print(_("Aggregate %s has been successfully deleted.") % aggregate.id) + + +@utils.arg( + 'aggregate', + metavar='', + help=_('Name or ID of aggregate to update.')) +@utils.arg( + '--name', + metavar='', + dest='name', + help=_('New name for aggregate.')) +@utils.arg( + '--availability-zone', + metavar='', + dest='availability_zone', + help=_('New availability zone for aggregate.')) +def do_aggregate_update(cs, args): + """Update the aggregate's name and optionally availability zone.""" + aggregate = _find_aggregate(cs, args.aggregate) + updates = {} + if args.name: + updates["name"] = args.name + 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) + + +@utils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate to update.')) +@utils.arg( + 'metadata', + metavar='', + nargs='+', + action='append', + default=[], + help=_('Metadata to add/update to aggregate. ' + 'Specify only the key to delete a metadata item.')) +def do_aggregate_set_metadata(cs, args): + """Update the metadata associated with the aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + metadata = _extract_metadata(args) + currentmetadata = getattr(aggregate, 'metadata', {}) + if set(metadata.items()) & set(currentmetadata.items()): + raise exceptions.CommandError(_("metadata already exists")) + for key, value in metadata.items(): + if value is None and key not in currentmetadata: + raise exceptions.CommandError(_("metadata key %s does not exist" + " hence can not be deleted") + % key) + aggregate = cs.aggregates.set_metadata(aggregate.id, metadata) + print(_("Metadata has been successfully updated for aggregate %s.") % + aggregate.id) + _print_aggregate_details(cs, aggregate) + + +@utils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate.')) +@utils.arg( + 'host', metavar='', + help=_('The host to add to the aggregate.')) +def do_aggregate_add_host(cs, args): + """Add the host to the specified aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + aggregate = cs.aggregates.add_host(aggregate.id, args.host) + print(_("Host %(host)s has been successfully added for aggregate " + "%(aggregate_id)s ") % {'host': args.host, + 'aggregate_id': aggregate.id}) + _print_aggregate_details(cs, aggregate) + + +@utils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate.')) +@utils.arg( + 'host', metavar='', + help=_('The host to remove from the aggregate.')) +def do_aggregate_remove_host(cs, args): + """Remove the specified host from the specified aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + aggregate = cs.aggregates.remove_host(aggregate.id, args.host) + print(_("Host %(host)s has been successfully removed from aggregate " + "%(aggregate_id)s ") % {'host': args.host, + 'aggregate_id': aggregate.id}) + _print_aggregate_details(cs, aggregate) + + +@utils.arg( + 'aggregate', metavar='', + help=_('Name or ID of aggregate.')) +def do_aggregate_show(cs, args): + """Show details of the specified aggregate.""" + aggregate = _find_aggregate(cs, args.aggregate) + _print_aggregate_details(cs, aggregate) + + +def _print_aggregate_details(cs, aggregate): + columns = ['Id', 'Name', 'Availability Zone', 'Hosts', 'Metadata'] + if cs.api_version >= api_versions.APIVersion('2.41'): + columns.append('UUID') + + def parser_metadata(fields): + return utils.pretty_choice_dict(getattr(fields, 'metadata', {}) or {}) + + def parser_hosts(fields): + return utils.pretty_choice_list(getattr(fields, 'hosts', [])) + + formatters = { + 'Metadata': parser_metadata, + 'Hosts': parser_hosts, + } + 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. If no host is specified, the scheduler ' + 'will choose one.')) +@utils.arg( + '--block-migrate', + action='store_true', + dest='block_migrate', + default=False, + help=_('True in case of block_migration. (Default=False:live_migration)'), + start_version="2.0", end_version="2.24") +@utils.arg( + '--block-migrate', + action='store_true', + dest='block_migrate', + default="auto", + help=_('True in case of block_migration. (Default=auto:live_migration)'), + start_version="2.25") +@utils.arg( + '--disk-over-commit', + action='store_true', + dest='disk_over_commit', + default=False, + help=_('Allow overcommit. (Default=False)'), + start_version="2.0", end_version="2.24") +@utils.arg( + '--force', + dest='force', + action='store_true', + default=False, + 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.""" + + update_kwargs = {} + if 'disk_over_commit' in args: + update_kwargs['disk_over_commit'] = args.disk_over_commit + if 'force' in args and args.force: + update_kwargs['force'] = args.force + + _find_server(cs, args.server).live_migrate(args.host, args.block_migrate, + **update_kwargs) + + +@api_versions.wraps("2.22") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('migration', metavar='', help=_('ID of migration.')) +def do_live_migration_force_complete(cs, args): + """Force on-going live migration to complete.""" + server = _find_server(cs, args.server) + cs.server_migrations.live_migrate_force_complete(server, args.migration) + + +@api_versions.wraps("2.23") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_server_migration_list(cs, args): + """Get the migrations list of specified server.""" + server = _find_server(cs, args.server) + migrations = cs.server_migrations.list(server) + + fields = ['Id', 'Source Node', 'Dest Node', 'Source Compute', + 'Dest Compute', 'Dest Host', 'Status', 'Server UUID', + 'Created At', 'Updated At'] + + format_name = ["Total Memory Bytes", "Processed Memory Bytes", + "Remaining Memory Bytes", "Total Disk Bytes", + "Processed Disk Bytes", "Remaining Disk Bytes"] + + format_key = ["memory_total_bytes", "memory_processed_bytes", + "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)) + + utils.print_list(migrations, fields + format_name, formatters) + + +@api_versions.wraps("2.23") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('migration', metavar='', help=_('ID of migration.')) +def do_server_migration_show(cs, args): + """Get the migration of specified server.""" + server = _find_server(cs, args.server) + migration = cs.server_migrations.get(server, args.migration) + utils.print_dict(migration.to_dict()) + + +@api_versions.wraps("2.24") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('migration', metavar='', help=_('ID of migration.')) +def do_live_migration_abort(cs, args): + """Abort an on-going live migration.""" + server = _find_server(cs, args.server) + cs.server_migrations.live_migration_abort(server, args.migration) + + +@utils.arg( + '--all-tenants', + action='store_const', + const=1, + default=0, + help=_('Reset state server(s) in another tenant by name (Admin only).')) +@utils.arg( + 'server', metavar='', nargs='+', + help=_('Name or ID of server(s).')) +@utils.arg( + '--active', action='store_const', dest='state', + default='error', const='active', + help=_('Request the server be reset to "active" state instead ' + 'of "error" state (the default).')) +def do_reset_state(cs, args): + """Reset the state of a server.""" + failure_flag = False + find_args = {'all_tenants': args.all_tenants} + + for server in args.server: + try: + _find_server(cs, server, **find_args).reset_state(args.state) + msg = "Reset state for server %s succeeded; new state is %s" + print(msg % (server, args.state)) + except Exception as e: + failure_flag = True + msg = "Reset state for server %s failed: %s" % (server, e) + print(msg) + + if failure_flag: + msg = "Unable to reset the state for the specified server(s)." + raise exceptions.CommandError(msg) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_reset_network(cs, args): + """Reset network of a server.""" + _find_server(cs, args.server).reset_network() + + +@utils.arg( + '--host', + metavar='', + default=None, + help=_('Name of host.')) +@utils.arg( + '--binary', + metavar='', + default=None, + help=_('Service binary.')) +def do_service_list(cs, args): + """Show a list of all running services. Filter by host & binary.""" + result = cs.services.list(host=args.host, binary=args.binary) + columns = ["Id", "Binary", "Host", "Zone", "Status", + "State", "Updated_at", "Disabled Reason"] + if cs.api_version >= api_versions.APIVersion('2.11'): + columns.append("Forced down") + + utils.print_list(result, columns) + + +# Before microversion 2.53, the service was identified using it's host/binary +# values. +@api_versions.wraps('2.0', '2.52') +@utils.arg('host', metavar='', help=_('Name of host.')) +def do_service_enable(cs, args): + """Enable the service.""" + result = cs.services.enable(args.host, 'nova-compute') + utils.print_list([result], ['Host', 'Binary', 'Status']) + + +# Starting in microversion 2.53, the service is identified by UUID ID. +@api_versions.wraps('2.53') +@utils.arg('id', metavar='', help=_('ID of the service as a UUID.')) +def do_service_enable(cs, args): + """Enable the service.""" + result = cs.services.enable(args.id) + utils.print_list([result], ['ID', 'Host', 'Binary', 'Status']) + + +# Before microversion 2.53, the service was identified using it's host/binary +# values. +@api_versions.wraps('2.0', '2.52') +@utils.arg('host', metavar='', help=_('Name of host.')) +@utils.arg( + '--reason', + metavar='', + help=_('Reason for disabling service.')) +def do_service_disable(cs, args): + """Disable the service.""" + if args.reason: + 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, 'nova-compute') + utils.print_list([result], ['Host', 'Binary', 'Status']) + + +# Starting in microversion 2.53, the service is identified by UUID ID. +@api_versions.wraps('2.53') +@utils.arg('id', metavar='', help=_('ID of the service as a UUID.')) +@utils.arg( + '--reason', + metavar='', + help=_('Reason for disabling the service.')) +def do_service_disable(cs, args): + """Disable the service.""" + if args.reason: + result = cs.services.disable_log_reason(args.id, args.reason) + utils.print_list( + [result], ['ID', 'Host', 'Binary', 'Status', 'Disabled Reason']) + else: + result = cs.services.disable(args.id) + utils.print_list([result], ['ID', 'Host', 'Binary', 'Status']) + + +# Before microversion 2.53, the service was identified using it's host/binary +# values. +@api_versions.wraps("2.11", "2.52") +@utils.arg('host', metavar='', help=_('Name of host.')) +@utils.arg( + '--unset', + dest='force_down', + help=_("Unset the force state down of service."), + action='store_false', + default=True) +def do_service_force_down(cs, args): + """Force service to down.""" + result = cs.services.force_down(args.host, 'nova-compute', args.force_down) + utils.print_list([result], ['Host', 'Binary', 'Forced down']) + + +# Starting in microversion 2.53, the service is identified by UUID ID. +@api_versions.wraps('2.53') +@utils.arg('id', metavar='', help=_('ID of the service as a UUID.')) +@utils.arg( + '--unset', + dest='force_down', + help=_("Unset the forced_down state of the service."), + action='store_false', + default=True) +def do_service_force_down(cs, args): + """Force service to down.""" + result = cs.services.force_down(args.id, args.force_down) + utils.print_list([result], ['ID', 'Host', 'Binary', 'Forced down']) + + +# Before microversion 2.53, the service was identified using it's host/binary +# values. +@api_versions.wraps('2.0', '2.52') +@utils.arg('id', metavar='', + 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. + + 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. + + 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): + """Get a hypervisor by name or ID.""" + return utils.find_resource(cs.hypervisors, hypervisor) + + +def _do_hypervisor_list(cs, matching=None, limit=None, marker=None): + columns = ['ID', 'Hypervisor hostname', 'State', 'Status'] + if matching: + utils.print_list(cs.hypervisors.search(matching), columns) + else: + params = {} + if limit is not None: + params['limit'] = limit + if marker is not None: + params['marker'] = marker + # Since we're not outputting detail data, choose + # detailed=False for server-side efficiency + utils.print_list(cs.hypervisors.list(False, **params), columns) + + +@api_versions.wraps("2.0", "2.32") +@utils.arg( + '--matching', + metavar='', + default=None, + help=_('List hypervisors matching the given (or pattern).')) +def do_hypervisor_list(cs, args): + """List hypervisors.""" + _do_hypervisor_list(cs, matching=args.matching) + + +@api_versions.wraps("2.33") +@utils.arg( + '--matching', + metavar='', + default=None, + help=_('List hypervisors matching the given (or pattern). ' + 'If matching is used limit and marker options will be ignored.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last hypervisor of the previous page; displays list of ' + 'hypervisors after "marker".')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_("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.")) +def do_hypervisor_list(cs, args): + """List hypervisors.""" + _do_hypervisor_list( + cs, matching=args.matching, limit=args.limit, marker=args.marker) + + +@utils.arg( + 'hostname', + metavar='', + help=_('The hypervisor hostname (or pattern) to search for.')) +def do_hypervisor_servers(cs, args): + """List servers belonging to specific hypervisors.""" + hypers = cs.hypervisors.search(args.hostname, servers=True) + + class InstanceOnHyper(object): + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + # Massage the result into a list to be displayed + instances = [] + for hyper in hypers: + hyper_host = hyper.hypervisor_hostname + hyper_id = hyper.id + if hasattr(hyper, 'servers'): + instances.extend([InstanceOnHyper(id=serv['uuid'], + name=serv['name'], + hypervisor_hostname=hyper_host, + hypervisor_id=hyper_id) + for serv in hyper.servers]) + + # Output the data + utils.print_list(instances, ['ID', 'Name', 'Hypervisor ID', + 'Hypervisor Hostname']) + + +@utils.arg( + 'hypervisor', + metavar='', + help=_('Name or ID of the hypervisor. Starting with microversion 2.53 ' + 'the ID must be a UUID.')) +@utils.arg( + '--wrap', dest='wrap', metavar='', default=40, + help=_('Wrap the output to a specified length. ' + 'Default is 40 or 0 to disable')) +def do_hypervisor_show(cs, args): + """Display the details of the specified hypervisor.""" + hyper = _find_hypervisor(cs, args.hypervisor) + utils.print_dict(utils.flatten_dict(hyper.to_dict()), wrap=int(args.wrap)) + + +@utils.arg( + 'hypervisor', + metavar='', + help=_('Name or ID of the hypervisor. Starting with microversion 2.53 ' + 'the ID must be a UUID.')) +def do_hypervisor_uptime(cs, args): + """Display the uptime of the specified hypervisor.""" + hyper = _find_hypervisor(cs, args.hypervisor) + hyper = cs.hypervisors.uptime(hyper) + + # Output the uptime information + 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', + dest='port', + action='store', + type=int, + default=22, + help=_('Optional flag to indicate which port to use for ssh. ' + '(Default=22)')) +@utils.arg( + '--private', + dest='private', + action='store_true', + default=False, + help=argparse.SUPPRESS) +@utils.arg( + '--address-type', + dest='address_type', + action='store', + type=str, + default='floating', + help=_('Optional flag to indicate which IP type to use. Possible values ' + 'includes fixed and floating (the Default).')) +@utils.arg( + '--network', metavar='', + help=_('Network to use for the ssh.'), default=None) +@utils.arg( + '--ipv6', + dest='ipv6', + action='store_true', + default=False, + help=_('Optional flag to indicate whether to use an IPv6 address ' + 'attached to a server. (Defaults to IPv4 address)')) +@utils.arg( + '--login', metavar='', help=_('Login to use.'), + default="root") +@utils.arg( + '-i', '--identity', + dest='identity', + help=_('Private key file, same as the -i option to the ssh command.'), + default='') +@utils.arg( + '--extra-opts', + dest='extra', + help=_('Extra options to pass to ssh. see: man ssh.'), + default='') +def do_ssh(cs, args): + """SSH into a server.""" + if '@' in args.server: + user, server = args.server.split('@', 1) + args.login = user + args.server = server + + addresses = _find_server(cs, args.server).addresses + address_type = "fixed" if args.private else args.address_type + version = 6 if args.ipv6 else 4 + pretty_version = 'IPv%d' % version + + # Select the network to use. + if args.network: + network_addresses = addresses.get(args.network) + if not network_addresses: + msg = _("Server '%(server)s' is not attached to network " + "'%(network)s'") + raise exceptions.ResourceNotFound( + msg % {'server': args.server, 'network': args.network}) + else: + if len(addresses) > 1: + msg = _("Server '%(server)s' is attached to more than one network." + " Please pick the network to use.") + raise exceptions.CommandError(msg % {'server': args.server}) + elif not addresses: + msg = _("Server '%(server)s' is not attached to any network.") + raise exceptions.CommandError(msg % {'server': args.server}) + else: + network_addresses = list(addresses.values())[0] + + # Select the address in the selected network. + # If the extension is not present, we assume the address to be floating. + match = lambda addr: all(( + addr.get('version') == version, + addr.get('OS-EXT-IPS:type', 'floating') == address_type)) + matching_addresses = [address.get('addr') + for address in network_addresses if match(address)] + if not any(matching_addresses): + msg = _("No address that would match network '%(network)s'" + " and type '%(address_type)s' of version %(pretty_version)s " + "has been found for server '%(server)s'.") + raise exceptions.ResourceNotFound(msg % { + 'network': args.network, 'address_type': address_type, + 'pretty_version': pretty_version, 'server': args.server}) + elif len(matching_addresses) > 1: + msg = _("More than one %(pretty_version)s %(address_type)s address " + "found.") + raise exceptions.CommandError(msg % {'pretty_version': pretty_version, + 'address_type': address_type}) + else: + ip_address = matching_addresses[0] + + identity = '-i %s' % args.identity if len(args.identity) else '' + + 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) # nosec: B605 + + +# NOTE(mriedem): In the 2.50 microversion, the os-quota-class-sets API +# will return the server_groups and server_group_members, but no longer +# 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', + 'injected_file_path_bytes', 'key_pairs', + 'security_groups', 'security_group_rules', + 'server_groups', 'server_group_members'] + + +def _quota_show(quotas): + class FormattedQuota(object): + def __init__(self, key, value): + setattr(self, 'quota', key) + setattr(self, 'limit', value) + + quota_list = [] + for resource in _quota_resources: + try: + quota = FormattedQuota(resource, getattr(quotas, resource)) + quota_list.append(quota) + except AttributeError: + pass + columns = ['Quota', 'Limit'] + utils.print_list(quota_list, columns) + + +def _quota_update(manager, identifier, args): + updates = {} + for resource in _quota_resources: + val = getattr(args, resource, None) + if val is not None: + updates[resource] = val + + if updates: + # default value of force is None to make sure this client + # will be compatible with old nova server + force_update = getattr(args, 'force', None) + user_id = getattr(args, 'user', None) + if isinstance(manager, quotas.QuotaSetManager): + manager.update(identifier, force=force_update, user_id=user_id, + **updates) + else: + manager.update(identifier, **updates) + + +@utils.arg( + '--tenant', + metavar='', + default=None, + help=_('ID of tenant to list the quotas for.')) +@utils.arg( + '--user', + metavar='', + default=None, + help=_('ID of user to list the quotas for.')) +@utils.arg( + '--detail', + action='store_true', + default=False, + help=_('Show detailed info (limit, reserved, in-use).')) +def do_quota_show(cs, args): + """List the quotas for a tenant/user.""" + + if args.tenant: + project_id = args.tenant + elif isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + project_id = auth.get_auth_ref(cs.client.session).project_id + else: + project_id = cs.client.tenant_id + + _quota_show(cs.quotas.get(project_id, user_id=args.user, + detail=args.detail)) + + +@utils.arg( + '--tenant', + metavar='', + default=None, + help=_('ID of tenant to list the default quotas for.')) +def do_quota_defaults(cs, args): + """List the default quotas for a tenant.""" + + if args.tenant: + project_id = args.tenant + elif isinstance(cs.client, client.SessionClient): + auth = cs.client.auth + project_id = auth.get_auth_ref(cs.client.session).project_id + else: + project_id = cs.client.tenant_id + + _quota_show(cs.quotas.defaults(project_id)) + + +@api_versions.wraps("2.0", "2.35") +@utils.arg( + 'tenant', + metavar='', + help=_('ID of tenant to set the quotas for.')) +@utils.arg( + '--user', + metavar='', + default=None, + help=_('ID of user to set the quotas for.')) +@utils.arg( + '--instances', + metavar='', + type=int, default=None, + help=_('New value for the "instances" quota.')) +@utils.arg( + '--cores', + metavar='', + type=int, default=None, + help=_('New value for the "cores" quota.')) +@utils.arg( + '--ram', + metavar='', + type=int, default=None, + help=_('New value for the "ram" quota.')) +@utils.arg( + '--floating-ips', + metavar='', + type=int, + default=None, + action=shell.DeprecatedAction, + help=_('New value for the "floating-ips" quota.')) +@utils.arg( + '--fixed-ips', + metavar='', + type=int, + default=None, + action=shell.DeprecatedAction, + help=_('New value for the "fixed-ips" quota.')) +@utils.arg( + '--metadata-items', + metavar='', + type=int, + default=None, + help=_('New value for the "metadata-items" quota.')) +@utils.arg( + '--injected-files', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-files" quota.')) +@utils.arg( + '--injected-file-content-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-content-bytes" quota.')) +@utils.arg( + '--injected-file-path-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-path-bytes" quota.')) +@utils.arg( + '--key-pairs', + metavar='', + type=int, + default=None, + help=_('New value for the "key-pairs" quota.')) +@utils.arg( + '--security-groups', + metavar='', + type=int, + default=None, + action=shell.DeprecatedAction, + help=_('New value for the "security-groups" quota.')) +@utils.arg( + '--security-group-rules', + metavar='', + type=int, + default=None, + action=shell.DeprecatedAction, + help=_('New value for the "security-group-rules" quota.')) +@utils.arg( + '--server-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "server-groups" quota.')) +@utils.arg( + '--server-group-members', + metavar='', + type=int, + default=None, + help=_('New value for the "server-group-members" quota.')) +@utils.arg( + '--force', + dest='force', + action="store_true", + default=None, + help=_('Whether force update the quota even if the already used and ' + 'reserved exceeds the new quota.')) +def do_quota_update(cs, args): + """Update the quotas for a tenant/user.""" + + _quota_update(cs.quotas, args.tenant, 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', + metavar='', + help=_('ID of tenant to set the quotas for.')) +@utils.arg( + '--user', + metavar='', + default=None, + help=_('ID of user to set the quotas for.')) +@utils.arg( + '--instances', + metavar='', + type=int, default=None, + help=_('New value for the "instances" quota.')) +@utils.arg( + '--cores', + metavar='', + type=int, default=None, + help=_('New value for the "cores" quota.')) +@utils.arg( + '--ram', + metavar='', + type=int, default=None, + help=_('New value for the "ram" quota.')) +@utils.arg( + '--metadata-items', + metavar='', + type=int, + default=None, + help=_('New value for the "metadata-items" quota.')) +@utils.arg( + '--injected-files', + metavar='', + type=int, + default=None, + 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.'), + 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.'), + start_version='2.36', end_version='2.56') +@utils.arg( + '--key-pairs', + metavar='', + type=int, + default=None, + help=_('New value for the "key-pairs" quota.')) +@utils.arg( + '--server-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "server-groups" quota.')) +@utils.arg( + '--server-group-members', + metavar='', + type=int, + default=None, + help=_('New value for the "server-group-members" quota.')) +@utils.arg( + '--force', + dest='force', + action="store_true", + default=None, + help=_('Whether force update the quota even if the already used and ' + 'reserved exceeds the new quota.')) +def do_quota_update(cs, args): + """Update the quotas for a tenant/user.""" + + _quota_update(cs.quotas, args.tenant, args) + + +@utils.arg( + '--tenant', + metavar='', + required=True, + help=_('ID of tenant to delete quota for.')) +@utils.arg( + '--user', + metavar='', + help=_('ID of user to delete quota for.')) +def do_quota_delete(cs, args): + """Delete quota for a tenant/user so their quota will Revert + back to default. + """ + + cs.quotas.delete(args.tenant, user_id=args.user) + + +@utils.arg( + 'class_name', + metavar='', + help=_('Name of quota class to list the quotas for.')) +def do_quota_class_show(cs, args): + """List the quotas for a quota class.""" + + _quota_show(cs.quota_classes.get(args.class_name)) + + +@api_versions.wraps("2.0", "2.49") +@utils.arg( + 'class_name', + metavar='', + help=_('Name of quota class to set the quotas for.')) +@utils.arg( + '--instances', + metavar='', + type=int, default=None, + help=_('New value for the "instances" quota.')) +@utils.arg( + '--cores', + metavar='', + type=int, default=None, + help=_('New value for the "cores" quota.')) +@utils.arg( + '--ram', + metavar='', + type=int, default=None, + help=_('New value for the "ram" quota.')) +@utils.arg( + '--floating-ips', + metavar='', + type=int, + default=None, + action=shell.DeprecatedAction, + help=_('New value for the "floating-ips" quota.')) +@utils.arg( + '--fixed-ips', + metavar='', + type=int, + default=None, + action=shell.DeprecatedAction, + help=_('New value for the "fixed-ips" quota.')) +@utils.arg( + '--metadata-items', + metavar='', + type=int, + default=None, + help=_('New value for the "metadata-items" quota.')) +@utils.arg( + '--injected-files', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-files" quota.')) +@utils.arg( + '--injected-file-content-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-content-bytes" quota.')) +@utils.arg( + '--injected-file-path-bytes', + metavar='', + type=int, + default=None, + help=_('New value for the "injected-file-path-bytes" quota.')) +@utils.arg( + '--key-pairs', + metavar='', + type=int, + default=None, + help=_('New value for the "key-pairs" quota.')) +@utils.arg( + '--security-groups', + metavar='', + type=int, + default=None, + action=shell.DeprecatedAction, + help=_('New value for the "security-groups" quota.')) +@utils.arg( + '--security-group-rules', + metavar='', + type=int, + default=None, + action=shell.DeprecatedAction, + help=_('New value for the "security-group-rules" quota.')) +@utils.arg( + '--server-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "server-groups" quota.')) +@utils.arg( + '--server-group-members', + metavar='', + type=int, + default=None, + help=_('New value for the "server-group-members" quota.')) +def do_quota_class_update(cs, args): + """Update the quotas for a quota class.""" + + _quota_update(cs.quota_classes, args.class_name, 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', + metavar='', + help=_('Name of quota class to set the quotas for.')) +@utils.arg( + '--instances', + metavar='', + type=int, default=None, + help=_('New value for the "instances" quota.')) +@utils.arg( + '--cores', + metavar='', + type=int, default=None, + help=_('New value for the "cores" quota.')) +@utils.arg( + '--ram', + metavar='', + type=int, default=None, + help=_('New value for the "ram" quota.')) +@utils.arg( + '--metadata-items', + metavar='', + type=int, + default=None, + help=_('New value for the "metadata-items" quota.')) +@utils.arg( + '--injected-files', + metavar='', + type=int, + default=None, + 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.'), + 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.'), + start_version='2.50', end_version='2.56') +@utils.arg( + '--key-pairs', + metavar='', + type=int, + default=None, + help=_('New value for the "key-pairs" quota.')) +@utils.arg( + '--server-groups', + metavar='', + type=int, + default=None, + help=_('New value for the "server-groups" quota.')) +@utils.arg( + '--server-group-members', + metavar='', + type=int, + default=None, + help=_('New value for the "server-group-members" quota.')) +def do_quota_class_update(cs, args): + """Update the quotas for a quota class.""" + + _quota_update(cs.quota_classes, args.class_name, args) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg( + 'host', metavar='', nargs='?', + help=_("Name or ID of the target host. " + "If no host is specified, the scheduler will choose one.")) +@utils.arg( + '--password', + dest='password', + metavar='', + help=_("Set the provided admin password on the evacuated server. Not" + " applicable if the server is on shared storage.")) +@utils.arg( + '--on-shared-storage', + dest='on_shared_storage', + action="store_true", + default=False, + help=_('Specifies whether server files are located on shared storage.'), + start_version='2.0', + end_version='2.13') +@utils.arg( + '--force', + dest='force', + action='store_true', + default=False, + 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) + update_kwargs = {} + if on_shared_storage is not None: + update_kwargs['on_shared_storage'] = on_shared_storage + if force: + update_kwargs['force'] = force + res = server.evacuate(host=args.host, password=args.password, + **update_kwargs)[1] + if isinstance(res, dict): + utils.print_dict(res) + + +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): + for col in columns: + key = col.lower().replace(" ", "_") + if hasattr(interface, key): + setattr(self, key, getattr(interface, key)) + self.ip_addresses = ",".join([fip['ip_address'] + for fip in interface.fixed_ips]) + utils.print_list([FormattedInterface(i) for i in interfaces], columns) + + +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_interface_list(cs, args): + """List interfaces attached to a server.""" + server = _find_server(cs, args.server) + + res = server.interface_list() + + # 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.')) +@utils.arg( + '--port-id', + metavar='', + help=_('Port ID.'), + dest="port_id") +@utils.arg( + '--net-id', + metavar='', + help=_('Network ID'), + default=None, dest="net_id") +@utils.arg( + '--fixed-ip', + metavar='', + help=_('Requested fixed IP.'), + default=None, dest="fixed_ip") +@utils.arg( + '--tag', + metavar='', + default=None, + dest="tag", + help=_('Tag for the attached interface.'), + start_version="2.49") +def do_interface_attach(cs, args): + """Attach a network interface to a server.""" + server = _find_server(cs, args.server) + + update_kwargs = {} + if 'tag' in args and args.tag: + update_kwargs['tag'] = args.tag + + 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.')) +@utils.arg('port_id', metavar='', help=_('Port ID.')) +def do_interface_detach(cs, args): + """Detach a network interface from a server.""" + server = _find_server(cs, args.server) + server.interface_detach(args.port_id) + + +@api_versions.wraps("2.17") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_trigger_crash_dump(cs, args): + """Trigger crash dump in an instance.""" + server = _find_server(cs, args.server) + + server.trigger_crash_dump() + + +def _treeizeAvailabilityZone(zone): + """Build a tree view for availability zones.""" + AvailabilityZone = availability_zones.AvailabilityZone + + az = AvailabilityZone(zone.manager, zone.to_dict(), zone._loaded) + result = [] + + # Zone tree view item + az.zoneName = zone.zoneName + az.zoneState = ('available' + if zone.zoneState['available'] else 'not available') + az.set_info('zoneName', az.zoneName) + az.set_info('zoneState', az.zoneState) + result.append(az) + + if zone.hosts is not None: + zone_hosts = sorted(zone.hosts.items(), key=lambda x: x[0]) + for (host, services) in zone_hosts: + # Host tree view item + az = AvailabilityZone(zone.manager, zone.to_dict(), zone._loaded) + az.zoneName = '|- %s' % host + az.zoneState = '' + az.set_info('zoneName', az.zoneName) + az.set_info('zoneState', az.zoneState) + result.append(az) + + for (svc, state) in services.items(): + # Service tree view item + az = AvailabilityZone(zone.manager, zone.to_dict(), + zone._loaded) + az.zoneName = '| |- %s' % svc + az.zoneState = '%s %s %s' % ( + 'enabled' if state['active'] else 'disabled', + ':-)' if state['available'] else 'XXX', + state['updated_at']) + az.set_info('zoneName', az.zoneName) + az.set_info('zoneState', az.zoneState) + result.append(az) + return result + + +@utils.service_type('compute') +def do_availability_zone_list(cs, _args): + """List all the availability zones.""" + try: + availability_zones = cs.availability_zones.list() + except exceptions.Forbidden as e: # policy doesn't allow probably + try: + availability_zones = cs.availability_zones.list(detailed=False) + except Exception: + raise e + + result = [] + for zone in availability_zones: + result += _treeizeAvailabilityZone(zone) + _translate_availability_zone_keys(result) + utils.print_list(result, ['Name', 'Status'], + sortby_index=None) + + +def _print_server_group_details(cs, server_group): + 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) + + +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_("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.")) +@utils.arg( + '--offset', + dest='offset', + metavar='', + type=int, + default=None, + help=_('The offset of groups list to display; use with limit to ' + 'return a slice of server groups.')) +@utils.arg( + '--all-projects', + dest='all_projects', + action='store_true', + default=False, + help=_('Display server groups from all projects (Admin only).')) +def do_server_group_list(cs, args): + """Print a list of all server groups.""" + server_groups = cs.server_groups.list(all_projects=args.all_projects, + limit=args.limit, + offset=args.offset) + _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='', + 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, + policies=args.policy) + _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='', + nargs='+', + help=_("Unique ID(s) of the server group to delete.")) +def do_server_group_delete(cs, args): + """Delete specific server group(s).""" + failure_count = 0 + + for sg in args.id: + try: + cs.server_groups.delete(sg) + print(_("Server group %s has been successfully deleted.") % sg) + except Exception as e: + failure_count += 1 + print(_("Delete for server group %(sg)s failed: %(e)s") % + {'sg': sg, 'e': e}) + if failure_count == len(args.id): + raise exceptions.CommandError(_("Unable to delete any of the " + "specified server groups.")) + + +@utils.arg( + 'id', + metavar='', + help=_("Unique ID of the server group to get.")) +def do_server_group_get(cs, args): + """Get a specific server group.""" + server_group = cs.server_groups.get(args.id) + _print_server_group_details(cs, [server_group]) + + +def do_version_list(cs, args): + """List all API versions.""" + result = cs.versions.list() + if 'min_version' in dir(result[0]): + columns = ["Id", "Status", "Updated", "Min Version", "Version"] + else: + columns = ["Id", "Status", "Updated"] + + print(_("Client supported API versions:")) + print(_("Minimum version %(v)s") % + {'v': novaclient.API_MIN_VERSION.get_string()}) + print(_("Maximum version %(v)s") % + {'v': novaclient.API_MAX_VERSION.get_string()}) + + print(_("\nServer supported API versions:")) + utils.print_list(result, columns) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_server_tag_list(cs, args): + """Get list of tags from a server.""" + server = _find_server(cs, args.server) + tags = server.tag_list() + formatters = {'Tag': lambda o: o} + utils.print_list(tags, ['Tag'], formatters=formatters) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('tag', metavar='', nargs='+', help=_('Tag(s) to add.')) +def do_server_tag_add(cs, args): + """Add one or more tags to a server.""" + server = _find_server(cs, args.server) + utils.do_action_on_many( + lambda t: server.add_tag(t), + args.tag, + _("Request to add tag %s to specified server has been accepted."), + _("Unable to add the specified tag to the server.")) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('tags', metavar='', nargs='+', help=_('Tag(s) to set.')) +def do_server_tag_set(cs, args): + """Set list of tags to a server.""" + server = _find_server(cs, args.server) + server.set_tags(args.tags) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('tag', metavar='', nargs='+', help=_('Tag(s) to delete.')) +def do_server_tag_delete(cs, args): + """Delete one or more tags from a server.""" + server = _find_server(cs, args.server) + utils.do_action_on_many( + lambda t: server.delete_tag(t), + args.tag, + _("Request to delete tag %s from specified server has been accepted."), + _("Unable to delete the specified tag from the server.")) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_server_tag_delete_all(cs, args): + """Delete all tags from a server.""" + server = _find_server(cs, args.server) + server.delete_all_tags() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_force_delete(cs, args): + """Force delete a server.""" + utils.find_resource(cs.servers, args.server).force_delete() + + +@utils.arg('server', metavar='', help='Name or ID of server.') +def do_restore(cs, args): + """Restore a soft-deleted server.""" + utils.find_resource(cs.servers, args.server, deleted=True).restore() + + +class EvacuateHostResponse(base.Resource): + pass + + +def _server_evacuate(cs, server, args): + success = True + error_message = "" + try: + 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) + elif api_versions.APIVersion("2.14") <= cs.api_version: + # if microversion 2.14 - 2.28 + cs.servers.evacuate(server=server['uuid'], host=args.target_host) + else: + # else microversion 2.0 - 2.13 + on_shared_storage = getattr(args, 'on_shared_storage', None) + cs.servers.evacuate(server=server['uuid'], + host=args.target_host, + on_shared_storage=on_shared_storage) + except Exception as e: + success = False + error_message = _("Error while evacuating instance: %s") % e + return EvacuateHostResponse(base.Manager, {"server_uuid": server['uuid'], + "evacuate_accepted": success, + "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 ' + 'want to evacuate from a specific host.') +@utils.arg( + '--target_host', + metavar='', + default=None, + help=_('Name of target host. If no host is specified the scheduler will ' + 'select a target.')) +@utils.arg( + '--on-shared-storage', + dest='on_shared_storage', + action="store_true", + default=False, + help=_('Specifies whether all instances files are on shared storage'), + start_version='2.0', + end_version='2.13') +@utils.arg( + '--force', + dest='force', + action='store_true', + default=False, + 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.""" + response = [] + 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): + class HostEvacuateLiveResponse(object): + def __init__(self, server_uuid, live_migration_accepted, + error_message): + self.server_uuid = server_uuid + self.live_migration_accepted = live_migration_accepted + self.error_message = error_message + success = True + error_message = "" + update_kwargs = {} + try: + # API >= 2.30 + if 'force' in args and args.force: + update_kwargs['force'] = args.force + # API 2.0->2.24 + if 'disk_over_commit' in args: + update_kwargs['disk_over_commit'] = args.disk_over_commit + cs.servers.live_migrate(server['uuid'], args.target_host, + args.block_migrate, **update_kwargs) + except Exception as e: + success = False + error_message = _("Error while live migrating instance: %s") % e + return HostEvacuateLiveResponse(server['uuid'], + success, + error_message) + + +@utils.arg('host', metavar='', + help='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.') +@utils.arg( + '--target-host', + metavar='', + default=None, + help=_('Name of target host. If no host is specified, the scheduler will ' + 'choose one.')) +@utils.arg( + '--block-migrate', + action='store_true', + default=False, + help=_('Enable block migration. (Default=False)'), + start_version="2.0", end_version="2.24") +@utils.arg( + '--block-migrate', + action='store_true', + default="auto", + help=_('Enable block migration. (Default=auto)'), + start_version="2.25") +@utils.arg( + '--disk-over-commit', + action='store_true', + default=False, + help=_('Enable disk overcommit.'), + start_version="2.0", end_version="2.24") +@utils.arg( + '--max-servers', + type=int, + dest='max_servers', + metavar='', + help='Maximum number of servers to live migrate simultaneously') +@utils.arg( + '--force', + dest='force', + action='store_true', + default=False, + 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 off the specified host + to other available hosts. + """ + response = [] + migrating = 0 + 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): + pass + + +def _server_migrate(cs, server): + success = True + error_message = "" + try: + cs.servers.migrate(server['uuid']) + except Exception as e: + success = False + error_message = _("Error while migrating instance: %s") % e + return HostServersMigrateResponse(base.Manager, + {"server_uuid": server['uuid'], + "migration_accepted": success, + "error_message": error_message}) + + +@utils.arg('host', metavar='', + 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. + """ + response = [] + 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( + 'server', + metavar='', + help=_('Name or UUID of the server to show actions for.'), + start_version="2.0", end_version="2.20") +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to show actions for. Only UUID can be ' + 'used to show actions for a deleted server.'), + start_version="2.21") +@utils.arg( + 'request_id', + metavar='', + help=_('Request ID of the action to get.')) +def do_instance_action(cs, args): + """Show an action.""" + if cs.api_version < api_versions.APIVersion("2.21"): + server = _find_server(cs, args.server) + else: + server = _find_server(cs, args.server, raise_if_notfound=False) + action_resource = cs.instance_action.get(server, args.request_id) + action = action_resource.to_dict() + if 'events' in action: + action['events'] = pprint.pformat(action['events']) + utils.print_dict(action) + + +@api_versions.wraps("2.0", "2.57") +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to list actions for.'), + start_version="2.0", end_version="2.20") +@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.'), + start_version="2.21") +def do_instance_action_list(cs, args): + """List actions on a server.""" + if cs.api_version < api_versions.APIVersion("2.21"): + server = _find_server(cs, args.server) + else: + server = _find_server(cs, args.server, raise_if_notfound=False) + actions = cs.instance_action.list(server) + utils.print_list(actions, + ['Action', 'Request_ID', 'Message', 'Start_Time'], + sortby_index=3) + + +@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='', + help='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.') +@utils.arg( + 'action', + metavar='', + choices=['set', 'delete'], + help=_("Actions: 'set' or 'delete'")) +@utils.arg( + 'metadata', + metavar='', + nargs='+', + 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.""" + for server in _hyper_servers(cs, args.host, args.strict): + metadata = _extract_metadata(args) + 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): + fields = ['Source Node', 'Dest Node', 'Source Compute', 'Dest Compute', + 'Dest Host', 'Status', 'Instance UUID', 'Old Flavor', + 'New Flavor', 'Created At', 'Updated At'] + + def old_flavor(migration): + return migration.old_instance_type_id + + def new_flavor(migration): + return migration.new_instance_type_id + + def migration_type(migration): + return migration.migration_type + + 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', + 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 .')) +@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.""" + 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 new file mode 100644 index 000000000..32fc55085 --- /dev/null +++ b/novaclient/v2/usage.py @@ -0,0 +1,132 @@ +# +# 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. + +""" +Usage interface. +""" + +import oslo_utils + +from novaclient import api_versions +from novaclient import base + + +class Usage(base.Resource): + """ + Usage contains information about a tenant's physical resource usage + """ + def __repr__(self): + return "" + + def get(self): + fmt = '%Y-%m-%dT%H:%M:%S.%f' + if self.start and self.stop and self.tenant_id: + # set_loaded() first ... so if we have to bail, we know we tried. + self.set_loaded(True) + start = oslo_utils.timeutils.parse_strtime(self.start, fmt=fmt) + stop = oslo_utils.timeutils.parse_strtime(self.stop, fmt=fmt) + + new = self.manager.get(self.tenant_id, start, stop) + if new: + self._add_details(new._info) + self.append_request_ids(new.request_ids) + + +class UsageManager(base.ManagerWithFind): + """ + Manage :class:`Usage` resources. + """ + resource_class = Usage + usage_prefix = 'os-simple-tenant-usage' + + def _usage_query(self, start, end, marker=None, limit=None, detailed=None): + query = "?start=%s&end=%s" % (start.isoformat(), end.isoformat()) + if limit: + query = "%s&limit=%s" % (query, int(limit)) + if marker: + query = "%s&marker=%s" % (query, marker) + if detailed is not None: + query = "%s&detailed=%s" % (query, int(bool(detailed))) + return query + + @api_versions.wraps("2.0", "2.39") + def list(self, start, end, detailed=False): + """ + Get usage for all tenants + + :param start: :class:`datetime.datetime` Start date in UTC + :param end: :class:`datetime.datetime` End date in UTC + :param detailed: Whether to include information about each + instance whose usage is part of the report + :rtype: list of :class:`Usage`. + """ + query_string = self._usage_query(start, end, detailed=detailed) + url = '/%s%s' % (self.usage_prefix, query_string) + return self._list(url, 'tenant_usages') + + @api_versions.wraps("2.40") + def list(self, start, end, detailed=False, marker=None, limit=None): + """ + Get usage for all tenants + + :param start: :class:`datetime.datetime` Start date in UTC + :param end: :class:`datetime.datetime` End date in UTC + :param detailed: Whether to include information about each + instance whose usage is part of the report + :param marker: Begin returning usage data for instances that appear + 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). 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) + url = '/%s%s' % (self.usage_prefix, query_string) + return self._list(url, 'tenant_usages') + + @api_versions.wraps("2.0", "2.39") + def get(self, tenant_id, start, end): + """ + Get usage for a specific tenant. + + :param tenant_id: Tenant ID to fetch usage for + :param start: :class:`datetime.datetime` Start date in UTC + :param end: :class:`datetime.datetime` End date in UTC + :rtype: :class:`Usage` + """ + query_string = self._usage_query(start, end) + url = '/%s/%s%s' % (self.usage_prefix, tenant_id, query_string) + return self._get(url, 'tenant_usage') + + @api_versions.wraps("2.40") + def get(self, tenant_id, start, end, marker=None, limit=None): + """ + Get usage for a specific tenant. + + :param tenant_id: Tenant ID to fetch usage for + :param start: :class:`datetime.datetime` Start date in UTC + :param end: :class:`datetime.datetime` End date in UTC + :param marker: Begin returning usage data for instances that appear + 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). 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) + url = '/%s/%s%s' % (self.usage_prefix, tenant_id, query_string) + return self._get(url, 'tenant_usage') diff --git a/novaclient/v2/versions.py b/novaclient/v2/versions.py new file mode 100644 index 000000000..b9895f911 --- /dev/null +++ b/novaclient/v2/versions.py @@ -0,0 +1,102 @@ +# Copyright 2014 NEC Corporation. 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. + +""" +version interface +""" + +from urllib import parse + +from novaclient import base +from novaclient import exceptions as exc + + +class Version(base.Resource): + """ + Compute REST API information + """ + def __repr__(self): + return "" + + +class VersionManager(base.ManagerWithFind): + resource_class = Version + + def _get_current(self): + """Returns info about current version.""" + + # TODO(sdague): we've now got to make up to 3 HTTP requests to + # determine what version we are running, due to differences in + # deployments and versions. We really need to cache the + # results of this per endpoint and keep the results of it for + # some reasonable TTL (like 24 hours) to reduce our round trip + # traffic. + try: + # Assume that the value of get_endpoint() is something + # we can get the version of. This is a 404 for Nova < + # Mitaka if the service catalog contains project_id. + # + # TODO(sdague): add microversion for when this will + # change + url = "%s" % self.api.client.get_endpoint() + return self._get(url, "version") + except exc.NotFound: + # If that's a 404, we can instead try hacking together + # an endpoint root url by chopping off the last 2 /s. + # This is kind of gross, but we've had this baked in + # so long people got used to this hard coding. + # + # NOTE(sdague): many service providers don't really + # implement GET / in the expected way, if we do a GET + # /v2 that's actually a 300 redirect to + # /v2/... because of how paste works. So adding the + # end slash is really important. + url = "%s/" % url.rsplit("/", 1)[0] + return self._get(url, "version") + + def get_current(self): + try: + return self._get_current() + except exc.Unauthorized: + # NOTE(sdague): RAX's repose configuration blocks access to the + # versioned endpoint, which is definitely non-compliant behavior. + # However, there is no defcore test for this yet. Remove this code + # block once we land things in defcore. + return None + + def list(self): + """List all versions.""" + + endpoint = self.api.client.get_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} + # 2. starting from microversion 2.18 project-id is not included: + # http://{host}:{optional_port}/v{2 or 2.1} + # 3. under wsgi: + # http://{host}:{optional_port}/compute/v{2 or 2.1} + if (url.path.endswith("v2") or "/v2/" in url.path or + url.path.endswith("v2.1") or "/v2.1/" in url.path): + # this way should handle all 3 possible formats + path = url.path[:url.path.rfind("/v2")] + version_url = '%s://%s%s' % (url.scheme, url.netloc, path) + else: + # NOTE(andreykurilin): probably, it is one of the next cases: + # * https://compute.example.com/ + # * https://example.com/compute + # leave as is without cropping. + version_url = endpoint + + return self._list(version_url, "versions") diff --git a/novaclient/v2/volumes.py b/novaclient/v2/volumes.py new file mode 100644 index 000000000..93fdd8682 --- /dev/null +++ b/novaclient/v2/volumes.py @@ -0,0 +1,221 @@ +# Copyright 2011 Denali Systems, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Volume interface +""" +import warnings + +from novaclient import api_versions +from novaclient import base + + +class Volume(base.Resource): + """ + A volume is an extra block level storage to the OpenStack + instances. + """ + NAME_ATTR = 'display_name' + + def __repr__(self): + return "" % self.id + + +class VolumeManager(base.Manager): + """ + Manage :class:`Volume` resources. This is really about volume attachments. + """ + 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): + """ + 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) + :rtype: :class:`Volume` + """ + 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", "2.78") + def create_server_volume(self, server_id, volume_id, device=None, + tag=None): + """ + 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) + :rtype: :class:`Volume` + """ + 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. + + 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 + :rtype: :class:`Volume` + """ + body = {'volumeAttachment': {'volumeId': dest_volid}} + return self._update("/servers/%s/os-volume_attachments/%s" % + (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 + the given server ID + + :param server_id: The ID of the server + :param volume_id: The ID of the volume to attach + :rtype: :class:`Volume` + """ + + if attachment_id is not None and volume_id is not None: + raise TypeError("You cannot specify both volume_id " + "and attachment_id arguments.") + + elif attachment_id is not None: + warnings.warn("attachment_id argument " + "of volumes.get_server_volume " + "method is deprecated in favor " + "of volume_id.") + volume_id = attachment_id + + if volume_id is None: + raise TypeError("volume_id is required argument.") + + return self._get("/servers/%s/os-volume_attachments/%s" % (server_id, + volume_id,), "volumeAttachment") + + def get_server_volumes(self, server_id): + """ + Get a list of all the attached volumes for the given server ID + + :param server_id: The ID of the server + :rtype: list of :class:`Volume` + """ + return self._list("/servers/%s/os-volume_attachments" % server_id, + "volumeAttachments") + + def delete_server_volume(self, server_id, volume_id=None, + attachment_id=None): + """ + Detach a volume identified by the volume ID from the given server + + :param server_id: The ID of the server + :param volume_id: The ID of the volume to attach + :returns: An instance of novaclient.base.TupleWithMeta + """ + if attachment_id is not None and volume_id is not None: + raise TypeError("You cannot specify both volume_id " + "and attachment_id arguments.") + + elif attachment_id is not None: + warnings.warn("attachment_id argument " + "of volumes.delete_server_volume " + "method is deprecated in favor " + "of volume_id.") + volume_id = attachment_id + + if volume_id is None: + raise TypeError("volume_id is required argument.") + + return self._delete("/servers/%s/os-volume_attachments/%s" % + (server_id, volume_id,)) diff --git a/openstack-common.conf b/openstack-common.conf deleted file mode 100644 index 76f08056e..000000000 --- a/openstack-common.conf +++ /dev/null @@ -1,7 +0,0 @@ -[DEFAULT] - -# The list of modules to copy from openstack-common -modules=setup,timeutils - -# The base module to hold the copy of openstack.common -base=novaclient 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/.placeholder b/releasenotes/notes/.placeholder new file mode 100644 index 000000000..e69de29bb 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-osprofiler-support-cc9dd228242e9919.yaml b/releasenotes/notes/add-osprofiler-support-cc9dd228242e9919.yaml new file mode 100644 index 000000000..a956ae147 --- /dev/null +++ b/releasenotes/notes/add-osprofiler-support-cc9dd228242e9919.yaml @@ -0,0 +1,25 @@ +--- +prelude: > + OSprofiler support was added to the client. That makes + possible to trigger Nova operation trace generation from + the CLI. +features: + - A new ``--profile`` option was added to allow Nova + profiling from the CLI. If the user wishes to trace a + nova boot request he or she needs to type the following + command -- ``nova --profile boot --image + --flavor ``, where ``secret_key`` should match one + of the keys defined in nova.conf. As a result of this operation + additional information regarding ``trace_id`` will be + printed, that can be used to generate human-friendly + html report -- ``osprofiler trace show --html --out + trace.html``. + To enable profiling, user needs to have osprofiler + installed in the local environment via ``pip install osprofiler``. +security: + - OSprofiler support, that was added during the Ocata release cycle, + requires passing of trace information between various + OpenStack services. This information is signed by one of + the HMAC keys defined in nova.conf file. That means that + only someone who knows this key is able to send the proper + header to trigger profiling. 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/add-user-agent-string-db77210dfd3ec671.yaml b/releasenotes/notes/add-user-agent-string-db77210dfd3ec671.yaml new file mode 100644 index 000000000..0e1eeb0f5 --- /dev/null +++ b/releasenotes/notes/add-user-agent-string-db77210dfd3ec671.yaml @@ -0,0 +1,6 @@ +--- +features: + - novaclient now adds information about itself to the keystoneauth + user-agent. Adding information about wrapping libraries or consuming + applications can be found at + https://docs.openstack.org/developer/python-novaclient/api.html 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-deprecate-image-meta-proxy-api-1483b75cf73b021e.yaml b/releasenotes/notes/bp-deprecate-image-meta-proxy-api-1483b75cf73b021e.yaml new file mode 100644 index 000000000..6d8e76c11 --- /dev/null +++ b/releasenotes/notes/bp-deprecate-image-meta-proxy-api-1483b75cf73b021e.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - Starting from microversion 2.39 'image-metadata' proxy API in Nova is + deprecated and python API bindings will not work, although 'image-meta' + CLI commands will still work, because of the fallback on the CLI to + version 2.35. 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/clarify-project-id-variable-5832698315000438.yaml b/releasenotes/notes/clarify-project-id-variable-5832698315000438.yaml new file mode 100644 index 000000000..83182d1ce --- /dev/null +++ b/releasenotes/notes/clarify-project-id-variable-5832698315000438.yaml @@ -0,0 +1,13 @@ +--- +deprecations: + - Keyword argument **tenant_id** of novaclient.client.Client entry-point was + deprecated in favor of **project_id**. + - Keyword argument **tenant_name** of novaclient.client.Client entry-point + was deprecated in favor of **project_name**. +other: + - The meaning of 'project_id' variable of novaclient.client.Client + entry-point was not clear. In different cases it was used as ID or Name of + project (in terms of Keystone). The time to identify meaning is come, so + now project_id/tenant_id variables specifically mean Project ID (in terms + of Keystone) and project_name/tenant_name variables mean Project Name (in + terms of Keystone). 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-baremetal-d67f58a2986b3565.yaml b/releasenotes/notes/deprecate-baremetal-d67f58a2986b3565.yaml new file mode 100644 index 000000000..123334679 --- /dev/null +++ b/releasenotes/notes/deprecate-baremetal-d67f58a2986b3565.yaml @@ -0,0 +1,13 @@ +--- +deprecations: + - | + The following CLIs and python API bindings are now deprecated for removal: + + * nova baremetal-node-list + * nova baremetal-node-show + * nova baremetal-interface-list + + These will be removed in the first major python-novaclient release after + the Nova 15.0.0 Ocata release. Use python-ironicclient or + python-openstackclient for CLI and python-ironicclient or openstacksdk + for python API bindings. 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-certs-1558d8e3b7888938.yaml b/releasenotes/notes/deprecate-certs-1558d8e3b7888938.yaml new file mode 100644 index 000000000..fd7e7599f --- /dev/null +++ b/releasenotes/notes/deprecate-certs-1558d8e3b7888938.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - | + The ``nova x509-create-cert`` and ``nova x509-get-root-cert`` commands + and ``novaclient.v2.certs`` API binding are now deprecated and will be + removed in the first major release after the Nova server 16.0.0 Pike + release. 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-cloudpipe-670202797fdf97b6.yaml b/releasenotes/notes/deprecate-cloudpipe-670202797fdf97b6.yaml new file mode 100644 index 000000000..1c51ab297 --- /dev/null +++ b/releasenotes/notes/deprecate-cloudpipe-670202797fdf97b6.yaml @@ -0,0 +1,8 @@ +--- +deprecations: + - | + The os-cloudpipe API has been removed from Nova. As a result, the + ``nova cloudpipe-list``, ``nova cloudpipe-create``, and ``nova + cloudpipe-configure`` commands and the ``novaclient.v2.cloudpipe`` + API bindings are now deprecated, and will be removed in the first + major release after the Nova server 16.0.0 Pike release. diff --git a/releasenotes/notes/deprecate-connection-pool-arg-cef35346d5ebf40c.yaml b/releasenotes/notes/deprecate-connection-pool-arg-cef35346d5ebf40c.yaml new file mode 100644 index 000000000..e12e0d2f1 --- /dev/null +++ b/releasenotes/notes/deprecate-connection-pool-arg-cef35346d5ebf40c.yaml @@ -0,0 +1,3 @@ +--- +deprecations: + - The **connection_pool** variable is deprecated now and will be ignored. 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/deprecate-network-cli-f0a539528be594d3.yaml b/releasenotes/notes/deprecate-network-cli-f0a539528be594d3.yaml new file mode 100644 index 000000000..2d8c86a6d --- /dev/null +++ b/releasenotes/notes/deprecate-network-cli-f0a539528be594d3.yaml @@ -0,0 +1,67 @@ +--- +upgrade: + - | + The ability to update the following network-related resources via the + ``nova quota-update`` and ``nova quota-class-update`` commands is now + deprecated: + + * Fixed IPs + * Floating IPs + * Security Groups + * Security Group Rules + + By default the quota and limits CLIs will not update or show those + resources using microversion >= 2.36. You can still use them, however, by + specifying ``--os-compute-api-version 2.35``. Quota information for network + resources should be retrieved from python-neutronclient or + python-openstackclient. +deprecations: + - | + The following commands are now deprecated: + + * dns-create + * dns-create-private-domain + * dns-create-public-domain + * dns-delete + * dns-delete-domain + * dns-domains + * dns-list + * fixed-ip-get + * fixed-ip-reserve + * fixed-ip-unreserve + * floating-ip-create + * floating-ip-delete + * floating-ip-list + * floating-ip-pool-list + * floating-ip-bulk-create + * floating-ip-bulk-delete + * floating-ip-bulk-list + * network-create + * network-delete + * network-disassociate + * network-associate-host + * network-associate-project + * network-list + * network-show + * scrub + * secgroup-create + * secgroup-delete + * secgroup-list + * secgroup-update + * secgroup-add-group-rule + * secgroup-delete-group-rule + * secgroup-add-rule + * secgroup-delete-rule + * secgroup-list-rules + * secgroup-list-default-rules + * secgroup-add-default-rule + * secgroup-delete-default-rule + * tenant-network-create + * tenant-network-delete + * tenant-network-list + * tenant-network-show + + With the 2.36 microversion these will fail in the API. The CLI will + fallback to passing the 2.35 microversion to ease the transition. Network + resource information should be retrieved from python-neutronclient or + python-openstackclient. diff --git a/releasenotes/notes/deprecate-no-cache-arg-7814806b4f79c1b9.yaml b/releasenotes/notes/deprecate-no-cache-arg-7814806b4f79c1b9.yaml new file mode 100644 index 000000000..2fc9e4bfc --- /dev/null +++ b/releasenotes/notes/deprecate-no-cache-arg-7814806b4f79c1b9.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - novaclient.client.Client entry-point accepted two arguments with same + meaning (**no_cache** and **os_cache**). Since **os_cache** is more widely + used in our code, **no_cache** was deprecated. diff --git a/releasenotes/notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml b/releasenotes/notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml new file mode 100644 index 000000000..c513b6ff8 --- /dev/null +++ b/releasenotes/notes/deprecate-proxy-args-a3f4e224f7664ff8.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - The **proxy_tenant_id** and **proxy_token** arguments to the + novaclient.client.Client entry-point were never documented nor tested and + are now deprecated for removal in a future release. diff --git a/releasenotes/notes/deprecate-service-binary-arg-2d5c446f5a2409a7.yaml b/releasenotes/notes/deprecate-service-binary-arg-2d5c446f5a2409a7.yaml new file mode 100644 index 000000000..254b42d68 --- /dev/null +++ b/releasenotes/notes/deprecate-service-binary-arg-2d5c446f5a2409a7.yaml @@ -0,0 +1,10 @@ +--- +deprecations: + - | + The ``binary`` argument to the ``nova service-enable``, + ``nova service-disable``, and ``nova service-force-down`` commands has been + deprecated. The only binary that it makes sense to use is ``nova-compute`` + since disabling a service like ``nova-scheduler`` or ``nova-conductor`` + does not actually do anything, and starting in the 16.0.0 Pike release the + compute API will not be able to look up services other than + ``nova-compute`` for these operations. diff --git a/releasenotes/notes/deprecate-volume-service-name-arg-4c65e8866f9624dd.yaml b/releasenotes/notes/deprecate-volume-service-name-arg-4c65e8866f9624dd.yaml new file mode 100644 index 000000000..26de99b53 --- /dev/null +++ b/releasenotes/notes/deprecate-volume-service-name-arg-4c65e8866f9624dd.yaml @@ -0,0 +1,7 @@ +--- +deprecations: + - CLI argument for volume_service_name was deprecated long time ago. All + novaclient's methods for communication with Volume API were deprecated and + removed. There is no need to leave **volume_service_name** argument of + novaclient.client.Client entry-point since it is not used anywhere, + so it is removed now. diff --git a/releasenotes/notes/deprecate_cell_name_arg-eb34cb7c43cfcb89.yaml b/releasenotes/notes/deprecate_cell_name_arg-eb34cb7c43cfcb89.yaml new file mode 100644 index 000000000..dbb8ae7ee --- /dev/null +++ b/releasenotes/notes/deprecate_cell_name_arg-eb34cb7c43cfcb89.yaml @@ -0,0 +1,6 @@ +--- +deprecations: + - CLI argument ``--cell_name`` for ``nova migration-list`` command is + deprecated. Nova API does not have logic for handling cell_name + parameter in **os-migrations**, and while the parameter is passed to Nova + it has never been used. diff --git a/releasenotes/notes/deprecate_contrib_extensions-0ec70c070b09eedb.yaml b/releasenotes/notes/deprecate_contrib_extensions-0ec70c070b09eedb.yaml new file mode 100644 index 000000000..66810756f --- /dev/null +++ b/releasenotes/notes/deprecate_contrib_extensions-0ec70c070b09eedb.yaml @@ -0,0 +1,21 @@ +--- +prelude: > + All extensions of API V2.0 were merged to 2.1, but NovaClient continued + to store them as a separate entities. +upgrade: + - All managers and resources from novaclient.v2.contrib submodules are moved + to appropriate submodules of novaclient.v2 (except barametal and + tenant_networks, which were deprecated previously) + - All shell commands from novaclient.v2.contrib submodules are moved to + novaclient.v2.shell module. + - novaclient.v2.client.Client imports all modules (which were located in + submodules of novaclient.v2.contrib) by-default for api version v2 + - Method novaclient.client.discover_extensions returns only barametal and + tenant_networks extensions, since they are not included by default. + - There are no modules and extensions for "deferred_delete", "host_evacuate", + "host_evacuate_live" and "metadata_extensions" anymore. Previously, they + contained only shell commands and shell module auto loads them (there is + no ability to not do it). +deprecations: + - All modules of novaclient.v2.contrib are deprecated now and will be + removed after OpenStack Pike. diff --git a/releasenotes/notes/drop-deprecated-aggregate-update-args-17bd019f4be34b18.yaml b/releasenotes/notes/drop-deprecated-aggregate-update-args-17bd019f4be34b18.yaml new file mode 100644 index 000000000..f02d0be84 --- /dev/null +++ b/releasenotes/notes/drop-deprecated-aggregate-update-args-17bd019f4be34b18.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The deprecated `name` and `availability_zone` positional arguments in + the ``nova aggregate-update`` command have been removed. Use the + ``--name`` and ``--availability-zone`` options instead. 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-booting-with-multiple-nics-c6e5885b948d35ba.yaml b/releasenotes/notes/fix-booting-with-multiple-nics-c6e5885b948d35ba.yaml new file mode 100644 index 000000000..5f7218760 --- /dev/null +++ b/releasenotes/notes/fix-booting-with-multiple-nics-c6e5885b948d35ba.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - Fix an ability to boot server with multiple nics which was broken with + microversion 2.42 (fix tag attribute disappearing). 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/fix-tag-attribute-disappearing-25483a80f548ef35.yaml b/releasenotes/notes/fix-tag-attribute-disappearing-25483a80f548ef35.yaml new file mode 100644 index 000000000..7f6ed29ec --- /dev/null +++ b/releasenotes/notes/fix-tag-attribute-disappearing-25483a80f548ef35.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Microversion 2.42 is related to the following bug. + + * https://bugs.launchpad.net/nova/+bug/1658571 + + The following options have been changed as of Microversion 2.42. + + * Remove ``tag`` attribute in ``--block-device`` option + on the server boot (nova boot) between microversion 2.33 and 2.41. + * Remove ``tag`` attribute in ``--nic`` option + on the server boot (nova boot) between microversion 2.37 and 2.41. diff --git a/releasenotes/notes/fix-token-auth-6c48c63a759f51d5.yaml b/releasenotes/notes/fix-token-auth-6c48c63a759f51d5.yaml new file mode 100644 index 000000000..29487586f --- /dev/null +++ b/releasenotes/notes/fix-token-auth-6c48c63a759f51d5.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - Fix an ability to authenticate using Keystone Token which was broken with + novaclient 7.0.0 release. + diff --git a/releasenotes/notes/fixed-ListExtResource-given-in-place-of-ListExtManager-a759a27079d16a44.yaml b/releasenotes/notes/fixed-ListExtResource-given-in-place-of-ListExtManager-a759a27079d16a44.yaml new file mode 100644 index 000000000..6491caecc --- /dev/null +++ b/releasenotes/notes/fixed-ListExtResource-given-in-place-of-ListExtManager-a759a27079d16a44.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + The contents of the list_extensions.py file was moved from contrib to v2 + directory in release 7.0.0, and a stub importing the objects from the new + location was left in its place for backward compatibility, together with + a warning informing about the new location. However, the stub incorrectly + assigned the ListExtResource class to the ListExtManager name. This has + now been fixed, and ListExtManager is used instead. 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/global_request_id-26f4e4301f84d403.yaml b/releasenotes/notes/global_request_id-26f4e4301f84d403.yaml new file mode 100644 index 000000000..e15358f77 --- /dev/null +++ b/releasenotes/notes/global_request_id-26f4e4301f84d403.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + A new ``global_request_id`` parameter is accepted on the client + constructor, which will then pass ``X-OpenStack-Request-ID`` on + all requests made. diff --git a/releasenotes/notes/image-api-deprecation-41944dc6fc024918.yaml b/releasenotes/notes/image-api-deprecation-41944dc6fc024918.yaml new file mode 100644 index 000000000..23d987332 --- /dev/null +++ b/releasenotes/notes/image-api-deprecation-41944dc6fc024918.yaml @@ -0,0 +1,14 @@ +--- +deprecations: + - | + The following CLIs and python API bindings are now deprecated for removal: + + * nova image-delete + * nova image-list + * nova image-meta + * nova image-show + + These will be removed in the first major python-novaclient release after + the Nova 15.0.0 Ocata release. Use python-glanceclient or + python-openstackclient for CLI and python-glanceclient or openstacksdk + for python API bindings. diff --git a/releasenotes/notes/instance-uuid-flag-in-migration-list-5d2fed7657d3def5.yaml b/releasenotes/notes/instance-uuid-flag-in-migration-list-5d2fed7657d3def5.yaml new file mode 100644 index 000000000..239c662ea --- /dev/null +++ b/releasenotes/notes/instance-uuid-flag-in-migration-list-5d2fed7657d3def5.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + A new ``--instance-uuid`` option is added to ``nova migration-list`` + command. This is used to query the migration history of a specific server + by the migration-list command. Please use ``nova server-migration-list`` + command for querying in-progress migrations of a specific server. 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/keystoneauth-8ec1e6be14cdbae3.yaml b/releasenotes/notes/keystoneauth-8ec1e6be14cdbae3.yaml new file mode 100644 index 000000000..d9e2d7ffa --- /dev/null +++ b/releasenotes/notes/keystoneauth-8ec1e6be14cdbae3.yaml @@ -0,0 +1,11 @@ +--- +features: +- keystoneauth plugins are now supported. +upgrade: +- novaclient now requires the keystoneauth library. +deprecations: +- novaclient auth strategy plugins are deprecated. Please use + keystoneauth auth plugins instead. +- nova credentials is deprecated. Please use openstack token issue +- nova endpoints is deprecated. Please use openstack catalog list + instead. diff --git a/releasenotes/notes/log-request-id-ce106497e0520fad.yaml b/releasenotes/notes/log-request-id-ce106497e0520fad.yaml new file mode 100644 index 000000000..5a7f6f508 --- /dev/null +++ b/releasenotes/notes/log-request-id-ce106497e0520fad.yaml @@ -0,0 +1,11 @@ +--- +prelude: > + - Log 'x-openstack-request-id' or 'x-compute-request-id' + in each API call. If the caller (e.g. heat) uses oslo.log, + the caller's request id in oslo.context and the callee's + request id can be output in the same log message (same line). +features: + - Log 'x-openstack-request-id' or 'x-compute-request-id' + in each API call. If the caller (e.g. heat) uses oslo.log, + the caller's request id in oslo.context and the callee's + request id can be output in the same log message (same line). diff --git a/releasenotes/notes/make-console-public-0c776bfda240cd9d.yaml b/releasenotes/notes/make-console-public-0c776bfda240cd9d.yaml new file mode 100644 index 000000000..e8743f67a --- /dev/null +++ b/releasenotes/notes/make-console-public-0c776bfda240cd9d.yaml @@ -0,0 +1,7 @@ +--- +features: + - Provides a public unified interface 'get_console_url' for classes + 'novaclient.v2.servers.Server' and 'novaclient.v2.servers.ServerManager'. + Users (Heat, OpenStack-client and etc.) can call this public interface + instead of calling the individual methods to retrieve a console url + of a particular protocol. diff --git a/releasenotes/notes/microversion-2.37-d03da96406a45e67.yaml b/releasenotes/notes/microversion-2.37-d03da96406a45e67.yaml new file mode 100644 index 000000000..211a4f9ce --- /dev/null +++ b/releasenotes/notes/microversion-2.37-d03da96406a45e67.yaml @@ -0,0 +1,27 @@ +--- +features: + - | + The 2.37 microversion is now supported. This introduces the following + changes: + + * CLI: The **--nic** value for the **nova boot** command now takes two + special values, 'auto' and 'none'. If --nic is not specified, the + CLI defaults to 'auto'. + * Python API: The **nics** kwarg is required when creating a server using + the *novaclient.v2.servers.ServerManager.create* API. The **nics** + value can be a list of dicts or a string with value 'auto' or + 'none'. + +upgrade: + - | + With the 2.37 microversion, the **nics** kwarg is required when creating + a server using the *novaclient.v2.servers.ServerManager.create* API. The + **nics** value can be a list of dicts or an enum string with one of the + following values: + + * **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. + * **none**: This tells the Compute service to not allocate any networking + for the server. diff --git a/releasenotes/notes/microversion-v2_28-abf653ae5cf5c4a9.yaml b/releasenotes/notes/microversion-v2_28-abf653ae5cf5c4a9.yaml new file mode 100644 index 000000000..064f23cc9 --- /dev/null +++ b/releasenotes/notes/microversion-v2_28-abf653ae5cf5c4a9.yaml @@ -0,0 +1,5 @@ +--- +upgrade: +- Support v2.28 microversion +- cpu_info property of hypervisor resource is a json now (previously it was + text). diff --git a/releasenotes/notes/microversion-v2_31-3e1a16eb5eb53f59.yaml b/releasenotes/notes/microversion-v2_31-3e1a16eb5eb53f59.yaml new file mode 100644 index 000000000..4ad0b4be5 --- /dev/null +++ b/releasenotes/notes/microversion-v2_31-3e1a16eb5eb53f59.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - Support for microversion 2.31 which fixes a bug in the + os-console-auth-tokens API diff --git a/releasenotes/notes/microversion-v2_33-10d12ea3b25839e8.yaml b/releasenotes/notes/microversion-v2_33-10d12ea3b25839e8.yaml new file mode 100644 index 000000000..0376b1455 --- /dev/null +++ b/releasenotes/notes/microversion-v2_33-10d12ea3b25839e8.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added microversion v2.33 that adds pagination support for hypervisors with + the help of new optional parameters 'limit' and 'marker' which were added + to hypervisor-list command. \ No newline at end of file diff --git a/releasenotes/notes/microversion-v2_34-a9c5601811152964.yaml b/releasenotes/notes/microversion-v2_34-a9c5601811152964.yaml new file mode 100644 index 000000000..284b01eb4 --- /dev/null +++ b/releasenotes/notes/microversion-v2_34-a9c5601811152964.yaml @@ -0,0 +1,3 @@ +--- +upgrade: + - Support for microversion 2.34 added. \ No newline at end of file diff --git a/releasenotes/notes/microversion-v2_35-537619a43278fbb5.yaml b/releasenotes/notes/microversion-v2_35-537619a43278fbb5.yaml new file mode 100644 index 000000000..0b220a4db --- /dev/null +++ b/releasenotes/notes/microversion-v2_35-537619a43278fbb5.yaml @@ -0,0 +1,5 @@ +--- +features: + - Added microversion v2.35 that adds pagination support for keypairs with + the help of new optional parameters 'limit' and 'marker' which were added + to keypair-list command. \ No newline at end of file diff --git a/releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml b/releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml new file mode 100644 index 000000000..a9944a72c --- /dev/null +++ b/releasenotes/notes/microversion-v2_38-0618fe2b3c7f96f9.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - Support for microversion 2.38 added. As of microversion 2.38, invalid + statuses passed to ``nova list --status invalid_status`` will result in a + HTTP 400 Bad Request error response. \ No newline at end of file diff --git a/releasenotes/notes/microversion-v2_40-484adba0806b08bf.yaml b/releasenotes/notes/microversion-v2_40-484adba0806b08bf.yaml new file mode 100644 index 000000000..8b856ac65 --- /dev/null +++ b/releasenotes/notes/microversion-v2_40-484adba0806b08bf.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added microversion v2.40 which introduces pagination support for usage + with the help of new optional parameters 'limit' and 'marker'. diff --git a/releasenotes/notes/microversion-v2_41-6df7a5a66a9ded35.yaml b/releasenotes/notes/microversion-v2_41-6df7a5a66a9ded35.yaml new file mode 100644 index 000000000..34d442d9e --- /dev/null +++ b/releasenotes/notes/microversion-v2_41-6df7a5a66a9ded35.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added support for microversion 2.41 which shows the aggregate UUID in CLI + output when listing, creating, showing, updating, setting metadata, and + adding or removing hosts from an aggregate. diff --git a/releasenotes/notes/microversion-v2_43-76db2ac463b431e4.yaml b/releasenotes/notes/microversion-v2_43-76db2ac463b431e4.yaml new file mode 100644 index 000000000..7f70ce557 --- /dev/null +++ b/releasenotes/notes/microversion-v2_43-76db2ac463b431e4.yaml @@ -0,0 +1,14 @@ +--- +deprecations: + - | + The following CLIs and their backing API bindings are 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 will be removed in the first major release after + Nova 16.0.0 Pike is released. diff --git a/releasenotes/notes/microversion-v2_44-d60c8834e436ad3d.yaml b/releasenotes/notes/microversion-v2_44-d60c8834e436ad3d.yaml new file mode 100644 index 000000000..2ac1b3b04 --- /dev/null +++ b/releasenotes/notes/microversion-v2_44-d60c8834e436ad3d.yaml @@ -0,0 +1,16 @@ +--- +deprecations: + - | + The following CLIs and their backing API bindings are 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 will be removed in the first major release after + Nova 16.0.0 Pike is released. diff --git a/releasenotes/notes/microversion-v2_45-1bfcae3914280534.yaml b/releasenotes/notes/microversion-v2_45-1bfcae3914280534.yaml new file mode 100644 index 000000000..1c278f3e1 --- /dev/null +++ b/releasenotes/notes/microversion-v2_45-1bfcae3914280534.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Support was added for microversion 2.45. This changes how the + ``createImage`` and ``createBackup`` server action APIs return + the created snapshot image ID in the response. With microversion + 2.45 and later, the image ID is return in a json dict response body + with an ``image_id`` key and uuid value. The old ``Location`` response + header is no longer returned in microversion 2.45 or later. + + There are no changes to the ``nova image-create`` CLI. However, the + ``nova backup`` CLI will print out the backup snapshot image information + with microversion 2.45 or greater now. diff --git a/releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml b/releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml new file mode 100644 index 000000000..c5945adba --- /dev/null +++ b/releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + Added support for microversion 2.47 which returns the flavor details + directly embedded in the server details when listing or showing servers. + With this change, CLI requests with microversion >= 2.47 will no longer + need to do additional queries to get the flavor and flavor extra_specs + information. Instead, the flavor information will be output as + separate key/value pairs with the keys namespaced with the + "flavor:" prefix. As one would expect, these keys can also be + specified as output fields when listing servers, like this: + + ``nova list --fields name,flavor:original_name`` + + When displaying details of a single server, the ``--minimal`` option will + display a ``flavor`` field with a value of the ``original_name`` of the + flavor. Prior to this microversion the value was the ``id`` of the flavor. + diff --git a/releasenotes/notes/microversion-v2_49-56bde596ee13366d.yaml b/releasenotes/notes/microversion-v2_49-56bde596ee13366d.yaml new file mode 100644 index 000000000..86120be48 --- /dev/null +++ b/releasenotes/notes/microversion-v2_49-56bde596ee13366d.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added support for microversion 2.49 that enables users to + attach tagged interfaces and volumes. + A new ``--tag`` option is added to ``nova volume-attach`` and + ``nova interface-attach`` commands. diff --git a/releasenotes/notes/microversion-v2_50-4f484658d66d01aa.yaml b/releasenotes/notes/microversion-v2_50-4f484658d66d01aa.yaml new file mode 100644 index 000000000..3ca1908cd --- /dev/null +++ b/releasenotes/notes/microversion-v2_50-4f484658d66d01aa.yaml @@ -0,0 +1,32 @@ +--- +fixes: + - | + Adds support for the ``2.50`` microversion which fixes the + ``nova quota-class-show`` and ``nova quota-class-update`` commands in the + following ways: + + * The ``server_groups`` and ``server_group_members`` quota resources will + now be shown in the output table for ``nova quota-class-show``. + * The ``floating_ips``, ``fixed_ips``, ``security_groups`` and + ``security_group_rules`` quota resources will no longer be able to + be updated using ``nova quota-class-update`` nor will they be shown in + the output of ``nova quota-class-show``. Use python-openstackclient or + python-neutronclient to work with quotas for network resources. + + In addition, the ``nova quota-class-update`` CLI was previously incorrectly + limiting the ability to update quota class values for ``floating_ips``, + ``fixed_ips``, ``security_groups`` and ``security_group_rules`` based on + the 2.36 microversion. That has been changed to limit based on the ``2.50`` + microversion. +upgrade: + - | + The ``novaclient.v2.quota_classes.QuotaClassSetManager.update`` method + now defines specific kwargs starting with microversion ``2.50`` since + updating network-related resource quota class values is not supported on + the server with microversion ``2.50``. The list of excluded resources is: + + - ``fixed_ips`` + - ``floating_ips`` + - ``networks`` + - ``security_groups`` + - ``security_group_rules`` diff --git a/releasenotes/notes/microversion-v2_52-2fe81b3bf2e4b4ea.yaml b/releasenotes/notes/microversion-v2_52-2fe81b3bf2e4b4ea.yaml new file mode 100644 index 000000000..0f7eeedcd --- /dev/null +++ b/releasenotes/notes/microversion-v2_52-2fe81b3bf2e4b4ea.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + `Microversion 2.52`_ is now supported which adds the ``--tags`` option to + the ``nova boot`` command and a ``tags`` kwarg to the + ``novaclient.v2.servers.ServerManager.create()`` python API binding method. + + .. _Microversion 2.52: https://docs.openstack.org/nova/latest/api_microversion_history.html#id47 diff --git a/releasenotes/notes/microversion-v2_53-3463b546a38c5f84.yaml b/releasenotes/notes/microversion-v2_53-3463b546a38c5f84.yaml new file mode 100644 index 000000000..0ee4cfcb9 --- /dev/null +++ b/releasenotes/notes/microversion-v2_53-3463b546a38c5f84.yaml @@ -0,0 +1,35 @@ +--- +features: + - | + Added support for `microversion 2.53`_. The following changes were made + for the ``services`` commands and python API bindings: + + - The ``nova service-list`` command and API will have a UUID value for the + ``id`` field in the output and response, respectively. + - The ``nova service-enable`` command and API will require a UUID service + id value to uniquely identify the service rather than a ``host`` and + ``binary`` value. The UUID ``id`` field will also be in the command + output. + - The ``nova service-disable`` command and API will require a UUID service + id value to uniquely identify the service rather than a ``host`` and + ``binary`` value. The UUID ``id`` field will also be in the command + output. + - The ``nova service-force-down`` command and API will require a UUID + service id value to uniquely identify the service rather than a ``host`` + and ``binary`` value. The UUID ``id`` field will also be in the command + output. + - The ``nova service-delete`` command and API will require a UUID + service id value to uniquely identify the service rather than an integer + service id value. + + The following changes were made for the ``hypervisors`` commands and python + API bindings: + + - The ID field in the various ``nova hypervisor-*`` commands and + ``Hypervisor.id`` attribute in the API binding will now be a UUID value. + - If paging over hypervisors using ``nova hypervisor-list``, the + ``--marker`` must be a UUID value. + - The ``nova hypervisor-show`` and ``nova hypervisor-uptime`` commands and + APIs now take a UUID value for the hypervisor ID. + + .. _microversion 2.53: https://docs.openstack.org/nova/latest/api_microversion_history.html#id48 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/no-glance-proxy-5c13001a4b13e8ce.yaml b/releasenotes/notes/no-glance-proxy-5c13001a4b13e8ce.yaml new file mode 100644 index 000000000..01b65bb76 --- /dev/null +++ b/releasenotes/notes/no-glance-proxy-5c13001a4b13e8ce.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + The 2.36 microversion deprecated the image proxy API. As such, CLI calls + now directly call the image service to get image details, for example, + as a convenience to boot a server with an image name rather than the image + id. To do this the following is assumed: + + #. There is an **image** entry in the service catalog. + #. The image v2 API is available. diff --git a/releasenotes/notes/no-neutron-proxy-18fd54febe939a6b.yaml b/releasenotes/notes/no-neutron-proxy-18fd54febe939a6b.yaml new file mode 100644 index 000000000..21007cd93 --- /dev/null +++ b/releasenotes/notes/no-neutron-proxy-18fd54febe939a6b.yaml @@ -0,0 +1,12 @@ +--- +upgrade: + - | + The 2.36 microversion deprecated the network proxy APIs in + Nova. Because of this we now go directly to neutron for name to + net-id lookups. For nova-net deployements the old proxies will + continue to be used. + + To do this the following is assumed: + + #. There is a **network** entry in the service catalog. + #. The network v2 API is available. diff --git a/releasenotes/notes/pike-rm-deprecated-img-d58e9ae2d774cbfc.yaml b/releasenotes/notes/pike-rm-deprecated-img-d58e9ae2d774cbfc.yaml new file mode 100644 index 000000000..0e5d109c4 --- /dev/null +++ b/releasenotes/notes/pike-rm-deprecated-img-d58e9ae2d774cbfc.yaml @@ -0,0 +1,13 @@ +--- +prelude: > + Deprecated image commands and python API bindings have been removed. +upgrade: + - | + The following deprecated image commands have been removed:: + + * nova image-list + * nova image-show + * nova image-meta + * nova image-delete + + Along with the related python API bindings in ``novaclient.v2.images``. diff --git a/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml new file mode 100644 index 000000000..8531b05be --- /dev/null +++ b/releasenotes/notes/pike-rm-deprecated-net-272aeb62b329a5bc.yaml @@ -0,0 +1,77 @@ +--- +prelude: > + Deprecated network-related resource commands and python API bindings + have been removed. From this point on, python-novaclient will no longer + work with nova-network *except* for the ``nova virtual-interface-list``, + ``nova add-fixed-ip`` and ``nova remove-fixed-ip`` commands. +upgrade: + - | + The following deprecated network-related resource commands have been + removed:: + + * nova dns-create + * nova dns-create-private-domain + * nova dns-create-public-domain + * nova dns-delete + * nova dns-delete-domain + * nova dns-domains + * nova dns-list + * nova fixed-ip-get + * nova fixed-ip-reserve + * nova fixed-ip-unreserve + * nova floating-ip-create + * nova floating-ip-delete + * nova floating-ip-list + * nova floating-ip-bulk-create + * nova floating-ip-bulk-delete + * nova floating-ip-bulk-list + * nova floating-ip-pool-list + * nova net + * nova net-create + * nova net-delete + * nova net-list + * nova network-create + * nova network-delete + * nova network-list + * nova network-show + * nova network-associate-host + * nova-network-associate-project + * nova network-disassociate + * nova scrub + * nova secgroup-create + * nova secgroup-delete + * nova secgroup-list + * nova secgroup-update + * nova secgroup-add-rule + * nova secgroup-delete-rule + * nova secgroup-list-rules + * nova secgroup-add-default-rule + * nova secgroup-delete-default-rule + * nova secgroup-list-default-rules + * nova secgroup-add-group-rule + * nova secgroup-delete-group-rule + * nova tenant-network-create + * nova tenant-network-delete + * nova tenant-network-list + * nova tenant-network-show + + Along with the following python API bindings:: + + * novaclient.v2.contrib.tenant_networks + * novaclient.v2.fixed_ips + * novaclient.v2.floating_ip_dns + * novaclient.v2.floating_ip_pools + * novaclient.v2.floating_ips + * novaclient.v2.floating_ips_bulk + * novaclient.v2.fping + * novaclient.v2.networks + * novaclient.v2.security_group_default_rules + * novaclient.v2.security_group_rules + * novaclient.v2.security_groups + +deprecations: + - | + The ``only_contrib`` parameter for the + ``novaclient.client.discover_extensions`` method is deprecated and now + results in an empty list returned since all contrib extensions are either + required or have been removed. \ No newline at end of file diff --git a/releasenotes/notes/remove-auth-system-b2cd247b8a312b72.yaml b/releasenotes/notes/remove-auth-system-b2cd247b8a312b72.yaml new file mode 100644 index 000000000..ce3fb4d0c --- /dev/null +++ b/releasenotes/notes/remove-auth-system-b2cd247b8a312b72.yaml @@ -0,0 +1,9 @@ +--- +prelude: > + The ability to use non-Keystone authentication systems has been removed. +upgrade: + - The ``--os-auth-system`` CLI option and ``OS_AUTH_SYSTEM`` environment + variable usage was deprecated in the 3.1.0 release during the Mitaka + development series. This release drops the support for using those options + to load non-Keystone authentication systems via the + ``openstack.client.auth_plugin`` extension point. 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-3.3.0-82a413157838570d.yaml b/releasenotes/notes/remove-deprecated-option-in-3.3.0-82a413157838570d.yaml new file mode 100644 index 000000000..f78208551 --- /dev/null +++ b/releasenotes/notes/remove-deprecated-option-in-3.3.0-82a413157838570d.yaml @@ -0,0 +1,53 @@ +--- +update: + - The following deprecated options have been removed: + - create instances: + - --num-instance replaced by --min-count and --max-count + - --key_name replaced by --key-name + - --user_data replaced by --user-data + - --availability_zone replaced by -- availability-zone + - --security_groups replaced by --sercurity-groups + - --block_device_mapping replaced by --block-device-mapping + - list servers: + - --reservation_id replaced by --reservation-id + - --instance_name replaced by --instance-name + - --all_tenants replaced by --all-tenants + - rebuild instance: + - --rebuild_password replaced by --rebuild-password + - get serial console: + - --console_type replaced by --console-type + - create dns private domain: + - --availability_zone replaced by --availability-zone + - list security groups: + - --all_tenants replaced by --all-tenants + - add key pairs: + - --pub_key replaced by --pub-key + - live-migrate servers: + - --block_migrate replaced by --block-migrate + - --disk_over_commit replaced by --disk-over-commit + - update quotas: + - --floating_ips replaced by --floating-ips + - --metadata_items replaced by --metadata-items + - --injected_files replaced by --injected-files + - --injected_file_content_bytes replaced by --injected-file-content-bytes + - update quota classes: + - --floating_ips replaced by --floating-ips + - --metadata_items replaced by --metadata-items + - --injected_files replaced by --injected-files + - --injected_file_content_bytes replaced by --injected-file-content-bytes + - create server groups: + - --policy + - Authentication Options: + - --os_username replaced by --os-username + - --os_password replaced by --os-password + - --os_tenant_name replaced by --os-tenant-name + - --os_auth_url replaced by --os-auth-url + - --os_region_name replaced by --os-region-name + - --os_auth_system replaced by --os-auth-system + - --endpoint-type replaced by --os-endpoint-type + - Optional arguments: + - --service_type replaced by --service-type + - --service_name replaced by --service-name + - --volume_service_name replaced by --volume-service-name + - --os_compute_api_version replaced by --os-compute-api-version + - --bypass_url replaced by --bypass-url 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 new file mode 100644 index 000000000..a98272c80 --- /dev/null +++ b/releasenotes/notes/remove-deprecated-option-in-9.0.0-bc76629d28f1d4c4.yaml @@ -0,0 +1,11 @@ +--- +upgrade: + - | + The following deprecated options have been removed: + + - ``--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-py26-support-f31379e86f40d975.yaml b/releasenotes/notes/remove-py26-support-f31379e86f40d975.yaml new file mode 100644 index 000000000..4f144875c --- /dev/null +++ b/releasenotes/notes/remove-py26-support-f31379e86f40d975.yaml @@ -0,0 +1,3 @@ +--- +upgrade: + - Python 2.6 support has been removed from python-novaclient. 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-run_tests.sh-3bdcaee4d388177a.yaml b/releasenotes/notes/remove-run_tests.sh-3bdcaee4d388177a.yaml new file mode 100644 index 000000000..524a2c048 --- /dev/null +++ b/releasenotes/notes/remove-run_tests.sh-3bdcaee4d388177a.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - The ``run_tests.sh`` shell script that was deprecated in Newton has + been removed. 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 new file mode 100644 index 000000000..4b32d376b --- /dev/null +++ b/releasenotes/notes/remove_api_v_1_1-88b3f18ce1423b46.yaml @@ -0,0 +1,4 @@ +--- +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/rename-apikey-to-password-735588d841efa49e.yaml b/releasenotes/notes/rename-apikey-to-password-735588d841efa49e.yaml new file mode 100644 index 000000000..36578342a --- /dev/null +++ b/releasenotes/notes/rename-apikey-to-password-735588d841efa49e.yaml @@ -0,0 +1,5 @@ +--- +deprecations: + - The **api_key** variable of novaclient.client.Client entry-point was + deprecated in favor of **password**. Nothing has changed in the case + of positional argument usage. diff --git a/releasenotes/notes/rename-bypass-url-42cd96956a6bc6dc.yaml b/releasenotes/notes/rename-bypass-url-42cd96956a6bc6dc.yaml new file mode 100644 index 000000000..dc6374e15 --- /dev/null +++ b/releasenotes/notes/rename-bypass-url-42cd96956a6bc6dc.yaml @@ -0,0 +1,10 @@ +--- +deprecations: + - The **--bypass-url** CLI argument was deprecated in favor of + **--endpoint-override** + - The **bypass_url** argument of novaclient.client.Client entry-point was + deprecated in favor of **endpoint_override**. +fixes: + - Inability to use bypass-url with keystone session is fixed. +features: + - You can use **bypass-url** / **endpoint-override** with Keystone V3. diff --git a/releasenotes/notes/restrict-args-for-novaclient-ep-491098c3634365be.yaml b/releasenotes/notes/restrict-args-for-novaclient-ep-491098c3634365be.yaml new file mode 100644 index 000000000..11dda395a --- /dev/null +++ b/releasenotes/notes/restrict-args-for-novaclient-ep-491098c3634365be.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + novaclient.client.Client entry-point accepts only 5 positional arguments:: + + version, username, api_key, project_id, auth_url + + Using positional arguments for all other options is now an error. diff --git a/releasenotes/notes/restrict-direct-use-of-v2client-c8e1ee2afefec5a1.yaml b/releasenotes/notes/restrict-direct-use-of-v2client-c8e1ee2afefec5a1.yaml new file mode 100644 index 000000000..0e90cc810 --- /dev/null +++ b/releasenotes/notes/restrict-direct-use-of-v2client-c8e1ee2afefec5a1.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - novaclient.v2.client.Client raises an exception in case of direct usage + instead of warning message. novaclient.client.Client is a primary + interface to initialize the python client for Nova. diff --git a/releasenotes/notes/restrict-interface-parameter-e5fe166f39ba0935.yaml b/releasenotes/notes/restrict-interface-parameter-e5fe166f39ba0935.yaml new file mode 100644 index 000000000..1ac4a1e76 --- /dev/null +++ b/releasenotes/notes/restrict-interface-parameter-e5fe166f39ba0935.yaml @@ -0,0 +1,4 @@ +--- +deprecations: + - keyword argument **interface** of novaclient.client.Client entry-point was + deprecated in favor of **endpoint_type**; diff --git a/releasenotes/notes/return-request-id-to-caller-52c5423794b33f8b.yaml b/releasenotes/notes/return-request-id-to-caller-52c5423794b33f8b.yaml new file mode 100644 index 000000000..e390c1629 --- /dev/null +++ b/releasenotes/notes/return-request-id-to-caller-52c5423794b33f8b.yaml @@ -0,0 +1,26 @@ +--- +prelude: > + Methods in manager classes and resource classes return wrapper classes + that wrap values returned originally. + For example, a wrapper class for list, a wrapper class for dict, + a wrapper class for str and so on. + The wrapper classes have a 'request_ids' property for request IDs + returned from Nova (nova-api). So the caller can get the + Nova's request IDs, then output them to logs with its own request ID. + The function to output them to the logs will be implemented + in other projects (cinder, heat, etc.). +features: + - Methods in manager classes and resource classes return wrapper classes + that wrap values returned originally. + For example, a wrapper class for list, a wrapper class for dict, + a wrapper class for str and so on. + The wrapper classes have a 'request_ids' property for request IDs + returned from Nova (nova-api). So the caller can get the + Nova's request IDs, then output them to logs with its own request ID. + The function to output them to the logs will be implemented + in other projects (cinder, heat, etc.). +upgrade: + - In case that methods return a response object and body originally and + body is None, the methods return the wrapper class for tuple as 'body' + instead of the wrapper class for None. + The wrapper class for None has not been added. diff --git a/releasenotes/notes/rm-baremetal-cli-api-fbc8c242d48cd2fb.yaml b/releasenotes/notes/rm-baremetal-cli-api-fbc8c242d48cd2fb.yaml new file mode 100644 index 000000000..d35a7349c --- /dev/null +++ b/releasenotes/notes/rm-baremetal-cli-api-fbc8c242d48cd2fb.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The baremetal CLIs and python API bindings were deprecated in the Newton + release and have been removed. Use python-openstackclient or + python-ironicclient for CLIs. Use python-ironicclient or openstacksdk for + python API bindings \ No newline at end of file diff --git a/releasenotes/notes/rm-deprecated-commands-options-ocata-00f249810e5bdf97.yaml b/releasenotes/notes/rm-deprecated-commands-options-ocata-00f249810e5bdf97.yaml new file mode 100644 index 000000000..d710ffa76 --- /dev/null +++ b/releasenotes/notes/rm-deprecated-commands-options-ocata-00f249810e5bdf97.yaml @@ -0,0 +1,17 @@ +--- +prelude: > + Several deprecated commands have been removed. See the upgrade section for + details. +upgrade: + - | + The following deprecated commands have been removed: + + * absolute-limits + * add-floating-ip + * aggregate-details + * credentials + * endpoints + * rate-limits + * remove-floating-ip + * rename + * root-password 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/notes/switch-to-sessionclient-aa49d16599fea570.yaml b/releasenotes/notes/switch-to-sessionclient-aa49d16599fea570.yaml new file mode 100644 index 000000000..4a60f7ec4 --- /dev/null +++ b/releasenotes/notes/switch-to-sessionclient-aa49d16599fea570.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + When using novaclient as a library (via novaclient.client.Client + entry-point), an internal implementation of HTTPClient was used if a + session object is not specified. For better user experience we switched + to using SessionClient which uses keystoneauth (Keystone folks maintain + this library) for all auth stuff. + The SessionClient interface is similar to HTTPClient, but there is a + small possibility that you will notice a difference. diff --git a/releasenotes/notes/volume-cli-removal-ffcb94421a356042.yaml b/releasenotes/notes/volume-cli-removal-ffcb94421a356042.yaml new file mode 100644 index 000000000..6a6495e1c --- /dev/null +++ b/releasenotes/notes/volume-cli-removal-ffcb94421a356042.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - Volume, volume-type and volume-snapshot create/update/delete/list CLIs + and python API bindings are removed. Use python-cinderclient or + python-openstackclient for CLIs instead. Use python-cinderclient or + python-openstacksdk for python API bindings instead. 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/_static/.placeholder b/releasenotes/source/_static/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/_templates/.placeholder b/releasenotes/source/_templates/.placeholder new file mode 100644 index 000000000..e69de29bb diff --git a/releasenotes/source/conf.py b/releasenotes/source/conf.py new file mode 100644 index 000000000..a328a1af1 --- /dev/null +++ b/releasenotes/source/conf.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# +# python-novaclient Release Notes documentation build configuration file + +# -- General configuration ------------------------------------------------ + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'reno.sphinxext', + 'openstackdocstheme', +] + +# The master toctree document. +master_doc = 'index' + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'openstackdocs' + + +# -- Options for Internationalization output ------------------------------ + +locale_dirs = ['locale/'] diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst new file mode 100644 index 000000000..5588c8d02 --- /dev/null +++ b/releasenotes/source/index.rst @@ -0,0 +1,38 @@ +Welcome to Nova Client Release Notes documentation! +=================================================== + +Contents +======== + +.. toctree:: + :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 + mitaka + liberty + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/releasenotes/source/liberty.rst b/releasenotes/source/liberty.rst new file mode 100644 index 000000000..68554fb81 --- /dev/null +++ b/releasenotes/source/liberty.rst @@ -0,0 +1,6 @@ +============================== + Liberty Series Release Notes +============================== + +.. release-notes:: + :branch: stable/liberty 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/locale/fr/LC_MESSAGES/releasenotes.po b/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po new file mode 100644 index 000000000..c201a0bf1 --- /dev/null +++ b/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po @@ -0,0 +1,255 @@ +# Gérald LONLAS , 2016. #zanata +msgid "" +msgstr "" +"Project-Id-Version: Nova Client Release Notes 7.1.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-03-24 05:03+0000\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2016-10-22 06:13+0000\n" +"Last-Translator: Gérald LONLAS \n" +"Language-Team: French\n" +"Language: fr\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" + +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:`genindex`" +msgstr ":ref:`genindex`" + +msgid ":ref:`search`" +msgstr ":ref:`search`" + +msgid "Contents" +msgstr "Contenu" + +msgid "Current Series Release Notes" +msgstr "Note de la release actuelle" + +msgid "Deprecation Notes" +msgstr "Notes dépréciées " + +msgid "Fixed IPs" +msgstr "IP fixes" + +msgid "Floating IPs" +msgstr "IP flottantes" + +msgid "Indices and tables" +msgstr "Index et table des matières" + +msgid "Known Issues" +msgstr "Problèmes connus" + +msgid "Liberty Series Release Notes" +msgstr "Note de release pour Liberty" + +msgid "Mitaka Series Release Notes" +msgstr "Note de release pour Mitaka" + +msgid "New Features" +msgstr "Nouvelles fonctionnalités" + +msgid "Newton Series Release Notes" +msgstr "Note de release pour Newton" + +msgid "Security Group Rules" +msgstr "Règles de groupe de sécurité" + +msgid "Security Groups" +msgstr "Groupes de sécurité" + +msgid "Upgrade Notes" +msgstr "Notes de mises à jours" + +msgid "Welcome to Nova Client Release Notes documentation!" +msgstr "Bienvenue dans la documentation de la note de Release du Client Nova" + +msgid "absolute-limits" +msgstr "absolute-limits" + +msgid "add-floating-ip" +msgstr "add-floating-ip" + +msgid "aggregate-details" +msgstr "aggregate-details" + +msgid "credentials" +msgstr "informations d'identification" + +msgid "dns-create" +msgstr "dns-create" + +msgid "dns-create-private-domain" +msgstr "dns-create-private-domain" + +msgid "dns-create-public-domain" +msgstr "dns-create-public-domain" + +msgid "dns-delete" +msgstr "dns-delete" + +msgid "dns-delete-domain" +msgstr "dns-delete-domain" + +msgid "dns-domains" +msgstr "dns-domains" + +msgid "dns-list" +msgstr "dns-list" + +msgid "endpoints" +msgstr "points de terminaison" + +msgid "fixed-ip-get" +msgstr "fixed-ip-get" + +msgid "fixed-ip-reserve" +msgstr "fixed-ip-reserve" + +msgid "fixed-ip-unreserve" +msgstr "fixed-ip-unreserve" + +msgid "floating-ip-bulk-create" +msgstr "floating-ip-bulk-create" + +msgid "floating-ip-bulk-delete" +msgstr "floating-ip-bulk-delete" + +msgid "floating-ip-bulk-list" +msgstr "floating-ip-bulk-list" + +msgid "floating-ip-create" +msgstr "floating-ip-create" + +msgid "floating-ip-delete" +msgstr "floating-ip-delete" + +msgid "floating-ip-list" +msgstr "floating-ip-list" + +msgid "floating-ip-pool-list" +msgstr "floating-ip-pool-list" + +msgid "network-associate-host" +msgstr "network-associate-host" + +msgid "network-associate-project" +msgstr "network-associate-project" + +msgid "network-create" +msgstr "network-create" + +msgid "network-delete" +msgstr "network-delete" + +msgid "network-disassociate" +msgstr "network-disassociate" + +msgid "network-list" +msgstr "network-list" + +msgid "network-show" +msgstr "network-show" + +msgid "nova baremetal-interface-list" +msgstr "nova baremetal-interface-list" + +msgid "nova baremetal-node-list" +msgstr "nova baremetal-node-list" + +msgid "nova baremetal-node-show" +msgstr "nova baremetal-node-show" + +msgid "nova image-delete" +msgstr "nova image-delete" + +msgid "nova image-list" +msgstr "nova image-list" + +msgid "nova image-meta" +msgstr "nova image-meta" + +msgid "nova image-show" +msgstr "nova image-show" + +msgid "rate-limits" +msgstr "rate-limits" + +msgid "remove-floating-ip" +msgstr "remove-floating-ip" + +msgid "rename" +msgstr "rename" + +msgid "root-password" +msgstr "root-password" + +msgid "secgroup-add-default-rule" +msgstr "secgroup-add-default-rule" + +msgid "secgroup-add-group-rule" +msgstr "secgroup-add-group-rule" + +msgid "secgroup-add-rule" +msgstr "secgroup-add-rule" + +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" + +msgid "secgroup-list-rules" +msgstr "secgroup-list-rules" + +msgid "secgroup-update" +msgstr "secgroup-update" + +msgid "tenant-network-create" +msgstr "tenant-network-create" + +msgid "tenant-network-delete" +msgstr "tenant-network-delete" + +msgid "tenant-network-list" +msgstr "tenant-network-list" + +msgid "tenant-network-show" +msgstr "tenant-network-show" diff --git a/releasenotes/source/mitaka.rst b/releasenotes/source/mitaka.rst new file mode 100644 index 000000000..5fd7ffc4a --- /dev/null +++ b/releasenotes/source/mitaka.rst @@ -0,0 +1,6 @@ +=================================== + Mitaka Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/mitaka diff --git a/releasenotes/source/newton.rst b/releasenotes/source/newton.rst new file mode 100644 index 000000000..72c7cb4fc --- /dev/null +++ b/releasenotes/source/newton.rst @@ -0,0 +1,6 @@ +=================================== + Newton Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/newton diff --git a/releasenotes/source/ocata.rst b/releasenotes/source/ocata.rst new file mode 100644 index 000000000..53fb86e38 --- /dev/null +++ b/releasenotes/source/ocata.rst @@ -0,0 +1,6 @@ +=================================== + Ocata Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/ocata diff --git a/releasenotes/source/pike.rst b/releasenotes/source/pike.rst new file mode 100644 index 000000000..e43bfc0ce --- /dev/null +++ b/releasenotes/source/pike.rst @@ -0,0 +1,6 @@ +=================================== + Pike Series Release Notes +=================================== + +.. release-notes:: + :branch: stable/pike 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/unreleased.rst b/releasenotes/source/unreleased.rst new file mode 100644 index 000000000..875030f9d --- /dev/null +++ b/releasenotes/source/unreleased.rst @@ -0,0 +1,5 @@ +============================ +Current Series Release Notes +============================ + +.. release-notes:: 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 new file mode 100644 index 000000000..88c0e2b6c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +# 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.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/run_tests.sh b/run_tests.sh deleted file mode 100755 index aa832b353..000000000 --- a/run_tests.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/bin/bash - -set -eu - -function usage { - echo "Usage: $0 [OPTION]..." - echo "Run python-novaclient test suite" - echo "" - echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" - echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" - echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment" - echo " -x, --stop Stop running tests after the first error or failure." - echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." - echo " -p, --pep8 Just run pep8" - echo " -P, --no-pep8 Don't run pep8" - echo " -c, --coverage Generate coverage report" - echo " -h, --help Print this usage message" - echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list" - echo "" - echo "Note: with no options specified, the script will try to run the tests in a virtual environment," - echo " If no virtualenv is found, the script will ask if you would like to create one. If you " - echo " prefer to run tests NOT in a virtual environment, simply pass the -N option." - exit -} - -function process_option { - case "$1" in - -h|--help) usage;; - -V|--virtual-env) always_venv=1; never_venv=0;; - -N|--no-virtual-env) always_venv=0; never_venv=1;; - -s|--no-site-packages) no_site_packages=1;; - -f|--force) force=1;; - -p|--pep8) just_pep8=1;; - -P|--no-pep8) no_pep8=1;; - -c|--coverage) coverage=1;; - -*) noseopts="$noseopts $1";; - *) noseargs="$noseargs $1" - esac -} - -venv=.venv -with_venv=tools/with_venv.sh -always_venv=0 -never_venv=0 -force=0 -no_site_packages=0 -installvenvopts= -noseargs= -noseopts= -wrapper="" -just_pep8=0 -no_pep8=0 -coverage=0 - -for arg in "$@"; do - process_option $arg -done - -# If enabled, tell nose to collect coverage data -if [ $coverage -eq 1 ]; then - noseopts="$noseopts --with-coverage --cover-package=novaclient" -fi - -if [ $no_site_packages -eq 1 ]; then - installvenvopts="--no-site-packages" -fi - -function run_tests { - # Just run the test suites in current environment - ${wrapper} $NOSETESTS - # If we get some short import error right away, print the error log directly - RESULT=$? - return $RESULT -} - -function run_pep8 { - echo "Running pep8 ..." - srcfiles="novaclient tests" - # Just run PEP8 in current environment - # - # NOTE(sirp): W602 (deprecated 3-arg raise) is being ignored for the - # following reasons: - # - # 1. It's needed to preserve traceback information when re-raising - # exceptions; this is needed b/c Eventlet will clear exceptions when - # switching contexts. - # - # 2. There doesn't appear to be an alternative, "pep8-tool" compatible way of doing this - # in Python 2 (in Python 3 `with_traceback` could be used). - # - # 3. Can find no corroborating evidence that this is deprecated in Python 2 - # other than what the PEP8 tool claims. It is deprecated in Python 3, so, - # perhaps the mistake was thinking that the deprecation applied to Python 2 - # as well. - pep8_opts="--ignore=E202,W602 --repeat" - ${wrapper} pep8 ${pep8_opts} ${srcfiles} -} - -NOSETESTS="nosetests $noseopts $noseargs" - -if [ $never_venv -eq 0 ] -then - # Remove the virtual environment if --force used - if [ $force -eq 1 ]; then - echo "Cleaning virtualenv..." - rm -rf ${venv} - fi - if [ -e ${venv} ]; then - wrapper="${with_venv}" - else - if [ $always_venv -eq 1 ]; then - # Automatically install the virtualenv - python tools/install_venv.py $installvenvopts - wrapper="${with_venv}" - else - echo -e "No virtual environment found...create one? (Y/n) \c" - read use_ve - if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then - # Install the virtualenv and run the test suite in it - python tools/install_venv.py $installvenvopts - wrapper=${with_venv} - fi - fi - fi -fi - -# Delete old coverage data from previous runs -if [ $coverage -eq 1 ]; then - ${wrapper} coverage erase -fi - -if [ $just_pep8 -eq 1 ]; then - run_pep8 - exit -fi - -run_tests - -# NOTE(sirp): we only want to run pep8 when we're running the full-test suite, -# not when we're running tests individually. To handle this, we need to -# distinguish between options (noseopts), which begin with a '-', and -# arguments (noseargs). -if [ -z "$noseargs" ]; then - if [ $no_pep8 -eq 0 ]; then - run_pep8 - fi -fi - -if [ $coverage -eq 1 ]; then - echo "Generating coverage report in covhtml/" - ${wrapper} coverage html -d covhtml -i -fi diff --git a/setup.cfg b/setup.cfg index 7fa3ddac5..d78f29505 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,15 +1,34 @@ -[nosetests] -cover-package = novaclient -cover-html = true -cover-erase = true -cover-inclusive = true -verbosity=2 -detailed-errors=1 +[metadata] +name = python-novaclient +summary = Client library for OpenStack Compute API +description_file = + README.rst +license = Apache License, Version 2.0 +author = OpenStack +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 + Environment :: OpenStack + Intended Audience :: Developers + Intended Audience :: Information Technology + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + 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 -[build_sphinx] -source-dir = doc/source -build-dir = doc/build -all_files = 1 +[files] +packages = + novaclient -[upload_sphinx] -upload-dir = doc/build/html +[entry_points] +console_scripts = + nova = novaclient.shell:main diff --git a/setup.py b/setup.py index e0e68fa14..cd35c3c35 100644 --- a/setup.py +++ b/setup.py @@ -1,53 +1,20 @@ -# Copyright 2011 OpenStack, LLC +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # 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 +# 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. +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. # See the License for the specific language governing permissions and # limitations under the License. -import os import setuptools -import sys - -from novaclient.openstack.common import setup - - -def read_file(file_name): - return open(os.path.join(os.path.dirname(__file__), file_name)).read() - setuptools.setup( - name="python-novaclient", - version=setup.get_post_version('novaclient'), - author="Rackspace, based on work by Jacob Kaplan-Moss", - author_email="github@racklabs.com", - description="Client library for OpenStack Nova API.", - long_description=read_file("README.rst"), - license="Apache License, Version 2.0", - url="https://github.com/openstack/python-novaclient", - packages=setuptools.find_packages(exclude=['tests', 'tests.*']), - install_requires=setup.parse_requirements(), - test_suite="nose.collector", - cmdclass=setup.get_cmdclass(), - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Environment :: OpenStack", - "Intended Audience :: Developers", - "Intended Audience :: Information Technology", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python" - ], - entry_points={ - "console_scripts": ["nova = novaclient.shell:main"] - }, - data_files=[('novaclient', ['novaclient/versioninfo'])] -) + setup_requires=['pbr>=2.0.0'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..9ade238ad --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,9 @@ +coverage>=4.4.1 # Apache-2.0 +fixtures>=3.0.0 # Apache-2.0/BSD +requests-mock>=1.2.0 # Apache-2.0 +openstacksdk>=0.11.2 # Apache-2.0 +osprofiler>=1.4.0 # Apache-2.0 +stestr>=2.0.0 # Apache-2.0 +testscenarios>=0.4 # Apache-2.0/BSD +testtools>=2.2.0 # MIT +tempest>=17.1.0 # Apache-2.0 diff --git a/tests/fakes.py b/tests/fakes.py deleted file mode 100644 index 248214ff0..000000000 --- a/tests/fakes.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -A fake server that "responds" to API methods with pre-canned responses. - -All of these responses come from the spec, so if for some reason the spec's -wrong the tests might raise AssertionError. I've indicated in comments the -places where actual behavior differs from the spec. -""" - - -def assert_has_keys(dict, required=[], optional=[]): - keys = dict.keys() - for k in required: - try: - assert k in keys - except AssertionError: - extra_keys = set(keys).difference(set(required + optional)) - raise AssertionError("found unexpected keys: %s" % - list(extra_keys)) - - -class FakeClient(object): - - def assert_called(self, method, url, body=None, pos=-1): - """ - Assert than an API method was just called. - """ - expected = (method, url) - called = self.client.callstack[pos][0:2] - - assert self.client.callstack, \ - "Expected %s %s but no calls were made." % expected - - assert expected == called, 'Expected %s %s; got %s %s' % \ - (expected + called) - - if body is not None: - assert self.client.callstack[pos][2] == body - - def assert_called_anytime(self, method, url, body=None): - """ - Assert than an API method was called anytime in the test. - """ - expected = (method, url) - - assert self.client.callstack, \ - "Expected %s %s but no calls were made." % expected - - found = False - for entry in self.client.callstack: - if expected == entry[0:2]: - found = True - break - - assert found, 'Expected %s %s; got %s' % \ - (expected, self.client.callstack) - if body is not None: - try: - assert entry[2] == body - except AssertionError: - print entry[2] - print "!=" - print body - raise - - self.client.callstack = [] - - def clear_callstack(self): - self.client.callstack = [] - - def authenticate(self): - pass diff --git a/tests/test_auth_plugins.py b/tests/test_auth_plugins.py deleted file mode 100644 index 8a3a53238..000000000 --- a/tests/test_auth_plugins.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import httplib2 -import mock -import pkg_resources - -try: - import json -except ImportError: - import simplejson as json - -from novaclient import exceptions -from novaclient.v1_1 import client -from tests import utils - - -def mock_http_request(resp=None): - """Mock an HTTP Request.""" - if not resp: - resp = { - "access": { - "token": { - "expires": "12345", - "id": "FAKE_ID", - }, - "serviceCatalog": [ - { - "type": "compute", - "endpoints": [ - { - "region": "RegionOne", - "adminURL": "http://localhost:8774/v1.1", - "internalURL":"http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - ], - }, - } - - auth_response = httplib2.Response({ - "status": 200, - "body": json.dumps(resp), - }) - return mock.Mock(return_value=(auth_response, - json.dumps(resp))) - - -def requested_headers(cs): - """Return requested passed headers.""" - return { - 'User-Agent': cs.client.USER_AGENT, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - - -class AuthPluginTest(utils.TestCase): - def test_auth_system_success(self): - class MockEntrypoint(pkg_resources.EntryPoint): - def load(self): - return self.authenticate - - def authenticate(self, cls, auth_url): - cls._authenticate(auth_url, {"fake": "me"}) - - def mock_iter_entry_points(_type): - if _type == 'openstack.client.authenticate': - return [MockEntrypoint("fake", "fake", ["fake"])] - - mock_request = mock_http_request() - - @mock.patch.object(pkg_resources, "iter_entry_points", - mock_iter_entry_points) - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="fake", - no_cache=True) - cs.client.authenticate() - - headers = requested_headers(cs) - token_url = cs.client.auth_url + "/tokens" - - mock_request.assert_called_with(token_url, "POST", - headers=headers, - body='{"fake": "me"}') - - test_auth_call() - - def test_auth_system_not_exists(self): - def mock_iter_entry_points(_t): - return [pkg_resources.EntryPoint("fake", "fake", ["fake"])] - - mock_request = mock_http_request() - - @mock.patch.object(pkg_resources, "iter_entry_points", - mock_iter_entry_points) - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", auth_system="notexists", - no_cache=True) - self.assertRaises(exceptions.AuthSystemNotFound, - cs.client.authenticate) - - test_auth_call() - - def test_auth_system_defining_auth_url(self): - class MockAuthUrlEntrypoint(pkg_resources.EntryPoint): - def load(self): - return self.auth_url - - def auth_url(self): - return "http://faked/v2.0" - - class MockAuthenticateEntrypoint(pkg_resources.EntryPoint): - def load(self): - return self.authenticate - - def authenticate(self, cls, auth_url): - cls._authenticate(auth_url, {"fake": "me"}) - - def mock_iter_entry_points(_type): - if _type == 'openstack.client.auth_url': - return [MockAuthUrlEntrypoint("fakewithauthurl", - "fakewithauthurl.plugin", - ["auth_url"])] - elif _type == 'openstack.client.authenticate': - return [MockAuthenticateEntrypoint("fakewithauthurl", - "fakewithauthurl.plugin", - ["auth_url"])] - mock_request = mock_http_request() - - @mock.patch.object(pkg_resources, "iter_entry_points", - mock_iter_entry_points) - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - cs = client.Client("username", "password", "project_id", - auth_system="fakewithauthurl", - no_cache=True) - cs.client.authenticate() - self.assertEquals(cs.client.auth_url, "http://faked/v2.0") - - test_auth_call() - - def test_auth_system_raises_exception_when_missing_auth_url(self): - class MockAuthUrlEntrypoint(pkg_resources.EntryPoint): - def load(self): - return self.auth_url - - def auth_url(self): - return None - - def mock_iter_entry_points(_type): - return [MockAuthUrlEntrypoint("fakewithauthurl", - "fakewithauthurl.plugin", - ["auth_url"])] - - @mock.patch.object(pkg_resources, "iter_entry_points", - mock_iter_entry_points) - def test_auth_call(): - with self.assertRaises(exceptions.EndpointNotFound): - cs = client.Client("username", "password", "project_id", - auth_system="fakewithauthurl", - no_cache=True) - - test_auth_call() diff --git a/tests/test_base.py b/tests/test_base.py deleted file mode 100644 index 30bea5708..000000000 --- a/tests/test_base.py +++ /dev/null @@ -1,56 +0,0 @@ -from novaclient import base -from novaclient import exceptions -from novaclient.v1_1 import flavors -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class BaseTest(utils.TestCase): - - def test_resource_repr(self): - r = base.Resource(None, dict(foo="bar", baz="spam")) - self.assertEqual(repr(r), "") - - def test_getid(self): - self.assertEqual(base.getid(4), 4) - - class TmpObject(object): - id = 4 - self.assertEqual(base.getid(TmpObject), 4) - - def test_resource_lazy_getattr(self): - f = flavors.Flavor(cs.flavors, {'id': 1}) - self.assertEqual(f.name, '256 MB Server') - cs.assert_called('GET', '/flavors/1') - - # Missing stuff still fails after a second get - self.assertRaises(AttributeError, getattr, f, 'blahblah') - - def test_eq(self): - # Two resources of the same type with the same id: equal - r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) - r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) - self.assertEqual(r1, r2) - - # Two resoruces of different types: never equal - r1 = base.Resource(None, {'id': 1}) - r2 = flavors.Flavor(None, {'id': 1}) - self.assertNotEqual(r1, r2) - - # Two resources with no ID: equal if their info is equal - r1 = base.Resource(None, {'name': 'joe', 'age': 12}) - r2 = base.Resource(None, {'name': 'joe', 'age': 12}) - self.assertEqual(r1, r2) - - def test_findall_invalid_attribute(self): - # Make sure findall with an invalid attribute doesn't cause errors. - # The following should not raise an exception. - cs.flavors.findall(vegetable='carrot') - - # However, find() should raise an error - self.assertRaises(exceptions.NotFound, - cs.flavors.find, - vegetable='carrot') diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index a1a454655..000000000 --- a/tests/test_client.py +++ /dev/null @@ -1,26 +0,0 @@ - -import novaclient.client -import novaclient.v1_1.client -from tests import utils - - -class ClientTest(utils.TestCase): - - def setUp(self): - pass - - def test_get_client_class_v2(self): - output = novaclient.client.get_client_class('2') - self.assertEqual(output, novaclient.v1_1.client.Client) - - def test_get_client_class_v2_int(self): - output = novaclient.client.get_client_class(2) - self.assertEqual(output, novaclient.v1_1.client.Client) - - def test_get_client_class_v1_1(self): - output = novaclient.client.get_client_class('1.1') - self.assertEqual(output, novaclient.v1_1.client.Client) - - def test_get_client_class_unknown(self): - self.assertRaises(novaclient.exceptions.UnsupportedVersion, - novaclient.client.get_client_class, '0') diff --git a/tests/test_http.py b/tests/test_http.py deleted file mode 100644 index aab7104b5..000000000 --- a/tests/test_http.py +++ /dev/null @@ -1,110 +0,0 @@ -import httplib2 -import mock - -from novaclient import client -from novaclient import exceptions -from tests import utils - - -fake_response = httplib2.Response({"status": 200}) -fake_body = '{"hi": "there"}' -mock_request = mock.Mock(return_value=(fake_response, fake_body)) - -refused_response = httplib2.Response({"status": 400}) -refused_body = '[Errno 111] Connection refused' -refused_mock_request = mock.Mock( - return_value=( - refused_response, - refused_body, - ) -) - -bad_req_response = httplib2.Response({"status": 400}) -bad_req_body = '' -bad_req_mock_request = mock.Mock( - return_value=( - bad_req_response, - bad_req_body, - ) -) - - -def get_client(): - cl = client.HTTPClient("username", "password", - "project_id", "auth_test") - return cl - - -def get_authed_client(): - cl = get_client() - cl.management_url = "http://example.com" - cl.auth_token = "token" - return cl - - -class ClientTest(utils.TestCase): - - def test_get(self): - cl = get_authed_client() - - @mock.patch.object(httplib2.Http, "request", mock_request) - @mock.patch('time.time', mock.Mock(return_value=1234)) - def test_get_call(): - resp, body = cl.get("/hi") - headers = {"X-Auth-Token": "token", - "X-Auth-Project-Id": "project_id", - "User-Agent": cl.USER_AGENT, - 'Accept': 'application/json', - } - mock_request.assert_called_with("http://example.com/hi", - "GET", headers=headers) - # Automatic JSON parsing - self.assertEqual(body, {"hi": "there"}) - - test_get_call() - - def test_post(self): - cl = get_authed_client() - - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_post_call(): - cl.post("/hi", body=[1, 2, 3]) - headers = { - "X-Auth-Token": "token", - "X-Auth-Project-Id": "project_id", - "Content-Type": "application/json", - 'Accept': 'application/json', - "User-Agent": cl.USER_AGENT - } - mock_request.assert_called_with("http://example.com/hi", "POST", - headers=headers, body='[1, 2, 3]') - - test_post_call() - - def test_auth_failure(self): - cl = get_client() - - # response must not have x-server-management-url header - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate) - - test_auth_call() - - def test_connection_refused(self): - cl = get_client() - - @mock.patch.object(httplib2.Http, "request", refused_mock_request) - def test_refused_call(): - self.assertRaises(exceptions.ConnectionRefused, cl.get, "/hi") - - test_refused_call() - - def test_bad_request(self): - cl = get_client() - - @mock.patch.object(httplib2.Http, "request", bad_req_mock_request) - def test_refused_call(): - self.assertRaises(exceptions.BadRequest, cl.get, "/hi") - - test_refused_call() diff --git a/tests/test_service_catalog.py b/tests/test_service_catalog.py deleted file mode 100644 index 4ac7703fe..000000000 --- a/tests/test_service_catalog.py +++ /dev/null @@ -1,144 +0,0 @@ -from novaclient import exceptions -from novaclient import service_catalog -from tests import utils - - -# Taken directly from keystone/content/common/samples/auth.json -# Do not edit this structure. Instead, grab the latest from there. - -SERVICE_CATALOG = { - "access": { - "token": { - "id": "ab48a9efdfedb23ty3494", - "expires": "2010-11-01T03:32:15-05:00", - "tenant": { - "id": "345", - "name": "My Project" - } - }, - "user": { - "id": "123", - "name": "jqsmith", - "roles": [ - { - "id": "234", - "name": "compute:admin", - }, - { - "id": "235", - "name": "object-store:admin", - "tenantId": "1", - } - ], - "roles_links": [], - }, - "serviceCatalog": [ - { - "name": "Cloud Servers", - "type": "compute", - "endpoints": [ - { - # Tenant 1, no region, v1.0 - "tenantId": "1", - "publicURL": "https://compute1.host/v1/1", - "internalURL": "https://compute1.host/v1/1", - "versionId": "1.0", - "versionInfo": "https://compute1.host/v1.0/", - "versionList": "https://compute1.host/" - }, - { - # Tenant 2, with region, v1.1 - "tenantId": "2", - "publicURL": "https://compute1.host/v1.1/2", - "internalURL": "https://compute1.host/v1.1/2", - "region": "North", - "versionId": "1.1", - "versionInfo": "https://compute1.host/v1.1/", - "versionList": "https://compute1.host/" - }, - { - # Tenant 1, with region, v2.0 - "tenantId": "1", - "publicURL": "https://compute1.host/v2/1", - "internalURL": "https://compute1.host/v2/1", - "region": "North", - "versionId": "2", - "versionInfo": "https://compute1.host/v2/", - "versionList": "https://compute1.host/" - }, - ], - "endpoints_links": [], - }, - { - "name": "Nova Volumes", - "type": "volume", - "endpoints": [ - { - "tenantId": "1", - "publicURL": "https://volume1.host/v1/1", - "internalURL": "https://volume1.host/v1/1", - "region": "South", - "versionId": "1.0", - "versionInfo": "uri", - "versionList": "uri" - }, - { - "tenantId": "2", - "publicURL": "https://volume1.host/v1.1/2", - "internalURL": "https://volume1.host/v1.1/2", - "region": "South", - "versionId": "1.1", - "versionInfo": "https://volume1.host/v1.1/", - "versionList": "https://volume1.host/" - }, - ], - "endpoints_links": [ - { - "rel": "next", - "href": "https://identity1.host/v2.0/endpoints" - }, - ], - }, - ], - "serviceCatalog_links": [ - { - "rel": "next", - "href": "https://identity.host/v2.0/endpoints?session=2hfh8Ar", - }, - ], - }, -} - - -class ServiceCatalogTest(utils.TestCase): - def test_building_a_service_catalog(self): - sc = service_catalog.ServiceCatalog(SERVICE_CATALOG) - - self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, - service_type='compute') - self.assertEquals(sc.url_for('tenantId', '1', service_type='compute'), - "https://compute1.host/v2/1") - self.assertEquals(sc.url_for('tenantId', '2', service_type='compute'), - "https://compute1.host/v1.1/2") - - self.assertRaises(exceptions.EndpointNotFound, sc.url_for, - "region", "South", service_type='compute') - - def test_building_a_service_catalog_insensitive_case(self): - sc = service_catalog.ServiceCatalog(SERVICE_CATALOG) - # Matching south (and catalog has South). - self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, - 'region', 'south', service_type='volume') - - def test_alternate_service_type(self): - sc = service_catalog.ServiceCatalog(SERVICE_CATALOG) - - self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for, - service_type='volume') - self.assertEquals(sc.url_for('tenantId', '1', service_type='volume'), - "https://volume1.host/v1/1") - self.assertEquals(sc.url_for('tenantId', '2', service_type='volume'), - "https://volume1.host/v1.1/2") - - self.assertRaises(exceptions.EndpointNotFound, sc.url_for, - "region", "North", service_type='volume') diff --git a/tests/test_shell.py b/tests/test_shell.py deleted file mode 100644 index 87f477201..000000000 --- a/tests/test_shell.py +++ /dev/null @@ -1,70 +0,0 @@ -import cStringIO -import os -import httplib2 -import sys - -from novaclient import exceptions -import novaclient.shell -from tests import utils - - -class ShellTest(utils.TestCase): - - # Patch os.environ to avoid required auth info. - def setUp(self): - global _old_env - fake_env = { - 'OS_USERNAME': 'username', - 'OS_PASSWORD': 'password', - 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where', - } - _old_env, os.environ = os.environ, fake_env.copy() - - def shell(self, argstr): - orig = sys.stdout - try: - sys.stdout = cStringIO.StringIO() - _shell = novaclient.shell.OpenStackComputeShell() - _shell.main(argstr.split()) - except SystemExit: - exc_type, exc_value, exc_traceback = sys.exc_info() - self.assertEqual(exc_value.code, 0) - finally: - out = sys.stdout.getvalue() - sys.stdout.close() - sys.stdout = orig - - return out - - def tearDown(self): - global _old_env - os.environ = _old_env - - def test_help_unknown_command(self): - self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo') - - def test_debug(self): - httplib2.debuglevel = 0 - self.shell('--debug help') - assert httplib2.debuglevel == 1 - - def test_help(self): - required = [ - '^usage: ', - '(?m)^\s+root-password\s+Change the root password', - '(?m)^See "nova help COMMAND" for help on a specific command', - ] - help_text = self.shell('help') - for r in required: - self.assertRegexpMatches(help_text, r) - - def test_help_on_subcommand(self): - required = [ - '^usage: nova root-password', - '(?m)^Change the root password', - '(?m)^Positional arguments:', - ] - help_text = self.shell('help root-password') - for r in required: - self.assertRegexpMatches(help_text, r) diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 0eb64cff2..000000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,100 +0,0 @@ - -from novaclient import exceptions -from novaclient import utils -from novaclient import base -from tests import utils as test_utils - -UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' - - -class FakeResource(object): - NAME_ATTR = 'name' - - def __init__(self, _id, properties): - self.id = _id - try: - self.name = properties['name'] - except KeyError: - pass - - -class FakeManager(base.ManagerWithFind): - - resource_class = FakeResource - - resources = [ - FakeResource('1234', {'name': 'entity_one'}), - FakeResource(UUID, {'name': 'entity_two'}), - FakeResource('5678', {'name': '9876'}) - ] - - def get(self, resource_id): - for resource in self.resources: - if resource.id == str(resource_id): - return resource - raise exceptions.NotFound(resource_id) - - def list(self): - return self.resources - - -class FakeDisplayResource(object): - NAME_ATTR = 'display_name' - - def __init__(self, _id, properties): - self.id = _id - try: - self.display_name = properties['display_name'] - except KeyError: - pass - - -class FakeDisplayManager(FakeManager): - - resource_class = FakeDisplayResource - - resources = [ - FakeDisplayResource('4242', {'display_name': 'entity_three'}), - ] - - -class FindResourceTestCase(test_utils.TestCase): - - def setUp(self): - self.manager = FakeManager(None) - - def test_find_none(self): - """Test a few non-valid inputs""" - self.assertRaises(exceptions.CommandError, - utils.find_resource, - self.manager, - 'asdf') - self.assertRaises(exceptions.CommandError, - utils.find_resource, - self.manager, - None) - self.assertRaises(exceptions.CommandError, - utils.find_resource, - self.manager, - {}) - - def test_find_by_integer_id(self): - output = utils.find_resource(self.manager, 1234) - self.assertEqual(output, self.manager.get('1234')) - - def test_find_by_str_id(self): - output = utils.find_resource(self.manager, '1234') - self.assertEqual(output, self.manager.get('1234')) - - def test_find_by_uuid(self): - output = utils.find_resource(self.manager, UUID) - self.assertEqual(output, self.manager.get(UUID)) - - def test_find_by_str_name(self): - output = utils.find_resource(self.manager, 'entity_one') - self.assertEqual(output, self.manager.get('1234')) - - def test_find_by_str_displayname(self): - display_manager = FakeDisplayManager(None) - output = utils.find_resource(display_manager, 'entity_three') - self.assertEqual(output, display_manager.get('4242')) diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 7f1c5dc71..000000000 --- a/tests/utils.py +++ /dev/null @@ -1,5 +0,0 @@ -import unittest2 - - -class TestCase(unittest2.TestCase): - pass diff --git a/tests/v1_1/contrib/test_list_extensions.py b/tests/v1_1/contrib/test_list_extensions.py deleted file mode 100644 index 8b0651158..000000000 --- a/tests/v1_1/contrib/test_list_extensions.py +++ /dev/null @@ -1,21 +0,0 @@ -from novaclient import extension -from novaclient.v1_1.contrib import list_extensions - -from tests import utils -from tests.v1_1 import fakes - - -extensions = [ - extension.Extension(list_extensions.__name__.split(".")[-1], - list_extensions), -] -cs = fakes.FakeClient(extensions=extensions) - - -class ListExtensionsTests(utils.TestCase): - def test_list_extensions(self): - all_exts = cs.list_extensions.show_all() - cs.assert_called('GET', '/extensions') - self.assertTrue(len(all_exts) > 0) - for r in all_exts: - self.assertTrue(len(r.summary) > 0) diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py deleted file mode 100644 index b4f125865..000000000 --- a/tests/v1_1/fakes.py +++ /dev/null @@ -1,1052 +0,0 @@ -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# Copyright 2011 OpenStack, LLC -# -# 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 httplib2 -import urlparse - -from novaclient import client as base_client -from novaclient.v1_1 import client -from tests import fakes - - -class FakeClient(fakes.FakeClient, client.Client): - - def __init__(self, *args, **kwargs): - client.Client.__init__(self, 'username', 'password', - 'project_id', 'auth_url', - extensions=kwargs.get('extensions')) - self.client = FakeHTTPClient(**kwargs) - - -class FakeHTTPClient(base_client.HTTPClient): - - def __init__(self, **kwargs): - self.username = 'username' - self.password = 'password' - self.auth_url = 'auth_url' - self.callstack = [] - - def _cs_request(self, url, method, **kwargs): - # Check that certain things are called correctly - if method in ['GET', 'DELETE']: - assert 'body' not in kwargs - elif method == 'PUT': - assert 'body' in kwargs - - # Call the method - args = urlparse.parse_qsl(urlparse.urlparse(url)[4]) - kwargs.update(args) - munged_url = url.rsplit('?', 1)[0] - munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') - munged_url = munged_url.replace('-', '_') - - callback = "%s_%s" % (method.lower(), munged_url) - - if not hasattr(self, callback): - raise AssertionError('Called unknown API method: %s %s, ' - 'expected fakes method name: %s' % - (method, url, callback)) - - # Note the call - self.callstack.append((method, url, kwargs.get('body', None))) - - status, body = getattr(self, callback)(**kwargs) - if hasattr(status, 'items'): - return httplib2.Response(status), body - else: - return httplib2.Response({"status": status}), body - - # - # 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, { - "extensions": exts, - }) - - # - # Limits - # - - def get_limits(self, **kw): - return (200, {"limits": { - "rate": [ - { - "uri": "*", - "regex": ".*", - "limit": [ - { - "value": 10, - "verb": "POST", - "remaining": 2, - "unit": "MINUTE", - "next-available": "2011-12-15T22:42:45Z" - }, - { - "value": 10, - "verb": "PUT", - "remaining": 2, - "unit": "MINUTE", - "next-available": "2011-12-15T22:42:45Z" - }, - { - "value": 100, - "verb": "DELETE", - "remaining": 100, - "unit": "MINUTE", - "next-available": "2011-12-15T22:42:45Z" - } - ] - }, - { - "uri": "*/servers", - "regex": "^/servers", - "limit": [ - { - "verb": "POST", - "value": 25, - "remaining": 24, - "unit": "DAY", - "next-available": "2011-12-15T22:42:45Z" - } - ] - } - ], - "absolute": { - "maxTotalRAMSize": 51200, - "maxServerMeta": 5, - "maxImageMeta": 5, - "maxPersonality": 5, - "maxPersonalitySize": 10240 - }, - }, - }) - - # - # Servers - # - - def get_servers(self, **kw): - return (200, {"servers": [ - {'id': 1234, 'name': 'sample-server'}, - {'id': 5678, 'name': 'sample-server2'} - ]}) - - def get_servers_detail(self, **kw): - return (200, {"servers": [ - { - "id": 1234, - "name": "sample-server", - "image": { - "id": 2, - "name": "sample image", - }, - "flavor": { - "id": 1, - "name": "256 MB Server", - }, - "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", - "status": "BUILD", - "progress": 60, - "addresses": { - "public": [{ - "version": 4, - "addr": "1.2.3.4", - }, - { - "version": 4, - "addr": "5.6.7.8", - }], - "private": [{ - "version": 4, - "addr": "10.11.12.13", - }], - }, - "metadata": { - "Server Label": "Web Head 1", - "Image Version": "2.1" - } - }, - { - "id": 5678, - "name": "sample-server2", - "image": { - "id": 2, - "name": "sample image", - }, - "flavor": { - "id": 1, - "name": "256 MB Server", - }, - "hostId": "9e107d9d372bb6826bd81d3542a419d6", - "status": "ACTIVE", - "addresses": { - "public": [{ - "version": 4, - "addr": "4.5.6.7", - }, - { - "version": 4, - "addr": "5.6.9.8", - }], - "private": [{ - "version": 4, - "addr": "10.13.12.13", - }], - }, - "metadata": { - "Server Label": "DB 1" - } - } - ]}) - - def post_servers(self, body, **kw): - assert set(body.keys()) <= set(['server', 'os:scheduler_hints']) - fakes.assert_has_keys(body['server'], - required=['name', 'imageRef', 'flavorRef'], - optional=['metadata', 'personality']) - if 'personality' in body['server']: - for pfile in body['server']['personality']: - fakes.assert_has_keys(pfile, required=['path', 'contents']) - return (202, self.get_servers_1234()[1]) - - def get_servers_1234(self, **kw): - r = {'server': self.get_servers_detail()[1]['servers'][0]} - return (200, r) - - def get_servers_5678(self, **kw): - r = {'server': self.get_servers_detail()[1]['servers'][1]} - return (200, r) - - def put_servers_1234(self, body, **kw): - assert body.keys() == ['server'] - fakes.assert_has_keys(body['server'], optional=['name', 'adminPass']) - return (204, None) - - def delete_servers_1234(self, **kw): - return (202, None) - - def delete_servers_1234_metadata_test_key(self, **kw): - return (204, None) - - def delete_servers_1234_metadata_key1(self, **kw): - return (204, None) - - def delete_servers_1234_metadata_key2(self, **kw): - return (204, None) - - def post_servers_1234_metadata(self, **kw): - return (204, {'metadata': {'test_key': 'test_value'}}) - - def get_servers_1234_diagnostics(self, **kw): - return (200, {'data': 'Fake diagnostics'}) - - def get_servers_1234_actions(self, **kw): - return (200, {'actions': [ - { - 'action': 'rebuild', - 'error': None, - 'created_at': '2011-12-30 11:45:36' - }, - { - 'action': 'reboot', - 'error': 'Failed!', - 'created_at': '2011-12-30 11:40:29' - }, - ]}) - - # - # Server Addresses - # - - def get_servers_1234_ips(self, **kw): - return (200, {'addresses': - self.get_servers_1234()[1]['server']['addresses']}) - - def get_servers_1234_ips_public(self, **kw): - return (200, {'public': - self.get_servers_1234_ips()[1]['addresses']['public']}) - - def get_servers_1234_ips_private(self, **kw): - return (200, {'private': - self.get_servers_1234_ips()[1]['addresses']['private']}) - - def delete_servers_1234_ips_public_1_2_3_4(self, **kw): - return (202, None) - - # - # Server actions - # - - def post_servers_1234_action(self, body, **kw): - _body = None - resp = 202 - assert len(body.keys()) == 1 - action = body.keys()[0] - if action == 'reboot': - assert body[action].keys() == ['type'] - assert body[action]['type'] in ['HARD', 'SOFT'] - elif action == 'rebuild': - keys = body[action].keys() - if 'adminPass' in keys: - keys.remove('adminPass') - assert keys == ['imageRef'] - _body = self.get_servers_1234()[1] - elif action == 'resize': - assert body[action].keys() == ['flavorRef'] - elif action == 'confirmResize': - assert body[action] is None - # This one method returns a different response code - return (204, None) - elif action == 'revertResize': - assert body[action] is None - elif action == 'migrate': - assert body[action] is None - elif action == 'os-stop': - assert body[action] is None - elif action == 'os-start': - assert body[action] is None - elif action == 'rescue': - assert body[action] is None - elif action == 'unrescue': - assert body[action] is None - elif action == 'lock': - assert body[action] is None - elif action == 'unlock': - assert body[action] is None - elif action == 'addFixedIp': - assert body[action].keys() == ['networkId'] - elif action == 'removeFixedIp': - assert body[action].keys() == ['address'] - elif action == 'addFloatingIp': - assert body[action].keys() == ['address'] - elif action == 'removeFloatingIp': - assert body[action].keys() == ['address'] - elif action == 'createImage': - assert set(body[action].keys()) == set(['name', 'metadata']) - resp = dict(status=202, location="http://blah/images/456") - elif action == 'changePassword': - assert body[action].keys() == ['adminPass'] - elif action == 'os-getConsoleOutput': - assert body[action].keys() == ['length'] - return (202, {'output': 'foo'}) - elif action == 'os-getVNCConsole': - assert body[action].keys() == ['type'] - elif action == 'os-migrateLive': - assert set(body[action].keys()) == set(['host', - 'block_migration', - 'disk_over_commit']) - elif action == 'os-resetState': - assert body[action].keys() == ['state'] - elif action == 'addSecurityGroup': - assert body[action].keys() == ['name'] - elif action == 'removeSecurityGroup': - assert body[action].keys() == ['name'] - elif action == 'createBackup': - assert set(body[action].keys()) == set(['name', - 'backup_type', - 'rotation']) - else: - raise AssertionError("Unexpected server action: %s" % action) - return (resp, _body) - - # - # 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'}) - - # - # Flavors - # - - def get_flavors(self, **kw): - return (200, {'flavors': [ - {'id': 1, 'name': '256 MB Server'}, - {'id': 2, 'name': '512 MB Server'} - ]}) - - def get_flavors_detail(self, **kw): - return (200, {'flavors': [ - {'id': 1, 'name': '256 MB 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, - 'OS-FLV-EXT-DATA:ephemeral': 20, - 'os-flavor-access:is_public': False, - 'links': {}}, - ]}) - - def get_flavors_1(self, **kw): - return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][0]}) - - def get_flavors_2(self, **kw): - return (200, {'flavor': self.get_flavors_detail()[1]['flavors'][1]}) - - def get_flavors_3(self, **kw): - # Diablo has no ephemeral - return (200, {'flavor': {'id': 3, 'name': '256 MB Server', - 'ram': 256, 'disk': 10}}) - - def delete_flavors_flavordelete(self, **kw): - return (202, None) - - def post_flavors(self, body, **kw): - return (202, {'flavor': self.get_flavors_detail()[1]['flavors'][0]}) - - def get_flavors_1_os_extra_specs(self, **kw): - return (200, - {'extra_specs': {"k1": "v1"}}) - - def get_flavors_2_os_extra_specs(self, **kw): - return (200, - {'extra_specs': {"k2": "v2"}}) - - def post_flavors_1_os_extra_specs(self, body, **kw): - assert body.keys() == ['extra_specs'] - fakes.assert_has_keys(body['extra_specs'], - required=['k1']) - return (200, - {'extra_specs': {"k1": "v1"}}) - - def delete_flavors_1_os_extra_specs_k1(self, **kw): - return (204, None) - - # - # Flavor access - # - - def get_flavors_1_os_flavor_access(self, **kw): - return (404, None) - - def get_flavors_2_os_flavor_access(self, **kw): - return (200, {'flavor_access': [ - {'flavor_id': '2', 'tenant_id': 'proj1'}, - {'flavor_id': '2', 'tenant_id': 'proj2'} - ]}) - - def post_flavors_2_action(self, body, **kw): - return (202, self.get_flavors_2_os_flavor_access()[1]) - - # - # Floating ips - # - - def get_os_floating_ip_pools(self): - return (200, {'floating_ip_pools': [{'name': 'foo', 'name': 'bar'}]}) - - def get_os_floating_ips(self, **kw): - return (200, {'floating_ips': [ - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'}, - {'id': 2, 'fixed_ip': '10.0.0.2', 'ip': '11.0.0.2'}, - ]}) - - def get_os_floating_ips_1(self, **kw): - return (200, {'floating_ip': - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1'} - }) - - def post_os_floating_ips(self, body, **kw): - return (202, self.get_os_floating_ips_1()[1]) - - def post_os_floating_ips(self, body): - if body.get('pool'): - return (200, {'floating_ip': - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1', - 'pool': 'nova'}}) - else: - return (200, {'floating_ip': - {'id': 1, 'fixed_ip': '10.0.0.1', 'ip': '11.0.0.1', - 'pool': None}}) - - def delete_os_floating_ips_1(self, **kw): - return (204, None) - - def get_os_floating_ip_dns(self, **kw): - return (205, {'domain_entries': - [{'domain': 'example.org'}, - {'domain': 'example.com'}]}) - - def get_os_floating_ip_dns_testdomain_entries(self, **kw): - if kw.get('ip'): - return (205, {'dns_entries': - [{'dns_entry': - {'ip': kw.get('ip'), - 'name': "host1", - 'type': "A", - 'domain': 'testdomain'}}, - {'dns_entry': - {'ip': kw.get('ip'), - 'name': "host2", - 'type': "A", - 'domain': 'testdomain'}}]}) - else: - return (404, None) - - def get_os_floating_ip_dns_testdomain_entries_testname(self, **kw): - return (205, {'dns_entry': - {'ip': "10.10.10.10", - 'name': 'testname', - 'type': "A", - 'domain': 'testdomain'}}) - - def put_os_floating_ip_dns_testdomain(self, body, **kw): - if body['domain_entry']['scope'] == 'private': - fakes.assert_has_keys(body['domain_entry'], - required=['availability_zone', 'scope']) - elif body['domain_entry']['scope'] == 'public': - fakes.assert_has_keys(body['domain_entry'], - required=['project', 'scope']) - - else: - fakes.assert_has_keys(body['domain_entry'], - required=['project', 'scope']) - return (205, None) - - def put_os_floating_ip_dns_testdomain_entries_testname(self, body, **kw): - fakes.assert_has_keys(body['dns_entry'], - required=['ip', 'dns_type']) - return (205, None) - - def delete_os_floating_ip_dns_testdomain(self, **kw): - return (200, None) - - def delete_os_floating_ip_dns_testdomain_entries_testname(self, **kw): - return (200, None) - - # - # Images - # - def get_images(self, **kw): - return (200, {'images': [ - {'id': 1, 'name': 'CentOS 5.2'}, - {'id': 2, 'name': 'My Server Backup'} - ]}) - - def get_images_detail(self, **kw): - return (200, {'images': [ - { - 'id': 1, - 'name': 'CentOS 5.2', - "updated": "2010-10-10T12:00:00Z", - "created": "2010-08-10T12:00:00Z", - "status": "ACTIVE", - "metadata": { - "test_key": "test_value", - }, - "links": {}, - }, - { - "id": 743, - "name": "My Server Backup", - "serverId": 1234, - "updated": "2010-10-10T12:00:00Z", - "created": "2010-08-10T12:00:00Z", - "status": "SAVING", - "progress": 80, - "links": {}, - } - ]}) - - def get_images_1(self, **kw): - return (200, {'image': self.get_images_detail()[1]['images'][0]}) - - def get_images_2(self, **kw): - return (200, {'image': self.get_images_detail()[1]['images'][1]}) - - def post_images(self, body, **kw): - assert body.keys() == ['image'] - fakes.assert_has_keys(body['image'], required=['serverId', 'name']) - return (202, self.get_images_1()[1]) - - def post_images_1_metadata(self, body, **kw): - assert body.keys() == ['metadata'] - fakes.assert_has_keys(body['metadata'], - required=['test_key']) - return (200, - {'metadata': self.get_images_1()[1]['image']['metadata']}) - - def delete_images_1(self, **kw): - return (204, None) - - def delete_images_1_metadata_test_key(self, **kw): - return (204, None) - - # - # Keypairs - # - def get_os_keypairs(self, *kw): - return (200, {"keypairs": [ - {'fingerprint': 'FAKE_KEYPAIR', 'name': 'test'} - ]}) - - def delete_os_keypairs_test(self, **kw): - return (202, None) - - def post_os_keypairs(self, body, **kw): - assert body.keys() == ['keypair'] - fakes.assert_has_keys(body['keypair'], - required=['name']) - r = {'keypair': self.get_os_keypairs()[1]['keypairs'][0]} - return (202, r) - - # - # Virtual Interfaces - # - def get_servers_1234_os_virtual_interfaces(self, **kw): - return (200, {"virtual_interfaces": [ - {'id': 'fakeid', 'mac_address': 'fakemac'} - ]}) - - # - # Quotas - # - - def get_os_quota_sets_test(self, **kw): - return (200, {'quota_set': { - 'tenant_id': 'test', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'volumes': 1, - 'gigabytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1}}) - - def get_os_quota_sets_test_defaults(self): - return (200, {'quota_set': { - 'tenant_id': 'test', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'volumes': 1, - 'gigabytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1}}) - - def put_os_quota_sets_test(self, body, **kw): - assert body.keys() == ['quota_set'] - fakes.assert_has_keys(body['quota_set'], - required=['tenant_id']) - return (200, {'quota_set': { - 'tenant_id': 'test', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'volumes': 2, - 'gigabytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1}}) - - # - # Quota Classes - # - - def get_os_quota_class_sets_test(self, **kw): - return (200, {'quota_class_set': { - 'class_name': 'test', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'volumes': 1, - 'gigabytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1}}) - - def put_os_quota_class_sets_test(self, body, **kw): - assert body.keys() == ['quota_class_set'] - fakes.assert_has_keys(body['quota_class_set'], - required=['class_name']) - return (200, {'quota_class_set': { - 'class_name': 'test', - 'metadata_items': [], - 'injected_file_content_bytes': 1, - 'volumes': 2, - 'gigabytes': 1, - 'ram': 1, - 'floating_ips': 1, - 'instances': 1, - 'injected_files': 1, - 'cores': 1}}) - - # - # Security Groups - # - def get_os_security_groups(self, **kw): - return (200, {"security_groups": [ - {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP'} - ]}) - - def get_os_security_groups_1(self, **kw): - return (200, {"security_group": - {'id': 1, 'name': 'test', 'description': 'FAKE_SECURITY_GROUP'} - }) - - def delete_os_security_groups_1(self, **kw): - return (202, None) - - def post_os_security_groups(self, body, **kw): - assert body.keys() == ['security_group'] - fakes.assert_has_keys(body['security_group'], - required=['name', 'description']) - r = {'security_group': - self.get_os_security_groups()[1]['security_groups'][0]} - return (202, r) - - # - # Security Group Rules - # - def get_os_security_group_rules(self, **kw): - return (200, {"security_group_rules": [ - {'id': 1, 'parent_group_id': 1, 'group_id': 2, - 'ip_protocol': 'TCP', 'from_port': '22', 'to_port': 22, - 'cidr': '10.0.0.0/8'} - ]}) - - def delete_os_security_group_rules_1(self, **kw): - return (202, None) - - def post_os_security_group_rules(self, body, **kw): - assert body.keys() == ['security_group_rule'] - fakes.assert_has_keys(body['security_group_rule'], - required=['parent_group_id'], - optional=['group_id', 'ip_protocol', 'from_port', - 'to_port', 'cidr']) - r = {'security_group_rule': - self.get_os_security_group_rules()[1]['security_group_rules'][0]} - return (202, r) - - # - # Tenant Usage - # - def get_os_simple_tenant_usage(self, **kw): - return (200, {u'tenant_usages': [{ - u'total_memory_mb_usage': 25451.762807466665, - u'total_vcpus_usage': 49.71047423333333, - u'total_hours': 49.71047423333333, - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'stop': u'2012-01-22 19:48:41.750722', - u'server_usages': [{ - u'hours': 49.71047423333333, - u'uptime': 27035, u'local_gb': 0, u'ended_at': None, - u'name': u'f15image1', - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'vcpus': 1, u'memory_mb': 512, u'state': u'active', - u'flavor': u'm1.tiny', - u'started_at': u'2012-01-20 18:06:06.479998'}], - u'start': u'2011-12-25 19:48:41.750687', - u'total_local_gb_usage': 0.0}]}) - - def get_os_simple_tenant_usage_tenantfoo(self, **kw): - return (200, {u'tenant_usage': { - u'total_memory_mb_usage': 25451.762807466665, - u'total_vcpus_usage': 49.71047423333333, - u'total_hours': 49.71047423333333, - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'stop': u'2012-01-22 19:48:41.750722', - u'server_usages': [{ - u'hours': 49.71047423333333, - u'uptime': 27035, u'local_gb': 0, u'ended_at': None, - u'name': u'f15image1', - u'tenant_id': u'7b0a1d73f8fb41718f3343c207597869', - u'vcpus': 1, u'memory_mb': 512, u'state': u'active', - u'flavor': u'm1.tiny', - u'started_at': u'2012-01-20 18:06:06.479998'}], - u'start': u'2011-12-25 19:48:41.750687', - u'total_local_gb_usage': 0.0}}) - - # - # 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'}}) - - # - # Aggregates - # - def get_os_aggregates(self, *kw): - return (200, {"aggregates": [ - {'id':'1', - 'name': 'test', - 'availability_zone': 'nova1'}, - {'id':'2', - 'name': 'test2', - 'availability_zone': 'nova1'}, - ]}) - - def _return_aggregate(self): - r = {'aggregate': self.get_os_aggregates()[1]['aggregates'][0]} - return (200, r) - - def get_os_aggregates_1(self, **kw): - return self._return_aggregate() - - def post_os_aggregates(self, body, **kw): - return self._return_aggregate() - - def put_os_aggregates_1(self, body, **kw): - return self._return_aggregate() - - def put_os_aggregates_2(self, body, **kw): - return self._return_aggregate() - - def post_os_aggregates_1_action(self, body, **kw): - return self._return_aggregate() - - def post_os_aggregates_2_action(self, body, **kw): - return self._return_aggregate() - - def delete_os_aggregates_1(self, **kw): - return (202, None) - - # - # Hosts - # - def get_os_hosts_host(self, *kw): - return (200, {'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}}]}) - - 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 get_os_hosts_sample_host(self, *kw): - return (200, {'host': [{'resource': {'host': 'sample_host'}}], }) - - 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_startup(self, **kw): - return (200, {'host': 'sample_host', - 'power_action': 'startup'}) - - def get_os_hosts_sample_host_reboot(self, **kw): - return (200, {'host': 'sample_host', - 'power_action': 'reboot'}) - - def get_os_hosts_sample_host_shutdown(self, **kw): - return (200, {'host': 'sample_host', - 'power_action': 'shutdown'}) - - def put_os_hosts_sample_host(self, body, **kw): - result = {'host': 'dummy'} - result.update(body) - return (200, result) - - def get_os_hypervisors(self, **kw): - return (200, {"hypervisors": [ - {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'}, - ]}) - - def get_os_hypervisors_detail(self, **kw): - return (200, {"hypervisors": [ - {'id': 1234, - 'service': {'id': 1, 'host': 'compute1'}, - 'vcpus': 4, - 'memory_mb': 10 * 1024, - 'local_gb': 250, - 'vcpus_used': 2, - 'memory_mb_used': 5 * 1024, - 'local_gb_used': 125, - 'hypervisor_type': "xen", - 'hypervisor_version': 3, - 'hypervisor_hostname': "hyper1", - 'free_ram_mb': 5 * 1024, - 'free_disk_gb': 125, - 'current_workload': 2, - 'running_vms': 2, - 'cpu_info': 'cpu_info', - 'disk_available_least': 100}, - {'id': 2, - 'service': {'id': 2, 'host': "compute2"}, - 'vcpus': 4, - 'memory_mb': 10 * 1024, - 'local_gb': 250, - 'vcpus_used': 2, - 'memory_mb_used': 5 * 1024, - 'local_gb_used': 125, - 'hypervisor_type': "xen", - 'hypervisor_version': 3, - 'hypervisor_hostname': "hyper2", - 'free_ram_mb': 5 * 1024, - 'free_disk_gb': 125, - 'current_workload': 2, - 'running_vms': 2, - 'cpu_info': 'cpu_info', - 'disk_available_least': 100} - ]}) - - def get_os_hypervisors_statistics(self, **kw): - return (200, {"hypervisor_statistics": { - 'count': 2, - 'vcpus': 8, - 'memory_mb': 20 * 1024, - 'local_gb': 500, - 'vcpus_used': 4, - 'memory_mb_used': 10 * 1024, - 'local_gb_used': 250, - 'free_ram_mb': 10 * 1024, - 'free_disk_gb': 250, - 'current_workload': 4, - 'running_vms': 4, - 'disk_available_least': 200, - }}) - - def get_os_hypervisors_hyper_search(self, **kw): - return (200, {'hypervisors': [ - {'id': 1234, 'hypervisor_hostname': 'hyper1'}, - {'id': 5678, 'hypervisor_hostname': 'hyper2'} - ]}) - - def get_os_hypervisors_hyper_servers(self, **kw): - return (200, {'hypervisors': [ - {'id': 1234, - 'hypervisor_hostname': 'hyper1', - 'servers': [ - {'name': 'inst1', 'uuid': 'uuid1'}, - {'name': 'inst2', 'uuid': 'uuid2'} - ]}, - {'id': 5678, - 'hypervisor_hostname': 'hyper2', - 'servers': [ - {'name': 'inst3', 'uuid': 'uuid3'}, - {'name': 'inst4', 'uuid': 'uuid4'} - ]} - ]}) - - def get_os_hypervisors_1234(self, **kw): - return (200, {'hypervisor': - {'id': 1234, - 'service': {'id': 1, 'host': 'compute1'}, - 'vcpus': 4, - 'memory_mb': 10 * 1024, - 'local_gb': 250, - 'vcpus_used': 2, - 'memory_mb_used': 5 * 1024, - 'local_gb_used': 125, - 'hypervisor_type': "xen", - 'hypervisor_version': 3, - 'hypervisor_hostname': "hyper1", - 'free_ram_mb': 5 * 1024, - 'free_disk_gb': 125, - 'current_workload': 2, - 'running_vms': 2, - 'cpu_info': 'cpu_info', - 'disk_available_least': 100}}) - - def get_os_hypervisors_1234_uptime(self, **kw): - return (200, {'hypervisor': - {'id': 1234, - 'hypervisor_hostname': "hyper1", - 'uptime': "fake uptime"}}) - - def get_os_networks(self, **kw): - return (200, {'networks': [{"label": "1", "cidr": "10.0.0.0/24"}]}) - - def get_os_networks_1(self, **kw): - return (200, {'network': {"label": "1", "cidr": "10.0.0.0/24"}}) - - def post_os_networks(self, **kw): - return (202, {'network': kw}) - - def delete_os_networks_networkdelete(self, **kw): - return (202, None) - - def post_os_networks_add(self, **kw): - return (202, None) - - def post_os_networks_networkdisassociate_action(self, **kw): - return (202, None) diff --git a/tests/v1_1/test_aggregates.py b/tests/v1_1/test_aggregates.py deleted file mode 100644 index ebab2cd9e..000000000 --- a/tests/v1_1/test_aggregates.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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.v1_1 import aggregates -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class AggregatesTest(utils.TestCase): - - def test_list_aggregates(self): - result = cs.aggregates.list() - cs.assert_called('GET', '/os-aggregates') - for aggregate in result: - self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) - - def test_create_aggregate(self): - body = {"aggregate": {"name": "test", "availability_zone": "nova1"}} - aggregate = cs.aggregates.create("test", "nova1") - cs.assert_called('POST', '/os-aggregates', body) - self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) - - def test_get_details(self): - aggregate = cs.aggregates.get_details("1") - cs.assert_called('GET', '/os-aggregates/1') - self.assertTrue(isinstance(aggregate, aggregates.Aggregate)) - - aggregate2 = cs.aggregates.get_details(aggregate) - cs.assert_called('GET', '/os-aggregates/1') - self.assertTrue(isinstance(aggregate2, aggregates.Aggregate)) - - def test_update(self): - aggregate = cs.aggregates.get_details("1") - values = {"name": "foo"} - body = {"aggregate": values} - - result1 = aggregate.update(values) - cs.assert_called('PUT', '/os-aggregates/1', body) - self.assertTrue(isinstance(result1, aggregates.Aggregate)) - - result2 = cs.aggregates.update(2, values) - cs.assert_called('PUT', '/os-aggregates/2', body) - self.assertTrue(isinstance(result2, aggregates.Aggregate)) - - def test_update_with_availability_zone(self): - aggregate = cs.aggregates.get_details("1") - values = {"name": "foo", "availability_zone": "new_zone"} - body = {"aggregate": values} - - result3 = cs.aggregates.update(aggregate, values) - cs.assert_called('PUT', '/os-aggregates/1', body) - self.assertTrue(isinstance(result3, aggregates.Aggregate)) - - def test_add_host(self): - aggregate = cs.aggregates.get_details("1") - host = "host1" - body = {"add_host": {"host": "host1"}} - - result1 = aggregate.add_host(host) - cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result1, aggregates.Aggregate)) - - result2 = cs.aggregates.add_host("2", host) - cs.assert_called('POST', '/os-aggregates/2/action', body) - self.assertTrue(isinstance(result2, aggregates.Aggregate)) - - result3 = cs.aggregates.add_host(aggregate, host) - cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result3, aggregates.Aggregate)) - - def test_remove_host(self): - aggregate = cs.aggregates.get_details("1") - host = "host1" - body = {"remove_host": {"host": "host1"}} - - result1 = aggregate.remove_host(host) - cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result1, aggregates.Aggregate)) - - result2 = cs.aggregates.remove_host("2", host) - cs.assert_called('POST', '/os-aggregates/2/action', body) - self.assertTrue(isinstance(result2, aggregates.Aggregate)) - - result3 = cs.aggregates.remove_host(aggregate, host) - cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result3, aggregates.Aggregate)) - - def test_set_metadata(self): - aggregate = cs.aggregates.get_details("1") - metadata = {"foo": "bar"} - body = {"set_metadata": {"metadata": metadata}} - - result1 = aggregate.set_metadata(metadata) - cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result1, aggregates.Aggregate)) - - result2 = cs.aggregates.set_metadata(2, metadata) - cs.assert_called('POST', '/os-aggregates/2/action', body) - self.assertTrue(isinstance(result2, aggregates.Aggregate)) - - result3 = cs.aggregates.set_metadata(aggregate, metadata) - cs.assert_called('POST', '/os-aggregates/1/action', body) - self.assertTrue(isinstance(result3, aggregates.Aggregate)) - - def test_delete_aggregate(self): - aggregate = cs.aggregates.list()[0] - aggregate.delete() - cs.assert_called('DELETE', '/os-aggregates/1') - - cs.aggregates.delete('1') - cs.assert_called('DELETE', '/os-aggregates/1') - - cs.aggregates.delete(aggregate) - cs.assert_called('DELETE', '/os-aggregates/1') diff --git a/tests/v1_1/test_auth.py b/tests/v1_1/test_auth.py deleted file mode 100644 index f1ae081f8..000000000 --- a/tests/v1_1/test_auth.py +++ /dev/null @@ -1,304 +0,0 @@ -import httplib2 -import json -import mock - -from novaclient.v1_1 import client -from novaclient import exceptions -from tests import utils - - -def to_http_response(resp_dict): - """Converts dict of response attributes to httplib response.""" - resp = httplib2.Response(resp_dict) - for k, v in resp_dict['headers'].items(): - resp[k] = v - return resp - - -class AuthenticateAgainstKeystoneTests(utils.TestCase): - def test_authenticate_success(self): - cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute', - no_cache=True) - resp = { - "access": { - "token": { - "expires": "12345", - "id": "FAKE_ID", - }, - "serviceCatalog": [ - { - "type": "compute", - "endpoints": [ - { - "region": "RegionOne", - "adminURL": "http://localhost:8774/v1.1", - "internalURL": "http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - ], - }, - } - auth_response = httplib2.Response({ - "status": 200, - "body": json.dumps(resp), - }) - - mock_request = mock.Mock(return_value=(auth_response, - json.dumps(resp))) - - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - headers = { - 'User-Agent': cs.client.USER_AGENT, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - body = { - 'auth': { - 'passwordCredentials': { - 'username': cs.client.user, - 'password': cs.client.password, - }, - 'tenantName': cs.client.projectid, - }, - } - - token_url = cs.client.auth_url + "/tokens" - mock_request.assert_called_with(token_url, "POST", - headers=headers, - body=json.dumps(body)) - - endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] - public_url = endpoints[0]["publicURL"].rstrip('/') - self.assertEqual(cs.client.management_url, public_url) - token_id = resp["access"]["token"]["id"] - self.assertEqual(cs.client.auth_token, token_id) - - test_auth_call() - - def test_authenticate_failure(self): - cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", no_cache=True) - resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} - auth_response = httplib2.Response({ - "status": 401, - "body": json.dumps(resp), - }) - - mock_request = mock.Mock(return_value=(auth_response, - json.dumps(resp))) - - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) - - test_auth_call() - - def test_auth_redirect(self): - cs = client.Client("username", "password", "project_id", - "auth_url/v1.0", service_type='compute', - no_cache=True) - dict_correct_response = { - "access": { - "token": { - "expires": "12345", - "id": "FAKE_ID", - }, - "serviceCatalog": [ - { - "type": "compute", - "endpoints": [ - { - "adminURL": "http://localhost:8774/v1.1", - "region": "RegionOne", - "internalURL": "http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - ], - }, - } - correct_response = json.dumps(dict_correct_response) - dict_responses = [ - {"headers": {'location':'http://127.0.0.1:5001'}, - "status": 305, - "body": "Use proxy"}, - # Configured on admin port, nova redirects to v2.0 port. - # When trying to connect on it, keystone auth succeed by v1.0 - # protocol (through headers) but tokens are being returned in - # body (looks like keystone bug). Leaved for compatibility. - {"headers": {}, - "status": 200, - "body": correct_response}, - {"headers": {}, - "status": 200, - "body": correct_response} - ] - - responses = [(to_http_response(resp), resp['body']) \ - for resp in dict_responses] - - def side_effect(*args, **kwargs): - return responses.pop(0) - - mock_request = mock.Mock(side_effect=side_effect) - - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - headers = { - 'User-Agent': cs.client.USER_AGENT, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - } - body = { - 'auth': { - 'passwordCredentials': { - 'username': cs.client.user, - 'password': cs.client.password, - }, - 'tenantName': cs.client.projectid, - }, - } - - token_url = cs.client.auth_url + "/tokens" - mock_request.assert_called_with(token_url, "POST", - headers=headers, - body=json.dumps(body)) - - resp = dict_correct_response - endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] - public_url = endpoints[0]["publicURL"].rstrip('/') - self.assertEqual(cs.client.management_url, public_url) - token_id = resp["access"]["token"]["id"] - self.assertEqual(cs.client.auth_token, token_id) - - test_auth_call() - - def test_ambiguous_endpoints(self): - cs = client.Client("username", "password", "project_id", - "auth_url/v2.0", service_type='compute', - no_cache=True) - resp = { - "access": { - "token": { - "expires": "12345", - "id": "FAKE_ID", - }, - "serviceCatalog": [ - { - "adminURL": "http://localhost:8774/v1.1", - "type": "compute", - "name": "Compute CLoud", - "endpoints": [ - { - "region": "RegionOne", - "internalURL": "http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - { - "adminURL": "http://localhost:8774/v1.1", - "type": "compute", - "name": "Hyper-compute Cloud", - "endpoints": [ - { - "internalURL": "http://localhost:8774/v1.1", - "publicURL": "http://localhost:8774/v1.1/", - }, - ], - }, - ], - }, - } - auth_response = httplib2.Response({ - "status": 200, - "body": json.dumps(resp), - }) - - mock_request = mock.Mock(return_value=(auth_response, - json.dumps(resp))) - - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - self.assertRaises(exceptions.AmbiguousEndpoints, - cs.client.authenticate) - - test_auth_call() - - -class AuthenticationTests(utils.TestCase): - def test_authenticate_success(self): - cs = client.Client("username", "password", "project_id", "auth_url", - no_cache=True) - management_url = 'https://localhost/v1.1/443470' - auth_response = httplib2.Response({ - 'status': 204, - 'x-server-management-url': management_url, - 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1', - }) - mock_request = mock.Mock(return_value=(auth_response, None)) - - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - cs.client.authenticate() - headers = { - 'Accept': 'application/json', - 'X-Auth-User': 'username', - 'X-Auth-Key': 'password', - 'X-Auth-Project-Id': 'project_id', - 'User-Agent': cs.client.USER_AGENT - } - mock_request.assert_called_with(cs.client.auth_url, 'GET', - headers=headers) - self.assertEqual(cs.client.management_url, - auth_response['x-server-management-url']) - self.assertEqual(cs.client.auth_token, - auth_response['x-auth-token']) - - test_auth_call() - - def test_authenticate_failure(self): - cs = client.Client("username", "password", "project_id", "auth_url", - no_cache=True) - auth_response = httplib2.Response({'status': 401}) - mock_request = mock.Mock(return_value=(auth_response, None)) - - @mock.patch.object(httplib2.Http, "request", mock_request) - def test_auth_call(): - self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) - - test_auth_call() - - def test_auth_automatic(self): - cs = client.Client("username", "password", "project_id", "auth_url", - no_cache=True) - http_client = cs.client - http_client.management_url = '' - mock_request = mock.Mock(return_value=(None, None)) - - @mock.patch.object(http_client, 'request', mock_request) - @mock.patch.object(http_client, 'authenticate') - def test_auth_call(m): - http_client.get('/') - m.assert_called() - mock_request.assert_called() - - test_auth_call() - - def test_auth_manual(self): - cs = client.Client("username", "password", "project_id", "auth_url", - no_cache=True) - - @mock.patch.object(cs.client, 'authenticate') - def test_auth_call(m): - cs.authenticate() - m.assert_called() - - test_auth_call() diff --git a/tests/v1_1/test_certs.py b/tests/v1_1/test_certs.py deleted file mode 100644 index dc6e76784..000000000 --- a/tests/v1_1/test_certs.py +++ /dev/null @@ -1,20 +0,0 @@ -from novaclient import exceptions -from novaclient.v1_1 import certs -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class FlavorsTest(utils.TestCase): - - def test_create_cert(self): - cert = cs.certs.create() - cs.assert_called('POST', '/os-certificates') - self.assertTrue(isinstance(cert, certs.Certificate)) - - def test_get_root_cert(self): - cert = cs.certs.get() - cs.assert_called('GET', '/os-certificates/root') - self.assertTrue(isinstance(cert, certs.Certificate)) diff --git a/tests/v1_1/test_cloudpipe.py b/tests/v1_1/test_cloudpipe.py deleted file mode 100644 index 82a750f0a..000000000 --- a/tests/v1_1/test_cloudpipe.py +++ /dev/null @@ -1,22 +0,0 @@ -from novaclient import exceptions -from novaclient.v1_1 import cloudpipe -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class CloudpipeTest(utils.TestCase): - - def test_list_cloudpipes(self): - cp = cs.cloudpipe.list() - cs.assert_called('GET', '/os-cloudpipe') - [self.assertTrue(isinstance(c, cloudpipe.Cloudpipe)) for c in cp] - - def test_create(self): - project = "test" - cp = cs.cloudpipe.create(project) - body = {'cloudpipe': {'project_id': project}} - cs.assert_called('POST', '/os-cloudpipe', body) - self.assertTrue(isinstance(cp, str)) diff --git a/tests/v1_1/test_flavor_access.py b/tests/v1_1/test_flavor_access.py deleted file mode 100644 index 2c137291a..000000000 --- a/tests/v1_1/test_flavor_access.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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.v1_1 import flavor_access -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class FlavorAccessTest(utils.TestCase): - - def test_list_access_by_flavor_private(self): - kwargs = {'flavor': cs.flavors.get(2)} - r = cs.flavor_access.list(**kwargs) - cs.assert_called('GET', '/flavors/2/os-flavor-access') - [self.assertTrue(isinstance(a, flavor_access.FlavorAccess)) for a in r] - - def test_add_tenant_access(self): - flavor = cs.flavors.get(2) - tenant = 'proj2' - r = cs.flavor_access.add_tenant_access(flavor, tenant) - - body = { - "addTenantAccess": { - "tenant": "proj2" - } - } - - cs.assert_called('POST', '/flavors/2/action', body) - [self.assertTrue(isinstance(a, flavor_access.FlavorAccess)) for a in r] - - def test_remove_tenant_access(self): - flavor = cs.flavors.get(2) - tenant = 'proj2' - r = cs.flavor_access.remove_tenant_access(flavor, tenant) - - body = { - "removeTenantAccess": { - "tenant": "proj2" - } - } - - cs.assert_called('POST', '/flavors/2/action', body) - [self.assertTrue(isinstance(a, flavor_access.FlavorAccess)) for a in r] diff --git a/tests/v1_1/test_flavors.py b/tests/v1_1/test_flavors.py deleted file mode 100644 index 6a766d97f..000000000 --- a/tests/v1_1/test_flavors.py +++ /dev/null @@ -1,127 +0,0 @@ -from novaclient import exceptions -from novaclient.v1_1 import flavors -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class FlavorsTest(utils.TestCase): - - def test_list_flavors(self): - fl = cs.flavors.list() - cs.assert_called('GET', '/flavors/detail') - [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl] - - def test_list_flavors_undetailed(self): - fl = cs.flavors.list(detailed=False) - cs.assert_called('GET', '/flavors') - [self.assertTrue(isinstance(f, flavors.Flavor)) for f in fl] - - def test_get_flavor_details(self): - f = cs.flavors.get(1) - cs.assert_called('GET', '/flavors/1') - self.assertTrue(isinstance(f, flavors.Flavor)) - self.assertEqual(f.ram, 256) - self.assertEqual(f.disk, 10) - self.assertEqual(f.ephemeral, 10) - self.assertEqual(f.is_public, True) - - def test_get_flavor_details_diablo(self): - f = cs.flavors.get(3) - cs.assert_called('GET', '/flavors/3') - self.assertTrue(isinstance(f, flavors.Flavor)) - self.assertEqual(f.ram, 256) - self.assertEqual(f.disk, 10) - self.assertEqual(f.ephemeral, 'N/A') - self.assertEqual(f.is_public, 'N/A') - - def test_find(self): - f = cs.flavors.find(ram=256) - cs.assert_called('GET', '/flavors/detail') - self.assertEqual(f.name, '256 MB Server') - - f = cs.flavors.find(disk=20) - self.assertEqual(f.name, '512 MB Server') - - self.assertRaises(exceptions.NotFound, cs.flavors.find, disk=12345) - - def test_create(self): - f = cs.flavors.create("flavorcreate", 512, 1, 10, 1234, ephemeral=10, - is_public=False) - - body = { - "flavor": { - "name": "flavorcreate", - "ram": 512, - "vcpus": 1, - "disk": 10, - "OS-FLV-EXT-DATA:ephemeral": 10, - "id": 1234, - "swap": 0, - "rxtx_factor": 1, - "os-flavor-access:is_public": False, - } - } - - cs.assert_called('POST', '/flavors', body) - self.assertTrue(isinstance(f, flavors.Flavor)) - - def test_create_ephemeral_ispublic_defaults(self): - f = cs.flavors.create("flavorcreate", 512, 1, 10, 1234) - - body = { - "flavor": { - "name": "flavorcreate", - "ram": 512, - "vcpus": 1, - "disk": 10, - "OS-FLV-EXT-DATA:ephemeral": 0, - "id": 1234, - "swap": 0, - "rxtx_factor": 1, - "os-flavor-access:is_public": True, - } - } - - cs.assert_called('POST', '/flavors', body) - self.assertTrue(isinstance(f, flavors.Flavor)) - - def test_invalid_parameters_create(self): - self.assertRaises(exceptions.CommandError, cs.flavors.create, - "flavorcreate", "invalid", 1, 10, 1234, swap=0, - ephemeral=0, rxtx_factor=1, is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, - "flavorcreate", 512, "invalid", 10, 1234, swap=0, - ephemeral=0, rxtx_factor=1, is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, - "flavorcreate", 512, 1, "invalid", 1234, swap=0, - ephemeral=0, rxtx_factor=1, is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, - "flavorcreate", 512, 1, 10, 1234, swap="invalid", - ephemeral=0, rxtx_factor=1, is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, - "flavorcreate", 512, 1, 10, 1234, swap=0, - ephemeral="invalid", rxtx_factor=1, is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, - "flavorcreate", 512, 1, 10, 1234, swap=0, - ephemeral=0, rxtx_factor="invalid", is_public=True) - self.assertRaises(exceptions.CommandError, cs.flavors.create, - "flavorcreate", 512, 1, 10, 1234, swap=0, - ephemeral=0, rxtx_factor=1, is_public='invalid') - - def test_delete(self): - cs.flavors.delete("flavordelete") - cs.assert_called('DELETE', '/flavors/flavordelete') - - def test_set_keys(self): - f = cs.flavors.get(1) - f.set_keys({'k1': 'v1'}) - cs.assert_called('POST', '/flavors/1/os-extra_specs', - {"extra_specs": {'k1': 'v1'}}) - - def test_unset_keys(self): - f = cs.flavors.get(1) - f.unset_keys(['k1']) - cs.assert_called('DELETE', '/flavors/1/os-extra_specs/k1') diff --git a/tests/v1_1/test_floating_ip_dns.py b/tests/v1_1/test_floating_ip_dns.py deleted file mode 100644 index 0dd179e2e..000000000 --- a/tests/v1_1/test_floating_ip_dns.py +++ /dev/null @@ -1,76 +0,0 @@ -from novaclient import exceptions -from novaclient.v1_1 import floating_ip_dns -from tests.v1_1 import fakes -from tests import utils - - -cs = fakes.FakeClient() - - -class FloatingIPDNSDomainTest(utils.TestCase): - - testdomain = "testdomain" - - def test_dns_domains(self): - domainlist = cs.dns_domains.domains() - self.assertEqual(len(domainlist), 2) - - for entry in domainlist: - self.assertTrue(isinstance(entry, - floating_ip_dns.FloatingIPDNSDomain)) - - self.assertEqual(domainlist[1].domain, 'example.com') - - def test_create_private_domain(self): - cs.dns_domains.create_private(self.testdomain, 'test_avzone') - cs.assert_called('PUT', '/os-floating-ip-dns/%s' % - self.testdomain) - - def test_create_public_domain(self): - cs.dns_domains.create_public(self.testdomain, 'test_project') - cs.assert_called('PUT', '/os-floating-ip-dns/%s' % - self.testdomain) - - def test_delete_domain(self): - cs.dns_domains.delete(self.testdomain) - cs.assert_called('DELETE', '/os-floating-ip-dns/%s' % - self.testdomain) - - -class FloatingIPDNSEntryTest(utils.TestCase): - - testname = "testname" - testip = "1.2.3.4" - testdomain = "testdomain" - testtype = "A" - - def test_get_dns_entries_by_ip(self): - entries = cs.dns_entries.get_for_ip(self.testdomain, ip=self.testip) - self.assertEqual(len(entries), 2) - - for entry in entries: - self.assertTrue(isinstance(entry, - floating_ip_dns.FloatingIPDNSEntry)) - - self.assertEqual(entries[1].dns_entry['name'], 'host2') - self.assertEqual(entries[1].dns_entry['ip'], self.testip) - - def test_get_dns_entry_by_name(self): - entry = cs.dns_entries.get(self.testdomain, - self.testname) - self.assertTrue(isinstance(entry, floating_ip_dns.FloatingIPDNSEntry)) - self.assertEqual(entry.name, self.testname) - - def test_create_entry(self): - cs.dns_entries.create(self.testdomain, - self.testname, - self.testip, - self.testtype) - - cs.assert_called('PUT', '/os-floating-ip-dns/%s/entries/%s' % - (self.testdomain, self.testname)) - - def test_delete_entry(self): - cs.dns_entries.delete(self.testdomain, self.testname) - cs.assert_called('DELETE', '/os-floating-ip-dns/%s/entries/%s' % - (self.testdomain, self.testname)) diff --git a/tests/v1_1/test_floating_ip_pools.py b/tests/v1_1/test_floating_ip_pools.py deleted file mode 100644 index 6091b2888..000000000 --- a/tests/v1_1/test_floating_ip_pools.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.v1_1 import floating_ip_pools -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class TestFloatingIPPools(utils.TestCase): - - def test_list_floating_ips(self): - fl = cs.floating_ip_pools.list() - cs.assert_called('GET', '/os-floating-ip-pools') - [self.assertTrue(isinstance(f, floating_ip_pools.FloatingIPPool)) - for f in fl] diff --git a/tests/v1_1/test_floating_ips.py b/tests/v1_1/test_floating_ips.py deleted file mode 100644 index f2895c6db..000000000 --- a/tests/v1_1/test_floating_ips.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. -# -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from novaclient.v1_1 import floating_ips -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class FloatingIPsTest(utils.TestCase): - - def test_list_floating_ips(self): - fl = cs.floating_ips.list() - cs.assert_called('GET', '/os-floating-ips') - [self.assertTrue(isinstance(f, floating_ips.FloatingIP)) for f in fl] - - def test_delete_floating_ip(self): - fl = cs.floating_ips.list()[0] - fl.delete() - cs.assert_called('DELETE', '/os-floating-ips/1') - cs.floating_ips.delete(1) - cs.assert_called('DELETE', '/os-floating-ips/1') - cs.floating_ips.delete(fl) - cs.assert_called('DELETE', '/os-floating-ips/1') - - def test_create_floating_ip(self): - fl = cs.floating_ips.create() - cs.assert_called('POST', '/os-floating-ips') - self.assertEqual(fl.pool, None) - self.assertTrue(isinstance(fl, floating_ips.FloatingIP)) - - def test_create_floating_ip_with_pool(self): - fl = cs.floating_ips.create('foo') - cs.assert_called('POST', '/os-floating-ips') - self.assertEqual(fl.pool, 'nova') - self.assertTrue(isinstance(fl, floating_ips.FloatingIP)) diff --git a/tests/v1_1/test_hosts.py b/tests/v1_1/test_hosts.py deleted file mode 100644 index 7daa135cb..000000000 --- a/tests/v1_1/test_hosts.py +++ /dev/null @@ -1,66 +0,0 @@ -from novaclient.v1_1 import hosts -from tests.v1_1 import fakes -from tests import utils - - -cs = fakes.FakeClient() - - -class HostsTest(utils.TestCase): - - def test_describe_resource(self): - hs = cs.hosts.get('host') - cs.assert_called('GET', '/os-hosts/host') - [self.assertTrue(isinstance(h, hosts.Host)) for h in hs] - - def test_list_host(self): - hs = cs.hosts.list_all() - cs.assert_called('GET', '/os-hosts') - [self.assertTrue(isinstance(h, hosts.Host)) for h in hs] - [self.assertEqual(h.zone, 'nova1') for h in hs] - - def test_list_host_with_zone(self): - hs = cs.hosts.list_all('nova') - cs.assert_called('GET', '/os-hosts?zone=nova') - [self.assertTrue(isinstance(h, hosts.Host)) for h in hs] - [self.assertEqual(h.zone, 'nova') for h in hs] - - def test_update_enable(self): - host = cs.hosts.get('sample_host')[0] - values = {"status": "enabled"} - result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertTrue(isinstance(result, hosts.Host)) - - def test_update_maintenance(self): - host = cs.hosts.get('sample_host')[0] - values = {"maintenance_mode": "enable"} - result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertTrue(isinstance(result, hosts.Host)) - - def test_update_both(self): - host = cs.hosts.get('sample_host')[0] - values = {"status": "enabled", - "maintenance_mode": "enable"} - result = host.update(values) - cs.assert_called('PUT', '/os-hosts/sample_host', values) - self.assertTrue(isinstance(result, hosts.Host)) - - def test_host_startup(self): - host = cs.hosts.get('sample_host')[0] - result = host.startup() - cs.assert_called('GET', '/os-hosts/sample_host/startup') - self.assertTrue(isinstance(result, hosts.Host)) - - def test_host_reboot(self): - host = cs.hosts.get('sample_host')[0] - result = host.reboot() - cs.assert_called('GET', '/os-hosts/sample_host/reboot') - self.assertTrue(isinstance(result, hosts.Host)) - - def test_host_shutdown(self): - host = cs.hosts.get('sample_host')[0] - result = host.shutdown() - cs.assert_called('GET', '/os-hosts/sample_host/shutdown') - self.assertTrue(isinstance(result, hosts.Host)) diff --git a/tests/v1_1/test_hypervisors.py b/tests/v1_1/test_hypervisors.py deleted file mode 100644 index 732210941..000000000 --- a/tests/v1_1/test_hypervisors.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# 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 tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class HypervisorsTest(utils.TestCase): - def compare_to_expected(self, expected, hyper): - for key, value in expected.items(): - self.assertEqual(getattr(hyper, key), value) - - def test_hypervisor_index(self): - expected = [ - dict(id=1234, hypervisor_hostname='hyper1'), - dict(id=5678, hypervisor_hostname='hyper2'), - ] - - result = cs.hypervisors.list(False) - cs.assert_called('GET', '/os-hypervisors') - - for idx, hyper in enumerate(result): - self.compare_to_expected(expected[idx], hyper) - - def test_hypervisor_detail(self): - expected = [ - dict(id=1234, - service=dict(id=1, host='compute1'), - vcpus=4, - memory_mb=10 * 1024, - local_gb=250, - vcpus_used=2, - memory_mb_used=5 * 1024, - local_gb_used=125, - hypervisor_type="xen", - hypervisor_version=3, - hypervisor_hostname="hyper1", - free_ram_mb=5 * 1024, - free_disk_gb=125, - current_workload=2, - running_vms=2, - cpu_info='cpu_info', - disk_available_least=100), - dict(id=2, - service=dict(id=2, host="compute2"), - vcpus=4, - memory_mb=10 * 1024, - local_gb=250, - vcpus_used=2, - memory_mb_used=5 * 1024, - local_gb_used=125, - hypervisor_type="xen", - hypervisor_version=3, - hypervisor_hostname="hyper2", - free_ram_mb=5 * 1024, - free_disk_gb=125, - current_workload=2, - running_vms=2, - cpu_info='cpu_info', - disk_available_least=100)] - - result = cs.hypervisors.list() - cs.assert_called('GET', '/os-hypervisors/detail') - - for idx, hyper in enumerate(result): - self.compare_to_expected(expected[idx], hyper) - - def test_hypervisor_search(self): - expected = [ - dict(id=1234, hypervisor_hostname='hyper1'), - dict(id=5678, hypervisor_hostname='hyper2'), - ] - - result = cs.hypervisors.search('hyper') - cs.assert_called('GET', '/os-hypervisors/hyper/search') - - for idx, hyper in enumerate(result): - self.compare_to_expected(expected[idx], hyper) - - def test_hypervisor_servers(self): - expected = [ - dict(id=1234, - hypervisor_hostname='hyper1', - servers=[ - dict(name='inst1', uuid='uuid1'), - dict(name='inst2', uuid='uuid2')]), - dict(id=5678, - hypervisor_hostname='hyper2', - servers=[ - dict(name='inst3', uuid='uuid3'), - dict(name='inst4', uuid='uuid4')]), - ] - - result = cs.hypervisors.search('hyper', True) - cs.assert_called('GET', '/os-hypervisors/hyper/servers') - - for idx, hyper in enumerate(result): - self.compare_to_expected(expected[idx], hyper) - - def test_hypervisor_get(self): - expected = dict( - id=1234, - service=dict(id=1, host='compute1'), - vcpus=4, - memory_mb=10 * 1024, - local_gb=250, - vcpus_used=2, - memory_mb_used=5 * 1024, - local_gb_used=125, - hypervisor_type="xen", - hypervisor_version=3, - hypervisor_hostname="hyper1", - free_ram_mb=5 * 1024, - free_disk_gb=125, - current_workload=2, - running_vms=2, - cpu_info='cpu_info', - disk_available_least=100) - - result = cs.hypervisors.get(1234) - cs.assert_called('GET', '/os-hypervisors/1234') - - self.compare_to_expected(expected, result) - - def test_hypervisor_uptime(self): - expected = dict( - id=1234, - hypervisor_hostname="hyper1", - uptime="fake uptime") - - result = cs.hypervisors.uptime(1234) - cs.assert_called('GET', '/os-hypervisors/1234/uptime') - - self.compare_to_expected(expected, result) - - def test_hypervisor_statistics(self): - expected = dict( - count=2, - vcpus=8, - memory_mb=20 * 1024, - local_gb=500, - vcpus_used=4, - memory_mb_used=10 * 1024, - local_gb_used=250, - free_ram_mb=10 * 1024, - free_disk_gb=250, - current_workload=4, - running_vms=4, - disk_available_least=200, - ) - - result = cs.hypervisors.statistics() - cs.assert_called('GET', '/os-hypervisors/statistics') - - self.compare_to_expected(expected, result) diff --git a/tests/v1_1/test_images.py b/tests/v1_1/test_images.py deleted file mode 100644 index 71676e165..000000000 --- a/tests/v1_1/test_images.py +++ /dev/null @@ -1,48 +0,0 @@ -from novaclient.v1_1 import images -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class ImagesTest(utils.TestCase): - - def test_list_images(self): - il = cs.images.list() - cs.assert_called('GET', '/images/detail') - [self.assertTrue(isinstance(i, images.Image)) for i in il] - - def test_list_images_undetailed(self): - il = cs.images.list(detailed=False) - cs.assert_called('GET', '/images') - [self.assertTrue(isinstance(i, images.Image)) for i in il] - - def test_get_image_details(self): - i = cs.images.get(1) - cs.assert_called('GET', '/images/1') - self.assertTrue(isinstance(i, images.Image)) - self.assertEqual(i.id, 1) - self.assertEqual(i.name, 'CentOS 5.2') - - def test_delete_image(self): - cs.images.delete(1) - cs.assert_called('DELETE', '/images/1') - - def test_delete_meta(self): - cs.images.delete_meta(1, {'test_key': 'test_value'}) - cs.assert_called('DELETE', '/images/1/metadata/test_key') - - def test_set_meta(self): - cs.images.set_meta(1, {'test_key': 'test_value'}) - cs.assert_called('POST', '/images/1/metadata', - {"metadata": {'test_key': 'test_value'}}) - - def test_find(self): - i = cs.images.find(name="CentOS 5.2") - self.assertEqual(i.id, 1) - cs.assert_called('GET', '/images/detail') - - iml = cs.images.findall(status='SAVING') - self.assertEqual(len(iml), 1) - self.assertEqual(iml[0].name, 'My Server Backup') diff --git a/tests/v1_1/test_keypairs.py b/tests/v1_1/test_keypairs.py deleted file mode 100644 index 10afd2497..000000000 --- a/tests/v1_1/test_keypairs.py +++ /dev/null @@ -1,33 +0,0 @@ -from novaclient.v1_1 import keypairs -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class KeypairsTest(utils.TestCase): - - def test_list_keypairs(self): - kps = cs.keypairs.list() - cs.assert_called('GET', '/os-keypairs') - [self.assertTrue(isinstance(kp, keypairs.Keypair)) for kp in kps] - - def test_delete_keypair(self): - kp = cs.keypairs.list()[0] - kp.delete() - cs.assert_called('DELETE', '/os-keypairs/test') - cs.keypairs.delete('test') - cs.assert_called('DELETE', '/os-keypairs/test') - cs.keypairs.delete(kp) - cs.assert_called('DELETE', '/os-keypairs/test') - - def test_create_keypair(self): - kp = cs.keypairs.create("foo") - cs.assert_called('POST', '/os-keypairs') - self.assertTrue(isinstance(kp, keypairs.Keypair)) - - def test_import_keypair(self): - kp = cs.keypairs.create("foo", "fake-public-key") - cs.assert_called('POST', '/os-keypairs') - self.assertTrue(isinstance(kp, keypairs.Keypair)) diff --git a/tests/v1_1/test_limits.py b/tests/v1_1/test_limits.py deleted file mode 100644 index f41a2e76b..000000000 --- a/tests/v1_1/test_limits.py +++ /dev/null @@ -1,52 +0,0 @@ - -from novaclient.v1_1 import limits -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class LimitsTest(utils.TestCase): - - def test_get_limits(self): - obj = cs.limits.get() - cs.assert_called('GET', '/limits') - self.assertTrue(isinstance(obj, limits.Limits)) - - def test_absolute_limits(self): - obj = cs.limits.get() - - expected = ( - limits.AbsoluteLimit("maxTotalRAMSize", 51200), - limits.AbsoluteLimit("maxServerMeta", 5), - limits.AbsoluteLimit("maxImageMeta", 5), - 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.assertTrue(limit in expected) - - def test_rate_limits(self): - obj = cs.limits.get() - - expected = ( - limits.RateLimit('POST', '*', '.*', 10, 2, 'MINUTE', - '2011-12-15T22:42:45Z'), - limits.RateLimit('PUT', '*', '.*', 10, 2, 'MINUTE', - '2011-12-15T22:42:45Z'), - limits.RateLimit('DELETE', '*', '.*', 100, 100, 'MINUTE', - '2011-12-15T22:42:45Z'), - limits.RateLimit('POST', '*/servers', '^/servers', 25, 24, 'DAY', - '2011-12-15T22:42:45Z'), - ) - - rate_limits = list(obj.rate) - self.assertEqual(len(rate_limits), len(expected)) - - for limit in rate_limits: - self.assertTrue(limit in expected) diff --git a/tests/v1_1/test_networks.py b/tests/v1_1/test_networks.py deleted file mode 100644 index 65d6b6d9f..000000000 --- a/tests/v1_1/test_networks.py +++ /dev/null @@ -1,40 +0,0 @@ -from novaclient import exceptions -from novaclient.v1_1 import networks -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class NetworksTest(utils.TestCase): - - def test_list_networks(self): - fl = cs.networks.list() - cs.assert_called('GET', '/os-networks') - [self.assertTrue(isinstance(f, networks.Network)) for f in fl] - - def test_get_network(self): - f = cs.networks.get(1) - cs.assert_called('GET', '/os-networks/1') - self.assertTrue(isinstance(f, networks.Network)) - - def test_delete(self): - cs.networks.delete('networkdelete') - cs.assert_called('DELETE', '/os-networks/networkdelete') - - def test_create(self): - f = cs.networks.create(label='foo') - cs.assert_called('POST', '/os-networks', - {'network': {'label': 'foo'}}) - self.assertTrue(isinstance(f, networks.Network)) - - def test_disassociate(self): - cs.networks.disassociate('networkdisassociate') - cs.assert_called('POST', '/os-networks/networkdisassociate/action', - {'disassociate': None}) - - def test_add(self): - cs.networks.add('networkadd') - cs.assert_called('POST', '/os-networks/add', - {'id': 'networkadd'}) diff --git a/tests/v1_1/test_quota_classes.py b/tests/v1_1/test_quota_classes.py deleted file mode 100644 index b917665db..000000000 --- a/tests/v1_1/test_quota_classes.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# 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 tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class QuotaClassSetsTest(utils.TestCase): - - def test_class_quotas_get(self): - class_name = 'test' - cs.quota_classes.get(class_name) - cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) - - def test_update_quota(self): - q = cs.quota_classes.get('test') - q.update(volumes=2) - cs.assert_called('PUT', '/os-quota-class-sets/test') - - def test_refresh_quota(self): - q = cs.quota_classes.get('test') - q2 = cs.quota_classes.get('test') - self.assertEqual(q.volumes, q2.volumes) - q2.volumes = 0 - self.assertNotEqual(q.volumes, q2.volumes) - q2.get() - self.assertEqual(q.volumes, q2.volumes) diff --git a/tests/v1_1/test_quotas.py b/tests/v1_1/test_quotas.py deleted file mode 100644 index 61e06ef67..000000000 --- a/tests/v1_1/test_quotas.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# 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 tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class QuotaSetsTest(utils.TestCase): - - def test_tenant_quotas_get(self): - tenant_id = 'test' - cs.quotas.get(tenant_id) - cs.assert_called('GET', '/os-quota-sets/%s' % tenant_id) - - def test_tenant_quotas_defaults(self): - tenant_id = 'test' - cs.quotas.defaults(tenant_id) - cs.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) - - def test_update_quota(self): - q = cs.quotas.get('test') - q.update(volumes=2) - cs.assert_called('PUT', '/os-quota-sets/test') - - def test_refresh_quota(self): - q = cs.quotas.get('test') - q2 = cs.quotas.get('test') - self.assertEqual(q.volumes, q2.volumes) - q2.volumes = 0 - self.assertNotEqual(q.volumes, q2.volumes) - q2.get() - self.assertEqual(q.volumes, q2.volumes) diff --git a/tests/v1_1/test_security_group_rules.py b/tests/v1_1/test_security_group_rules.py deleted file mode 100644 index 872dab78d..000000000 --- a/tests/v1_1/test_security_group_rules.py +++ /dev/null @@ -1,17 +0,0 @@ -from novaclient.v1_1 import security_group_rules -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class SecurityGroupRulesTest(utils.TestCase): - def test_delete_security_group_rule(self): - cs.security_group_rules.delete(1) - cs.assert_called('DELETE', '/os-security-group-rules/1') - - def test_create_security_group(self): - sg = cs.security_group_rules.create(1) - cs.assert_called('POST', '/os-security-group-rules') - self.assertTrue(isinstance(sg, security_group_rules.SecurityGroupRule)) diff --git a/tests/v1_1/test_security_groups.py b/tests/v1_1/test_security_groups.py deleted file mode 100644 index 122ebe6a5..000000000 --- a/tests/v1_1/test_security_groups.py +++ /dev/null @@ -1,54 +0,0 @@ -from novaclient.v1_1 import security_groups -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class SecurityGroupsTest(utils.TestCase): - def _do_test_list_security_groups(self, search_opts, path): - sgs = cs.security_groups.list(search_opts=search_opts) - cs.assert_called('GET', path) - for sg in sgs: - self.assertTrue(isinstance(sg, security_groups.SecurityGroup)) - - def test_list_security_groups_all_tenants_on(self): - self._do_test_list_security_groups( - None, '/os-security-groups') - - def test_list_security_groups_all_tenants_on(self): - self._do_test_list_security_groups( - {'all_tenants': 1}, '/os-security-groups?all_tenants=1') - - def test_list_security_groups_all_tenants_off(self): - self._do_test_list_security_groups( - {'all_tenants': 0}, '/os-security-groups') - - def test_get_security_groups(self): - sg = cs.security_groups.get(1) - cs.assert_called('GET', '/os-security-groups/1') - self.assertTrue(isinstance(sg, security_groups.SecurityGroup)) - - def test_delete_security_group(self): - sg = cs.security_groups.list()[0] - sg.delete() - cs.assert_called('DELETE', '/os-security-groups/1') - cs.security_groups.delete(1) - cs.assert_called('DELETE', '/os-security-groups/1') - cs.security_groups.delete(sg) - cs.assert_called('DELETE', '/os-security-groups/1') - - def test_create_security_group(self): - sg = cs.security_groups.create("foo", "foo barr") - cs.assert_called('POST', '/os-security-groups') - self.assertTrue(isinstance(sg, security_groups.SecurityGroup)) - - def test_refresh_security_group(self): - sg = cs.security_groups.get(1) - sg2 = cs.security_groups.get(1) - self.assertEqual(sg.name, sg2.name) - sg2.name = "should be test" - self.assertNotEqual(sg.name, sg2.name) - sg2.get() - self.assertEqual(sg.name, sg2.name) diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py deleted file mode 100644 index 4041dad8b..000000000 --- a/tests/v1_1/test_servers.py +++ /dev/null @@ -1,367 +0,0 @@ -# -*- coding: utf-8 -*- - -import StringIO - -from novaclient import exceptions -from novaclient.v1_1 import servers -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class ServersTest(utils.TestCase): - - def test_list_servers(self): - sl = cs.servers.list() - cs.assert_called('GET', '/servers/detail') - [self.assertTrue(isinstance(s, servers.Server)) for s in sl] - - def test_list_servers_undetailed(self): - sl = cs.servers.list(detailed=False) - cs.assert_called('GET', '/servers') - [self.assertTrue(isinstance(s, servers.Server)) for s in sl] - - def test_get_server_details(self): - s = cs.servers.get(1234) - cs.assert_called('GET', '/servers/1234') - self.assertTrue(isinstance(s, servers.Server)) - self.assertEqual(s.id, 1234) - self.assertEqual(s.status, 'BUILD') - - def test_create_server(self): - s = cs.servers.create( - name="My server", - image=1, - flavor=1, - meta={'foo': 'bar'}, - userdata="hello moto", - key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': StringIO.StringIO('data'), # a stream - } - ) - cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) - - def test_create_server_userdata_file_object(self): - s = cs.servers.create( - name="My server", - image=1, - flavor=1, - meta={'foo': 'bar'}, - userdata=StringIO.StringIO('hello moto'), - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': StringIO.StringIO('data'), # a stream - }, - ) - cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) - - def test_create_server_userdata_unicode(self): - s = cs.servers.create( - name="My server", - image=1, - flavor=1, - meta={'foo': 'bar'}, - userdata=u'こんにちは', - key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': StringIO.StringIO('data'), # a stream - }, - ) - cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) - - def test_create_server_userdata_utf8(self): - s = cs.servers.create( - name="My server", - image=1, - flavor=1, - meta={'foo': 'bar'}, - userdata='こんにちは', - key_name="fakekey", - files={ - '/etc/passwd': 'some data', # a file - '/tmp/foo.txt': StringIO.StringIO('data'), # a stream - }, - ) - cs.assert_called('POST', '/servers') - self.assertTrue(isinstance(s, servers.Server)) - - def test_update_server(self): - s = cs.servers.get(1234) - - # Update via instance - s.update(name='hi') - cs.assert_called('PUT', '/servers/1234') - s.update(name='hi') - cs.assert_called('PUT', '/servers/1234') - - # Silly, but not an error - s.update() - - # Update via manager - cs.servers.update(s, name='hi') - cs.assert_called('PUT', '/servers/1234') - - def test_delete_server(self): - s = cs.servers.get(1234) - s.delete() - cs.assert_called('DELETE', '/servers/1234') - cs.servers.delete(1234) - cs.assert_called('DELETE', '/servers/1234') - cs.servers.delete(s) - cs.assert_called('DELETE', '/servers/1234') - - def test_delete_server_meta(self): - s = cs.servers.delete_meta(1234, ['test_key']) - cs.assert_called('DELETE', '/servers/1234/metadata/test_key') - - def test_set_server_meta(self): - s = cs.servers.set_meta(1234, {'test_key': 'test_value'}) - reval = cs.assert_called('POST', '/servers/1234/metadata', - {'metadata': {'test_key': 'test_value'}}) - - def test_find(self): - s = cs.servers.find(name='sample-server') - cs.assert_called('GET', '/servers/detail') - self.assertEqual(s.name, 'sample-server') - - self.assertRaises(exceptions.NoUniqueMatch, cs.servers.find, - flavor={"id": 1, "name": "256 MB Server"}) - - sl = cs.servers.findall(flavor={"id": 1, "name": "256 MB Server"}) - self.assertEqual([s.id for s in sl], [1234, 5678]) - - def test_reboot_server(self): - s = cs.servers.get(1234) - s.reboot() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.reboot(s, reboot_type='HARD') - cs.assert_called('POST', '/servers/1234/action') - - def test_rebuild_server(self): - s = cs.servers.get(1234) - s.rebuild(image=1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.rebuild(s, image=1) - cs.assert_called('POST', '/servers/1234/action') - s.rebuild(image=1, password='5678') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.rebuild(s, image=1, password='5678') - cs.assert_called('POST', '/servers/1234/action') - - def test_resize_server(self): - s = cs.servers.get(1234) - s.resize(flavor=1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.resize(s, flavor=1) - cs.assert_called('POST', '/servers/1234/action') - - def test_confirm_resized_server(self): - s = cs.servers.get(1234) - s.confirm_resize() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.confirm_resize(s) - cs.assert_called('POST', '/servers/1234/action') - - def test_revert_resized_server(self): - s = cs.servers.get(1234) - s.revert_resize() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.revert_resize(s) - cs.assert_called('POST', '/servers/1234/action') - - def test_migrate_server(self): - s = cs.servers.get(1234) - s.migrate() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.migrate(s) - cs.assert_called('POST', '/servers/1234/action') - - def test_add_fixed_ip(self): - s = cs.servers.get(1234) - s.add_fixed_ip(1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.add_fixed_ip(s, 1) - cs.assert_called('POST', '/servers/1234/action') - - def test_remove_fixed_ip(self): - s = cs.servers.get(1234) - s.remove_fixed_ip('10.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.remove_fixed_ip(s, '10.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - - def test_add_floating_ip(self): - s = cs.servers.get(1234) - s.add_floating_ip('11.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.add_floating_ip(s, '11.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - f = cs.floating_ips.list()[0] - cs.servers.add_floating_ip(s, f) - cs.assert_called('POST', '/servers/1234/action') - s.add_floating_ip(f) - cs.assert_called('POST', '/servers/1234/action') - - def test_remove_floating_ip(self): - s = cs.servers.get(1234) - s.remove_floating_ip('11.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.remove_floating_ip(s, '11.0.0.1') - cs.assert_called('POST', '/servers/1234/action') - f = cs.floating_ips.list()[0] - cs.servers.remove_floating_ip(s, f) - cs.assert_called('POST', '/servers/1234/action') - s.remove_floating_ip(f) - cs.assert_called('POST', '/servers/1234/action') - - def test_stop(self): - s = cs.servers.get(1234) - s.stop() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.stop(s) - cs.assert_called('POST', '/servers/1234/action') - - def test_start(self): - s = cs.servers.get(1234) - s.start() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.start(s) - cs.assert_called('POST', '/servers/1234/action') - - def test_rescue(self): - s = cs.servers.get(1234) - s.rescue() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.rescue(s) - cs.assert_called('POST', '/servers/1234/action') - - def test_unrescue(self): - s = cs.servers.get(1234) - s.unrescue() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.unrescue(s) - cs.assert_called('POST', '/servers/1234/action') - - def test_lock(self): - s = cs.servers.get(1234) - s.lock() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.lock(s) - cs.assert_called('POST', '/servers/1234/action') - - def test_unlock(self): - s = cs.servers.get(1234) - s.unlock() - cs.assert_called('POST', '/servers/1234/action') - cs.servers.unlock(s) - cs.assert_called('POST', '/servers/1234/action') - - def test_backup(self): - s = cs.servers.get(1234) - s.backup('back1', 'daily', 1) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.backup(s, 'back1', 'daily', 2) - cs.assert_called('POST', '/servers/1234/action') - - def test_get_console_output_without_length(self): - success = 'foo' - s = cs.servers.get(1234) - s.get_console_output() - self.assertEqual(s.get_console_output(), success) - cs.assert_called('POST', '/servers/1234/action') - - cs.servers.get_console_output(s) - self.assertEqual(cs.servers.get_console_output(s), success) - cs.assert_called('POST', '/servers/1234/action') - - def test_get_console_output_with_length(self): - success = 'foo' - - s = cs.servers.get(1234) - s.get_console_output(length=50) - self.assertEqual(s.get_console_output(length=50), success) - cs.assert_called('POST', '/servers/1234/action') - - cs.servers.get_console_output(s, length=50) - self.assertEqual(cs.servers.get_console_output(s, length=50), success) - cs.assert_called('POST', '/servers/1234/action') - - def test_get_server_actions(self): - s = cs.servers.get(1234) - actions = s.actions() - self.assertTrue(actions is not None) - cs.assert_called('GET', '/servers/1234/actions') - - actions_from_manager = cs.servers.actions(1234) - self.assertTrue(actions_from_manager is not None) - cs.assert_called('GET', '/servers/1234/actions') - - self.assertEqual(actions, actions_from_manager) - - def test_get_server_diagnostics(self): - s = cs.servers.get(1234) - diagnostics = s.diagnostics() - self.assertTrue(diagnostics is not None) - cs.assert_called('GET', '/servers/1234/diagnostics') - - diagnostics_from_manager = cs.servers.diagnostics(1234) - self.assertTrue(diagnostics_from_manager is not None) - cs.assert_called('GET', '/servers/1234/diagnostics') - - self.assertEqual(diagnostics, diagnostics_from_manager) - - def test_get_vnc_console(self): - s = cs.servers.get(1234) - s.get_vnc_console('fake') - cs.assert_called('POST', '/servers/1234/action') - - cs.servers.get_vnc_console(s, 'fake') - cs.assert_called('POST', '/servers/1234/action') - - def test_create_image(self): - s = cs.servers.get(1234) - s.create_image('123') - cs.assert_called('POST', '/servers/1234/action') - s.create_image('123', {}) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.create_image(s, '123') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.create_image(s, '123', {}) - - def test_live_migrate_server(self): - s = cs.servers.get(1234) - s.live_migrate(host='hostname', block_migration=False, - disk_over_commit=False) - cs.assert_called('POST', '/servers/1234/action') - cs.servers.live_migrate(s, host='hostname', block_migration=False, - disk_over_commit=False) - cs.assert_called('POST', '/servers/1234/action') - - def test_reset_state(self): - s = cs.servers.get(1234) - s.reset_state('newstate') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.reset_state(s, 'newstate') - cs.assert_called('POST', '/servers/1234/action') - - def test_add_security_group(self): - s = cs.servers.get(1234) - s.add_security_group('newsg') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.add_security_group(s, 'newsg') - cs.assert_called('POST', '/servers/1234/action') - - def test_remove_security_group(self): - s = cs.servers.get(1234) - s.remove_security_group('oldsg') - cs.assert_called('POST', '/servers/1234/action') - cs.servers.remove_security_group(s, 'oldsg') - cs.assert_called('POST', '/servers/1234/action') diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py deleted file mode 100644 index 78a2da1a7..000000000 --- a/tests/v1_1/test_shell.py +++ /dev/null @@ -1,555 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss - -# Copyright 2011 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import datetime -import os -import mock -import sys -import tempfile - -import novaclient.shell -import novaclient.client -from novaclient import exceptions -from novaclient.openstack.common import timeutils -from tests.v1_1 import fakes -from tests import utils - - -class ShellTest(utils.TestCase): - - # Patch os.environ to avoid required auth info. - def setUp(self): - """Run before each test.""" - self.old_environment = os.environ.copy() - os.environ = { - 'NOVA_USERNAME': 'username', - 'NOVA_PASSWORD': 'password', - 'NOVA_PROJECT_ID': 'project_id', - 'OS_COMPUTE_API_VERSION': '1.1', - 'NOVA_URL': 'http://no.where', - } - - self.shell = novaclient.shell.OpenStackComputeShell() - - #HACK(bcwaldon): replace this when we start using stubs - self.old_get_client_class = novaclient.client.get_client_class - novaclient.client.get_client_class = lambda *_: fakes.FakeClient - - def tearDown(self): - os.environ = self.old_environment - # For some method like test_image_meta_bad_action we are - # testing a SystemExit to be thrown and object self.shell has - # no time to get instantatiated which is OK in this case, so - # we make sure the method is there before launching it. - if hasattr(self.shell, 'cs'): - self.shell.cs.clear_callstack() - - #HACK(bcwaldon): replace this when we start using stubs - novaclient.client.get_client_class = self.old_get_client_class - - timeutils.clear_time_override() - - def run_command(self, cmd): - self.shell.main(cmd.split()) - - def assert_called(self, method, url, body=None, **kwargs): - return self.shell.cs.assert_called(method, url, body, **kwargs) - - def assert_called_anytime(self, method, url, body=None): - return self.shell.cs.assert_called_anytime(method, url, body) - - def test_boot(self): - self.run_command('boot --flavor 1 --image 1 some-server') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavorRef': '1', - 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, - 'networks': [], - }}, - ) - - def test_boot_metadata(self): - self.run_command('boot --image 1 --flavor 1 --meta foo=bar=pants' - ' --meta spam=eggs some-server ') - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavorRef': '1', - 'name': 'some-server', - 'imageRef': '1', - 'metadata': {'foo': 'bar=pants', 'spam': 'eggs'}, - 'min_count': 1, - 'max_count': 1, - 'networks': [], - }}, - ) - - def test_boot_hints(self): - self.run_command('boot --image 1 --flavor 1 --hint a=b=c some-server ') - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavorRef': '1', - 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, - 'networks': [], - }, - 'os:scheduler_hints': {'a': 'b=c'}, - }, - ) - - def test_boot_nics(self): - cmd = ('boot --image 1 --flavor 1 ' - '--nic net-id=a=c,v4-fixed-ip=10.0.0.1 some-server') - self.run_command(cmd) - self.assert_called_anytime( - 'POST', '/servers', - { - 'server': { - 'flavorRef': '1', - 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, - 'networks': [ - {'uuid': 'a=c', 'fixed_ip': '10.0.0.1'}, - ], - }, - }, - ) - - def test_boot_files(self): - testfile = os.path.join(os.path.dirname(__file__), 'testfile.txt') - expected_file_data = open(testfile).read().encode('base64') - - cmd = 'boot some-server --flavor 1 --image 1 ' \ - '--file /tmp/foo=%s --file /tmp/bar=%s' - self.run_command(cmd % (testfile, testfile)) - - self.assert_called_anytime( - 'POST', '/servers', - {'server': { - 'flavorRef': '1', - 'name': 'some-server', - 'imageRef': '1', - 'min_count': 1, - 'max_count': 1, - 'networks': [], - 'personality': [ - {'path': '/tmp/bar', 'contents': expected_file_data}, - {'path': '/tmp/foo', 'contents': expected_file_data}, - ]}, - }, - ) - - def test_boot_invalid_file(self): - invalid_file = os.path.join(os.path.dirname(__file__), - 'asdfasdfasdfasdf') - cmd = 'boot some-server --image 1 --file /foo=%s' % invalid_file - self.assertRaises(exceptions.CommandError, self.run_command, cmd) - - def test_flavor_list(self): - self.run_command('flavor-list') - self.assert_called('GET', '/flavors/2/os-extra_specs') - self.assert_called_anytime('GET', '/flavors/detail') - - def test_flavor_show(self): - self.run_command('flavor-show 1') - self.assert_called_anytime('GET', '/flavors/1') - - def test_image_show(self): - self.run_command('image-show 1') - self.assert_called('GET', '/images/1') - - def test_image_meta_set(self): - self.run_command('image-meta 1 set test_key=test_value') - self.assert_called('POST', '/images/1/metadata', - {'metadata': {'test_key': 'test_value'}}) - - def test_image_meta_del(self): - self.run_command('image-meta 1 delete test_key=test_value') - self.assert_called('DELETE', '/images/1/metadata/test_key') - - def test_image_meta_bad_action(self): - tmp = tempfile.TemporaryFile() - - # Suppress stdout and stderr - (stdout, stderr) = (sys.stdout, sys.stderr) - (sys.stdout, sys.stderr) = (tmp, tmp) - - self.assertRaises(SystemExit, self.run_command, - 'image-meta 1 BAD_ACTION test_key=test_value') - - # Put stdout and stderr back - sys.stdout, sys.stderr = (stdout, stderr) - - def test_image_list(self): - self.run_command('image-list') - self.assert_called('GET', '/images/detail') - - def test_create_image(self): - self.run_command('image-create sample-server mysnapshot') - self.assert_called( - 'POST', '/servers/1234/action', - {'createImage': {'name': 'mysnapshot', 'metadata': {}}}, - ) - - def test_image_delete(self): - self.run_command('image-delete 1') - self.assert_called('DELETE', '/images/1') - - def test_list(self): - self.run_command('list') - self.assert_called('GET', '/servers/detail') - - def test_reboot(self): - self.run_command('reboot sample-server') - self.assert_called('POST', '/servers/1234/action', - {'reboot': {'type': 'SOFT'}}) - self.run_command('reboot sample-server --hard') - self.assert_called('POST', '/servers/1234/action', - {'reboot': {'type': 'HARD'}}) - - def test_rebuild(self): - self.run_command('rebuild sample-server 1') - # XXX need a way to test multiple calls - #self.assert_called('POST', '/servers/1234/action', - # {'rebuild': {'imageRef': 1}}) - self.assert_called('GET', '/images/2') - - self.run_command('rebuild sample-server 1 --rebuild-password asdf') - # XXX need a way to test multiple calls - #self.assert_called('POST', '/servers/1234/action', - # {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}}) - self.assert_called('GET', '/images/2') - - def test_rename(self): - self.run_command('rename sample-server newname') - self.assert_called('PUT', '/servers/1234', - {'server': {'name': 'newname'}}) - - def test_resize(self): - self.run_command('resize sample-server 1') - self.assert_called('POST', '/servers/1234/action', - {'resize': {'flavorRef': 1}}) - - def test_resize_confirm(self): - self.run_command('resize-confirm sample-server') - self.assert_called('POST', '/servers/1234/action', - {'confirmResize': None}) - - def test_resize_revert(self): - self.run_command('resize-revert sample-server') - self.assert_called('POST', '/servers/1234/action', - {'revertResize': None}) - - @mock.patch('getpass.getpass', mock.Mock(return_value='p')) - def test_root_password(self): - self.run_command('root-password sample-server') - self.assert_called('POST', '/servers/1234/action', - {'changePassword': {'adminPass': 'p'}}) - - def test_show(self): - self.run_command('show 1234') - self.assert_called('GET', '/servers/1234', pos=-3) - self.assert_called('GET', '/flavors/1', pos=-2) - self.assert_called('GET', '/images/2') - - def test_show_bad_id(self): - self.assertRaises(exceptions.CommandError, - self.run_command, 'show xxx') - - def test_delete(self): - self.run_command('delete 1234') - self.assert_called('DELETE', '/servers/1234') - self.run_command('delete sample-server') - self.assert_called('DELETE', '/servers/1234') - - def test_diagnostics(self): - self.run_command('diagnostics 1234') - self.assert_called('GET', '/servers/1234/diagnostics') - self.run_command('diagnostics sample-server') - self.assert_called('GET', '/servers/1234/diagnostics') - - def test_actions(self): - self.run_command('actions 1234') - self.assert_called('GET', '/servers/1234/actions') - self.run_command('actions sample-server') - self.assert_called('GET', '/servers/1234/actions') - - def test_set_meta_set(self): - self.run_command('meta 1234 set key1=val1 key2=val2') - self.assert_called('POST', '/servers/1234/metadata', - {'metadata': {'key1': 'val1', 'key2': 'val2'}}) - - def test_set_meta_delete_dict(self): - self.run_command('meta 1234 delete key1=val1 key2=val2') - self.assert_called('DELETE', '/servers/1234/metadata/key1') - self.assert_called('DELETE', '/servers/1234/metadata/key2', pos=-2) - - def test_set_meta_delete_keys(self): - self.run_command('meta 1234 delete key1 key2') - self.assert_called('DELETE', '/servers/1234/metadata/key1') - self.assert_called('DELETE', '/servers/1234/metadata/key2', pos=-2) - - def test_dns_create(self): - self.run_command('dns-create 192.168.1.1 testname testdomain') - self.assert_called('PUT', - '/os-floating-ip-dns/testdomain/entries/testname') - - self.run_command('dns-create 192.168.1.1 testname testdomain --type A') - self.assert_called('PUT', - '/os-floating-ip-dns/testdomain/entries/testname') - - def test_dns_create_public_domain(self): - self.run_command('dns-create-public-domain testdomain ' - '--project test_project') - self.assert_called('PUT', '/os-floating-ip-dns/testdomain') - - def test_dns_create_private_domain(self): - self.run_command('dns-create-private-domain testdomain ' - '--availability-zone av_zone') - self.assert_called('PUT', '/os-floating-ip-dns/testdomain') - - def test_dns_delete(self): - self.run_command('dns-delete testdomain testname') - self.assert_called('DELETE', - '/os-floating-ip-dns/testdomain/entries/testname') - - def test_dns_delete_domain(self): - self.run_command('dns-delete-domain testdomain') - self.assert_called('DELETE', '/os-floating-ip-dns/testdomain') - - def test_dns_list(self): - self.run_command('dns-list testdomain --ip 192.168.1.1') - self.assert_called('GET', - '/os-floating-ip-dns/testdomain/entries?ip=192.168.1.1') - - self.run_command('dns-list testdomain --name testname') - self.assert_called('GET', - '/os-floating-ip-dns/testdomain/entries/testname') - - def test_dns_domains(self): - self.run_command('dns-domains') - self.assert_called('GET', '/os-floating-ip-dns') - - def test_usage_list(self): - self.run_command('usage-list --start 2000-01-20 --end 2005-02-01') - self.assert_called('GET', - '/os-simple-tenant-usage?' + - 'start=2000-01-20T00:00:00&' + - 'end=2005-02-01T00:00:00&' + - 'detailed=1') - - def test_usage_list_no_args(self): - timeutils.set_time_override(datetime.datetime(2005, 2, 1, 0, 0)) - self.run_command('usage-list') - self.assert_called('GET', - '/os-simple-tenant-usage?' + - 'start=2005-01-04T00:00:00&' + - 'end=2005-02-02T00:00:00&' + - 'detailed=1') - - def test_flavor_delete(self): - self.run_command("flavor-delete flavordelete") - self.assert_called('DELETE', '/flavors/flavordelete') - - def test_flavor_create(self): - self.run_command("flavor-create flavorcreate " - "1234 512 10 1 --swap 1024 --ephemeral 10 " - "--is-public true") - self.assert_called('POST', '/flavors', pos=-3) - self.assert_called('GET', '/flavors/1', pos=-2) - self.assert_called('GET', '/flavors/1/os-extra_specs', pos=-1) - - def test_aggregate_list(self): - self.run_command('aggregate-list') - self.assert_called('GET', '/os-aggregates') - - def test_aggregate_create(self): - self.run_command('aggregate-create test_name nova1') - body = {"aggregate": {"name": "test_name", - "availability_zone": "nova1"}} - self.assert_called('POST', '/os-aggregates', body) - - def test_aggregate_delete(self): - self.run_command('aggregate-delete 1') - self.assert_called('DELETE', '/os-aggregates/1') - - def test_aggregate_update(self): - self.run_command('aggregate-update 1 new_name') - body = {"aggregate": {"name": "new_name"}} - self.assert_called('PUT', '/os-aggregates/1', body) - - def test_aggregate_update_with_availability_zone(self): - self.run_command('aggregate-update 1 foo new_zone') - body = {"aggregate": {"name": "foo", "availability_zone": "new_zone"}} - self.assert_called('PUT', '/os-aggregates/1', body) - - def test_aggregate_set_metadata(self): - self.run_command('aggregate-set-metadata 1 foo=bar delete_key') - body = {"set_metadata": {"metadata": {"foo": "bar", - "delete_key": None}}} - self.assert_called('POST', '/os-aggregates/1/action', body) - - def test_aggregate_add_host(self): - self.run_command('aggregate-add-host 1 host1') - body = {"add_host": {"host": "host1"}} - self.assert_called('POST', '/os-aggregates/1/action', body) - - def test_aggregate_remove_host(self): - self.run_command('aggregate-remove-host 1 host1') - body = {"remove_host": {"host": "host1"}} - self.assert_called('POST', '/os-aggregates/1/action', body) - - def test_aggregate_details(self): - self.run_command('aggregate-details 1') - self.assert_called('GET', '/os-aggregates/1') - - def test_live_migration(self): - self.run_command('live-migration sample-server hostname') - self.assert_called('POST', '/servers/1234/action', - {'os-migrateLive': {'host': 'hostname', - 'block_migration': False, - 'disk_over_commit': False}}) - self.run_command('live-migration sample-server hostname \ - --block-migrate') - self.assert_called('POST', '/servers/1234/action', - {'os-migrateLive': {'host': 'hostname', - 'block_migration': True, - 'disk_over_commit': False}}) - self.run_command('live-migration sample-server hostname \ - --block-migrate --disk-over-commit') - self.assert_called('POST', '/servers/1234/action', - {'os-migrateLive': {'host': 'hostname', - 'block_migration': True, - 'disk_over_commit': True}}) - - def test_reset_state(self): - self.run_command('reset-state sample-server') - self.assert_called('POST', '/servers/1234/action', - {'os-resetState': {'state': 'error'}}) - self.run_command('reset-state sample-server --active') - self.assert_called('POST', '/servers/1234/action', - {'os-resetState': {'state': 'active'}}) - - def test_host_list(self): - self.run_command('host-list') - 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): - self.run_command('host-update sample-host_1 --status enabled') - body = {'status': 'enabled'} - self.assert_called('PUT', '/os-hosts/sample-host_1', body) - - def test_host_update_maintenance(self): - self.run_command('host-update sample-host_2 --maintenance enable') - body = {'maintenance_mode': 'enable'} - self.assert_called('PUT', '/os-hosts/sample-host_2', body) - - def test_host_update_multiple_settings(self): - self.run_command('host-update sample-host_3 ' - '--status disabled --maintenance enable') - body = {'status': 'disabled', 'maintenance_mode': 'enable'} - self.assert_called('PUT', '/os-hosts/sample-host_3', body) - - def test_host_startup(self): - self.run_command('host-action sample-host --action startup') - 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_hypervisor_list(self): - self.run_command('hypervisor-list') - self.assert_called('GET', '/os-hypervisors') - - def test_hypervisor_list_matching(self): - self.run_command('hypervisor-list --matching hyper') - self.assert_called('GET', '/os-hypervisors/hyper/search') - - def test_hypervisor_servers(self): - self.run_command('hypervisor-servers hyper') - self.assert_called('GET', '/os-hypervisors/hyper/servers') - - def test_hypervisor_show(self): - self.run_command('hypervisor-show 1234') - self.assert_called('GET', '/os-hypervisors/1234') - - def test_hypervisor_uptime(self): - self.run_command('hypervisor-uptime 1234') - self.assert_called('GET', '/os-hypervisors/1234/uptime') - - def test_hypervisor_stats(self): - self.run_command('hypervisor-stats') - self.assert_called('GET', '/os-hypervisors/statistics') - - def test_quota_show(self): - self.run_command('quota-show test') - self.assert_called('GET', '/os-quota-sets/test') - - def test_quota_defaults(self): - self.run_command('quota-defaults test') - self.assert_called('GET', '/os-quota-sets/test/defaults') - - def test_quota_update(self): - self.run_command('quota-update test --instances=5') - self.assert_called('PUT', '/os-quota-sets/test') - - def test_quota_class_show(self): - self.run_command('quota-class-show test') - self.assert_called('GET', '/os-quota-class-sets/test') - - def test_quota_class_update(self): - self.run_command('quota-class-update test --instances=5') - self.assert_called('PUT', '/os-quota-class-sets/test') - - def test_network_list(self): - self.run_command('network-list') - self.assert_called('GET', '/os-networks') - - def test_network_show(self): - self.run_command('network-show 1') - self.assert_called('GET', '/os-networks/1') - - def test_backup(self): - self.run_command('backup sample-server back1 daily 1') - self.assert_called('POST', '/servers/1234/action', - {'createBackup': {'name': 'back1', - 'backup_type': 'daily', - 'rotation': '1'}}) - self.run_command('backup 1234 back1 daily 1') - self.assert_called('POST', '/servers/1234/action', - {'createBackup': {'name': 'back1', - 'backup_type': 'daily', - 'rotation': '1'}}) diff --git a/tests/v1_1/test_usage.py b/tests/v1_1/test_usage.py deleted file mode 100644 index 10ca37db6..000000000 --- a/tests/v1_1/test_usage.py +++ /dev/null @@ -1,35 +0,0 @@ -import datetime - -from novaclient.v1_1 import usage -from tests import utils -from tests.v1_1 import fakes - - -cs = fakes.FakeClient() - - -class UsageTest(utils.TestCase): - - def test_usage_list(self, detailed=False): - now = datetime.datetime.now() - usages = cs.usage.list(now, now, detailed) - - cs.assert_called('GET', - "/os-simple-tenant-usage?" + - ("start=%s&" % now.isoformat()) + - ("end=%s&" % now.isoformat()) + - ("detailed=%s" % int(bool(detailed)))) - [self.assertTrue(isinstance(u, usage.Usage)) for u in usages] - - def test_usage_list_detailed(self): - self.test_usage_list(True) - - def test_usage_get(self): - now = datetime.datetime.now() - u = cs.usage.get("tenantfoo", now, now) - - cs.assert_called('GET', - "/os-simple-tenant-usage/tenantfoo?" + - ("start=%s&" % now.isoformat()) + - ("end=%s" % now.isoformat())) - self.assertTrue(isinstance(u, usage.Usage)) diff --git a/tests/v1_1/utils.py b/tests/v1_1/utils.py deleted file mode 100644 index f878a5e26..000000000 --- a/tests/v1_1/utils.py +++ /dev/null @@ -1,29 +0,0 @@ -from nose.tools import ok_ - - -def fail(msg): - raise AssertionError(msg) - - -def assert_in(thing, seq, msg=None): - msg = msg or "'%s' not found in %s" % (thing, seq) - ok_(thing in seq, msg) - - -def assert_not_in(thing, seq, msg=None): - msg = msg or "unexpected '%s' found in %s" % (thing, seq) - ok_(thing not in seq, msg) - - -def assert_has_keys(dict, required=[], optional=[]): - keys = dict.keys() - for k in required: - assert_in(k, keys, "required key %s missing from %s" % (k, dict)) - allowed_keys = set(required) | set(optional) - extra_keys = set(keys).difference(set(required + optional)) - if extra_keys: - fail("found unexpected keys: %s" % list(extra_keys)) - - -def assert_isinstance(thing, kls): - ok_(isinstance(thing, kls), "%s is not an instance of %s" % (thing, kls)) diff --git a/tools/install_venv.py b/tools/install_venv.py deleted file mode 100644 index 6cb3a6938..000000000 --- a/tools/install_venv.py +++ /dev/null @@ -1,246 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# Copyright 2010 OpenStack, LLC -# -# 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. - -""" -Installation script for Nova's development virtualenv -""" - -import optparse -import os -import subprocess -import sys -import platform - - -ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) -VENV = os.path.join(ROOT, '.venv') -PIP_REQUIRES = os.path.join(ROOT, 'tools', 'pip-requires') -TEST_REQUIRES = os.path.join(ROOT, 'tools', 'test-requires') -PY_VERSION = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) - - -def die(message, *args): - print >> sys.stderr, message % args - sys.exit(1) - - -def check_python_version(): - if sys.version_info < (2, 6): - die("Need Python Version >= 2.6") - - -def run_command_with_code(cmd, redirect_output=True, check_exit_code=True): - """ - Runs a command in an out-of-process shell, returning the - output of that command. Working directory is ROOT. - """ - if redirect_output: - stdout = subprocess.PIPE - else: - stdout = None - - proc = subprocess.Popen(cmd, cwd=ROOT, stdout=stdout) - output = proc.communicate()[0] - if check_exit_code and proc.returncode != 0: - die('Command "%s" failed.\n%s', ' '.join(cmd), output) - return (output, proc.returncode) - - -def run_command(cmd, redirect_output=True, check_exit_code=True): - return run_command_with_code(cmd, redirect_output, check_exit_code)[0] - - -class Distro(object): - - def check_cmd(self, cmd): - return bool(run_command(['which', cmd], check_exit_code=False).strip()) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if self.check_cmd('easy_install'): - print 'Installing virtualenv via easy_install...', - if run_command(['easy_install', 'virtualenv']): - print 'Succeeded' - return - else: - print 'Failed' - - die('ERROR: virtualenv not found.\n\nDevelopment' - ' requires virtualenv, please install it using your' - ' favorite package management tool') - - def post_process(self): - """Any distribution-specific post-processing gets done here. - - In particular, this is useful for applying patches to code inside - the venv.""" - pass - - -class Debian(Distro): - """This covers all Debian-based distributions.""" - - def check_pkg(self, pkg): - return run_command_with_code(['dpkg', '-l', pkg], - check_exit_code=False)[1] == 0 - - def apt_install(self, pkg, **kwargs): - run_command(['sudo', 'apt-get', 'install', '-y', pkg], **kwargs) - - def apply_patch(self, originalfile, patchfile): - run_command(['patch', originalfile, patchfile]) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.apt_install('python-virtualenv', check_exit_code=False) - - super(Debian, self).install_virtualenv() - - -class Fedora(Distro): - """This covers all Fedora-based distributions. - - Includes: Fedora, RHEL, CentOS, Scientific Linux""" - - def check_pkg(self, pkg): - return run_command_with_code(['rpm', '-q', pkg], - check_exit_code=False)[1] == 0 - - def yum_install(self, pkg, **kwargs): - run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs) - - def apply_patch(self, originalfile, patchfile): - run_command(['patch', originalfile, patchfile]) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.yum_install('python-virtualenv', check_exit_code=False) - - super(Fedora, self).install_virtualenv() - - -def get_distro(): - if os.path.exists('/etc/fedora-release') or \ - os.path.exists('/etc/redhat-release'): - return Fedora() - elif os.path.exists('/etc/debian_version'): - return Debian() - else: - return Distro() - - -def check_dependencies(): - get_distro().install_virtualenv() - - -def create_virtualenv(venv=VENV, no_site_packages=True): - """Creates the virtual environment and installs PIP only into the - virtual environment - """ - print 'Creating venv...', - if no_site_packages: - run_command(['virtualenv', '-q', '--no-site-packages', VENV]) - else: - run_command(['virtualenv', '-q', VENV]) - print 'done.' - print 'Installing pip in virtualenv...', - if not run_command(['tools/with_venv.sh', 'easy_install', - 'pip>1.0']).strip(): - die("Failed to install pip.") - print 'done.' - - -def pip_install(*args): - run_command(['tools/with_venv.sh', - 'pip', 'install', '--upgrade'] + list(args), - redirect_output=False) - - -def install_dependencies(venv=VENV): - print 'Installing dependencies with pip (this can take a while)...' - - # First things first, make sure our venv has the latest pip and distribute. - pip_install('pip') - pip_install('distribute') - - pip_install('-r', PIP_REQUIRES) - pip_install('-r', TEST_REQUIRES) - - # Tell the virtual env how to "import nova" - pthfile = os.path.join(venv, "lib", PY_VERSION, "site-packages", - "novaclient.pth") - f = open(pthfile, 'w') - f.write("%s\n" % ROOT) - - -def post_process(): - get_distro().post_process() - - -def print_help(): - help = """ - python-novaclient development environment setup is complete. - - python-novaclient development uses virtualenv to track and manage Python - dependencies while in development and testing. - - To activate the python-novaclient virtualenv for the extent of your current - shell session you can run: - - $ source .venv/bin/activate - - Or, if you prefer, you can run commands in the virtualenv on a case by case - basis by running: - - $ tools/with_venv.sh - - Also, make test will automatically use the virtualenv. - """ - print help - - -def parse_args(): - """Parse command-line arguments""" - parser = optparse.OptionParser() - parser.add_option("-n", "--no-site-packages", dest="no_site_packages", - default=False, action="store_true", - help="Do not inherit packages from global Python install") - return parser.parse_args() - - -def main(argv): - (options, args) = parse_args() - check_python_version() - check_dependencies() - create_virtualenv(no_site_packages=options.no_site_packages) - install_dependencies() - post_process() - print_help() - -if __name__ == '__main__': - main(sys.argv) diff --git a/tools/nova.bash_completion b/tools/nova.bash_completion index a552de87f..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/\s-h\s/\s/"`" - _nova_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" - _nova_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" - _nova_opts_exp="`echo "$_nova_opts" | sed -e "s/\s/|/g"`" - 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/nova.zsh_completion b/tools/nova.zsh_completion new file mode 100644 index 000000000..43c4d649a --- /dev/null +++ b/tools/nova.zsh_completion @@ -0,0 +1,29 @@ +#compdef nova + +local -a nbc _nova_opts _nova_flags _nova_opts_exp cur prev + +nbc=(${(ps: :)$(_call_program options "$service bash-completion" 2>/dev/null)}) +_nova_opts=(${nbc:#-*}) +_nova_flags=(${(M)nbc:#-*}) +_nova_opt_exp=${${nbc:#-*}// /|} +cur=$words[CURRENT] +prev=$words[(( CURRENT - 1 ))] + +_checkcomp(){ + for word in $words[@]; do + if [[ -n ${_nova_opts[(r)$word]} ]]; then + return 0 + fi + done + return 1 +} + +echo $_nova_opts[@] |grep --color nova +if [[ "$prev" != "help" ]] && _checkcomp; then + COMPLETION_CACHE=(~/.novaclient/*/*-cache) + cflags=($_nova_flags[@] ${(ps: :)$(cat $COMPLETION_CACHE 2>/dev/null)}) + compadd "$@" -d $cflags[@] +else + compadd "$@" -d $_nova_opts[@] +fi + diff --git a/tools/pip-requires b/tools/pip-requires deleted file mode 100644 index 2c409b3c2..000000000 --- a/tools/pip-requires +++ /dev/null @@ -1,5 +0,0 @@ -argparse -httplib2 -iso8601>=0.1.4 -prettytable>=0.6,<0.7 -simplejson diff --git a/tools/test-requires b/tools/test-requires deleted file mode 100644 index 43a946955..000000000 --- a/tools/test-requires +++ /dev/null @@ -1,11 +0,0 @@ -distribute>=0.6.24 - -mock -nose -nose-exclude -nosexcover -openstack.nose_plugin -nosehtmloutput -pep8==1.1 -sphinx>=1.1.2 -unittest2 diff --git a/tools/with_venv.sh b/tools/with_venv.sh deleted file mode 100755 index c8d2940fc..000000000 --- a/tools/with_venv.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -TOOLS=`dirname $0` -VENV=$TOOLS/../.venv -source $VENV/bin/activate && $@ diff --git a/tox.ini b/tox.ini index 7ddc885b1..e52b28949 100644 --- a/tox.ini +++ b/tox.ini @@ -1,46 +1,124 @@ [tox] -envlist = py26,py27,pep8 +envlist = py3,pep8,docs +minversion = 4.6.0 [testenv] -setenv = VIRTUAL_ENV={envdir} - NOSE_WITH_OPENSTACK=1 - NOSE_OPENSTACK_COLOR=1 - NOSE_OPENSTACK_RED=0.05 - NOSE_OPENSTACK_YELLOW=0.025 - NOSE_OPENSTACK_SHOW_ELAPSED=1 -deps = -r{toxinidir}/tools/pip-requires - -r{toxinidir}/tools/test-requires -commands = nosetests +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 + stestr run {posargs} [testenv:pep8] -deps = pep8==1.1 -commands = pep8 --repeat --show-source novaclient setup.py +description = + Run style checks. +deps = + pre-commit +commands = + pre-commit run --all-files --show-diff-on-failure + +[testenv:bandit] +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:cover] -commands = nosetests --cover-erase --cover-package=novaclient --with-xcoverage +[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 = + 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 -[tox:jenkins] -downloadcache = ~/cache/pip +[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:jenkins26] -basepython = python2.6 -setenv = NOSE_WITH_XUNIT=1 -deps = file://{toxinidir}/.cache.bundle +[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:jenkins27] -basepython = python2.7 -setenv = NOSE_WITH_XUNIT=1 -deps = file://{toxinidir}/.cache.bundle +[testenv:cover] +description = + Run unit tests and print coverage information. +setenv = + PYTHON=coverage run --source novaclient --parallel-mode +commands = + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report -[testenv:jenkinscover] -deps = file://{toxinidir}/.cache.bundle -setenv = NOSE_WITH_XUNIT=1 -commands = nosetests --cover-erase --cover-package=novaclient --with-xcoverage +[flake8] +# Following checks should be enabled in the future. +# +# H404 multi line docstring should start without a leading new line +# H405 multi line docstring summary not separated with an empty line +# +# Following checks are ignored on purpose. +# +# 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 -[testenv:jenkinsvenv] -deps = file://{toxinidir}/.cache.bundle -setenv = NOSE_WITH_XUNIT=1 -commands = {posargs} +[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