summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.stestr.conf3
-rw-r--r--.testr.conf7
-rw-r--r--README.rst6
-rw-r--r--doc/source/cli/nova.rst63
-rw-r--r--doc/source/user/shell.rst10
-rw-r--r--lower-constraints.txt7
-rw-r--r--novaclient/__init__.py2
-rw-r--r--novaclient/shell.py53
-rw-r--r--novaclient/tests/functional/v2/legacy/test_readonly_nova.py2
-rw-r--r--novaclient/tests/functional/v2/test_server_groups.py69
-rw-r--r--novaclient/tests/unit/test_shell.py25
-rw-r--r--novaclient/tests/unit/v2/fakes.py9
-rw-r--r--novaclient/tests/unit/v2/test_server_groups.py34
-rw-r--r--novaclient/tests/unit/v2/test_servers.py67
-rw-r--r--novaclient/tests/unit/v2/test_shell.py295
-rw-r--r--novaclient/v2/server_groups.py30
-rw-r--r--novaclient/v2/servers.py35
-rw-r--r--novaclient/v2/shell.py114
-rw-r--r--releasenotes/notes/bug-1744118-0b064d7062117317.yaml15
-rw-r--r--releasenotes/notes/bug-1778536-a1b5d65a0d4ad622.yaml12
-rw-r--r--releasenotes/notes/microversion-v2_63-cd058a9145550cae.yaml17
-rw-r--r--releasenotes/notes/microversion-v2_64-66366829ec65bea4.yaml.yaml15
-rw-r--r--requirements.txt2
-rw-r--r--test-requirements.txt4
-rwxr-xr-xtools/pretty_tox.sh16
-rw-r--r--tox.ini34
27 files changed, 871 insertions, 77 deletions
diff --git a/.gitignore b/.gitignore
index 82ede6ad..611c10e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
.coverage
.venv
-.testrepository
+.stestr/
subunit.log
.tox
*,cover
diff --git a/.stestr.conf b/.stestr.conf
new file mode 100644
index 00000000..ac945e83
--- /dev/null
+++ b/.stestr.conf
@@ -0,0 +1,3 @@
+[DEFAULT]
+test_path=${OS_TEST_PATH:-./novaclient/tests/unit}
+top_dir=./
diff --git a/.testr.conf b/.testr.conf
deleted file mode 100644
index c8fae426..00000000
--- a/.testr.conf
+++ /dev/null
@@ -1,7 +0,0 @@
-[DEFAULT]
-test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
- OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
- OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-300} \
- ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./novaclient/tests/unit} $LISTOPT $IDOPTION
-test_id_option=--load-list $IDFILE
-test_list_option=--list
diff --git a/README.rst b/README.rst
index 15655512..06914fe4 100644
--- a/README.rst
+++ b/README.rst
@@ -15,10 +15,6 @@ Python bindings to the OpenStack Compute API
:target: https://pypi.org/project/python-novaclient/
:alt: Latest Version
-.. image:: https://img.shields.io/pypi/dm/python-novaclient.svg
- :target: https://pypi.org/project/python-novaclient/
- :alt: Downloads
-
This is a client for the OpenStack Compute API. It provides a Python API (the
``novaclient`` module) and a command-line script (``nova``). Each implements
100% of the OpenStack Compute API.
@@ -32,6 +28,7 @@ This is a client for the OpenStack Compute API. It provides a Python API (the
* `Source`_
* `Specs`_
* `How to Contribute`_
+* `Release Notes`_
.. _PyPi: https://pypi.org/project/python-novaclient
.. _Online Documentation: https://docs.openstack.org/python-novaclient/latest
@@ -41,3 +38,4 @@ This is a client for the OpenStack Compute API. It provides a Python API (the
.. _Source: https://git.openstack.org/cgit/openstack/python-novaclient
.. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html
.. _Specs: http://specs.openstack.org/openstack/nova-specs/
+.. _Release Notes: https://docs.openstack.org/releasenotes/python-novaclient
diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst
index bb5738c6..55ea49f8 100644
--- a/doc/source/cli/nova.rst
+++ b/doc/source/cli/nova.rst
@@ -33,7 +33,7 @@ nova usage
[--service-name <service-name>]
[--os-endpoint-type <endpoint-type>]
[--os-compute-api-version <compute-api-ver>]
- [--endpoint-override <bypass-url>] [--profile HMAC_KEY]
+ [--os-endpoint-override <bypass-url>] [--profile HMAC_KEY]
[--insecure] [--os-cacert <ca-certificate>]
[--os-cert <certificate>] [--os-key <key>] [--timeout <seconds>]
[--os-auth-type <name>] [--os-auth-url OS_AUTH_URL]
@@ -678,10 +678,10 @@ nova optional arguments
minor part) or "X.latest", defaults to
``env[OS_COMPUTE_API_VERSION]``.
-``--endpoint-override <bypass-url>``
+``--os-endpoint-override <bypass-url>``
Use this API endpoint instead of the Service
Catalog. Defaults to
- ``env[NOVACLIENT_ENDPOINT_OVERRIDE]``.
+ ``env[OS_ENDPOINT_OVERRIDE]``.
``--profile HMAC_KEY``
HMAC key to use for encrypting context data
@@ -1011,6 +1011,7 @@ nova boot
[--config-drive <value>] [--poll] [--admin-pass <value>]
[--access-ip-v4 <value>] [--access-ip-v6 <value>]
[--description <description>]
+ [--trusted-image-certificate-id <trusted-image-certificate-id>]
<name>
Boot a new server.
@@ -1164,6 +1165,13 @@ Boot a new server.
Description for the server. (Supported by API
versions '2.19' - '2.latest')
+``--trusted-image-certificate-id <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')
+
.. _nova_cell-capacities:
nova cell-capacities
@@ -1499,6 +1507,29 @@ Show details about the given flavor.
``<flavor>``
Name or ID of flavor.
+nova flavor-update
+------------------
+
+.. code-block:: console
+
+ usage: nova flavor-update <flavor> <description>
+
+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**
+
+``<flavor>``
+ Name or ID of the flavor to update.
+
+``<description>``
+ A free form description of the flavor. Limited to 65535
+ characters in length. Only printable characters are allowed.
+
.. _nova_force-delete:
nova force-delete
@@ -2660,6 +2691,8 @@ nova rebuild
[--minimal] [--preserve-ephemeral] [--name <name>]
[--description <description>] [--meta <key=value>]
[--file <dst-path=src-path>]
+ [--trusted-image-certificate-id <trusted-image-certificate-id>]
+ [--trusted-image-certificates-unset]
<server> <image>
Shutdown, re-image, and re-boot a server.
@@ -2707,6 +2740,18 @@ Shutdown, re-image, and re-boot a server.
to <dst-path> on the new server. You may store
up to 5 files.
+``--trusted-image-certificate-id <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')
+
.. _nova_refresh-network:
nova refresh-network
@@ -2906,7 +2951,7 @@ nova server-group-create
.. code-block:: console
- usage: nova server-group-create <name> <policy> [<policy> ...]
+ usage: nova server-group-create [--rules <key=value>] <name> <policy>
Create a new server group with the specified details.
@@ -2916,7 +2961,15 @@ Create a new server group with the specified details.
Server group name.
``<policy>``
- Policies for the server groups.
+ Policy for the server groups.
+
+``--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:
diff --git a/doc/source/user/shell.rst b/doc/source/user/shell.rst
index bd1fb7e9..882bb756 100644
--- a/doc/source/user/shell.rst
+++ b/doc/source/user/shell.rst
@@ -60,6 +60,16 @@ some environment variables:
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
diff --git a/lower-constraints.txt b/lower-constraints.txt
index 3f7fd32a..4f156569 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -10,6 +10,7 @@ cmd2==0.8.0
contextlib2==0.4.0
coverage==4.0
cryptography==2.1
+ddt==1.0.1
debtcollector==1.2.0
decorator==3.4.0
deprecation==1.0
@@ -47,7 +48,6 @@ netifaces==0.10.4
openstacksdk==0.11.2
os-client-config==1.28.0
os-service-types==1.2.0
-os-testr==1.0.0
osc-lib==1.8.0
oslo.concurrency==3.25.0
oslo.config==5.2.0
@@ -87,7 +87,7 @@ pytz==2013.6
PyYAML==3.12
repoze.lru==0.7
requests==2.14.2
-requests-mock==1.1.0
+requests-mock==1.2.0
requestsexceptions==1.2.0
rfc3986==0.3.1
Routes==2.3.1
@@ -95,11 +95,10 @@ simplejson==3.5.1
six==1.10.0
smmap==0.9.0
statsd==3.2.1
-stestr==1.0.0
stevedore==1.20.0
tempest==17.1.0
tenacity==3.2.1
-testrepository==0.0.18
+stestr==2.0.0
testscenarios==0.4
testtools==2.2.0
traceback2==1.4.0
diff --git a/novaclient/__init__.py b/novaclient/__init__.py
index 1530b37a..1923e6a1 100644
--- a/novaclient/__init__.py
+++ b/novaclient/__init__.py
@@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1")
# 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.62")
+API_MAX_VERSION = api_versions.APIVersion("2.64")
diff --git a/novaclient/shell.py b/novaclient/shell.py
index 2702fe23..8eb4311c 100644
--- a/novaclient/shell.py
+++ b/novaclient/shell.py
@@ -270,6 +270,14 @@ class OpenStackComputeShell(object):
'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(
@@ -350,19 +358,32 @@ class OpenStackComputeShell(object):
'"X.latest", defaults to env[OS_COMPUTE_API_VERSION].'))
parser.add_argument(
- '--endpoint-override',
+ '--os-endpoint-override',
metavar='<bypass-url>',
dest='endpoint_override',
- default=utils.env('NOVACLIENT_ENDPOINT_OVERRIDE',
+ default=utils.env('OS_ENDPOINT_OVERRIDE',
+ 'NOVACLIENT_ENDPOINT_OVERRIDE',
'NOVACLIENT_BYPASS_URL'),
help=_("Use this API endpoint instead of the Service Catalog. "
- "Defaults to env[NOVACLIENT_ENDPOINT_OVERRIDE]."))
+ "Defaults to env[OS_ENDPOINT_OVERRIDE]."))
+
+ # NOTE(takashin): This dummy '--end' argument was added
+ # to avoid misinterpreting command line arguments.
+ # If there is not this dummy argument, the '--end' is interpreted to
+ # the '--endpoint-override'.
+ # TODO(takashin): Remove this dummy '--end' argument
+ # when the deprecated '--endpoint-override' argument is removed.
+ parser.add_argument(
+ '--end',
+ metavar='<end>',
+ nargs='?',
+ help=argparse.SUPPRESS)
parser.add_argument(
- '--bypass-url',
+ '--endpoint-override',
action=DeprecatedAction,
- use=_('use "%s"; this option will be removed after Pike OpenStack '
- 'release.') % '--os-endpoint-override',
+ use=_('use "%s"; this option will be removed after Rocky '
+ 'OpenStack release.') % '--os-endpoint-override',
dest='endpoint_override',
help=argparse.SUPPRESS)
@@ -596,6 +617,26 @@ class OpenStackComputeShell(object):
_("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 = (
diff --git a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py
index df88b8c6..6b9598e9 100644
--- a/novaclient/tests/functional/v2/legacy/test_readonly_nova.py
+++ b/novaclient/tests/functional/v2/legacy/test_readonly_nova.py
@@ -124,4 +124,4 @@ class SimpleReadOnlyNovaClientTest(base.ClientTestBase):
self.assertRaises(exceptions.CommandFailed,
self.nova,
'list',
- flags='--endpoint-override badurl')
+ flags='--os-endpoint-override badurl')
diff --git a/novaclient/tests/functional/v2/test_server_groups.py b/novaclient/tests/functional/v2/test_server_groups.py
index b2ad1262..37ebaa66 100644
--- a/novaclient/tests/functional/v2/test_server_groups.py
+++ b/novaclient/tests/functional/v2/test_server_groups.py
@@ -17,7 +17,9 @@ 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.latest"
+ COMPUTE_API_VERSION = "2.13"
+ expected_metadata = True
+ expected_policy_rules = False
def test_create_server_group(self):
sg_id = self._create_sg("affinity")
@@ -29,6 +31,11 @@ class TestServerGroupClientV213(test_server_groups.TestServerGroupClient):
self._get_column_value_from_single_row_table(
sg, "Project Id")
self.assertEqual(sg_id, result)
+ self._get_column_value_from_single_row_table(sg, "Metadata")
+ self.assertIn(
+ 'affinity',
+ self._get_column_value_from_single_row_table(sg, 'Policies'))
+ self.assertNotIn('Rules', sg)
def test_list_server_groups(self):
sg_id = self._create_sg("affinity")
@@ -40,6 +47,22 @@ class TestServerGroupClientV213(test_server_groups.TestServerGroupClient):
self._get_column_value_from_single_row_table(
sg, "Project Id")
self.assertEqual(sg_id, result)
+ if self.expected_metadata:
+ self._get_column_value_from_single_row_table(sg, "Metadata")
+ else:
+ self.assertNotIn(sg, 'Metadata')
+ if self.expected_policy_rules:
+ self.assertEqual(
+ 'affinity',
+ self._get_column_value_from_single_row_table(sg, "Policy"))
+ self.assertEqual(
+ '{}',
+ self._get_column_value_from_single_row_table(sg, "Rules"))
+ else:
+ self.assertIn(
+ 'affinity',
+ self._get_column_value_from_single_row_table(sg, 'Policies'))
+ self.assertNotIn('Rules', sg)
def test_get_server_group(self):
sg_id = self._create_sg("affinity")
@@ -51,3 +74,47 @@ class TestServerGroupClientV213(test_server_groups.TestServerGroupClient):
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/unit/test_shell.py b/novaclient/tests/unit/test_shell.py
index 86047923..122f0727 100644
--- a/novaclient/tests/unit/test_shell.py
+++ b/novaclient/tests/unit/test_shell.py
@@ -16,6 +16,7 @@ import distutils.version as dist_version
import re
import sys
+import ddt
import fixtures
from keystoneauth1 import fixture
import mock
@@ -35,7 +36,11 @@ FAKE_ENV = {'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': 'http://no.where/v2.0',
- 'OS_COMPUTE_API_VERSION': '2'}
+ 'OS_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',
@@ -349,6 +354,7 @@ class ParserTest(utils.TestCase):
self.assertTrue(args.tic_tac)
+@ddt.ddt
class ShellTest(utils.TestCase):
_msg_no_tenant_project = ("You must provide a project name or project"
@@ -521,6 +527,23 @@ class ShellTest(utils.TestCase):
else:
self.fail('CommandError not raised')
+ @ddt.data(
+ (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', '')
+ )
+ @ddt.unpack
+ def test_basic_attributes(self, exclude, client_arg, env_var):
+ 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)
diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py
index 1d6f3f32..374648be 100644
--- a/novaclient/tests/unit/v2/fakes.py
+++ b/novaclient/tests/unit/v2/fakes.py
@@ -2236,8 +2236,13 @@ class FakeSessionClient(base_client.SessionClient):
return (200, {}, {"server_groups": server_groups})
def _return_server_group(self):
- r = {'server_group':
- self.get_os_server_groups()[2]['server_groups'][0]}
+ if self.api_version < api_versions.APIVersion("2.64"):
+ r = {'server_group':
+ self.get_os_server_groups()[2]['server_groups'][0]}
+ else:
+ r = {"members": [], "id": "2cbd51f4-fafe-4cdb-801b-cf913a6f288b",
+ 'server_group': {'name': 'ig1', 'policy': 'anti-affinity',
+ 'rules': {'max_server_per_host': 3}}}
return (200, {}, r)
def post_os_server_groups(self, body, **kw):
diff --git a/novaclient/tests/unit/v2/test_server_groups.py b/novaclient/tests/unit/v2/test_server_groups.py
index 9881b20a..40af1a13 100644
--- a/novaclient/tests/unit/v2/test_server_groups.py
+++ b/novaclient/tests/unit/v2/test_server_groups.py
@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+from novaclient import api_versions
from novaclient import exceptions
from novaclient.tests.unit.fixture_data import client
from novaclient.tests.unit.fixture_data import server_groups as data
@@ -106,3 +107,36 @@ class ServerGroupsTest(utils.FixturedTestCase):
self.cs.server_groups.find,
**kwargs)
self.assert_called('GET', '/os-server-groups')
+
+
+class ServerGroupsTestV264(ServerGroupsTest):
+ def setUp(self):
+ super(ServerGroupsTestV264, self).setUp()
+ self.cs.api_version = api_versions.APIVersion("2.64")
+
+ def test_create_server_group(self):
+ name = 'ig1'
+ policy = 'anti-affinity'
+ server_group = self.cs.server_groups.create(name, policy)
+ self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST)
+ body = {'server_group': {'name': name, 'policy': policy}}
+ self.assert_called('POST', '/os-server-groups', body)
+ self.assertIsInstance(server_group,
+ server_groups.ServerGroup)
+
+ def test_create_server_group_with_rules(self):
+ kwargs = {'name': 'ig1',
+ 'policy': 'anti-affinity',
+ 'rules': {'max_server_per_host': 3}}
+ server_group = self.cs.server_groups.create(**kwargs)
+ self.assert_request_id(server_group, fakes.FAKE_REQUEST_ID_LIST)
+ body = {
+ 'server_group': {
+ 'name': 'ig1',
+ 'policy': 'anti-affinity',
+ 'rules': {'max_server_per_host': 3}
+ }
+ }
+ self.assert_called('POST', '/os-server-groups', body)
+ self.assertIsInstance(server_group,
+ server_groups.ServerGroup)
diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py
index 07ad1497..540968cf 100644
--- a/novaclient/tests/unit/v2/test_servers.py
+++ b/novaclient/tests/unit/v2/test_servers.py
@@ -1542,3 +1542,70 @@ class ServersV257Test(ServersV256Test):
exceptions.UnsupportedAttribute, s.rebuild, image=1, name='new',
meta={'foo': 'bar'}, files=files)
self.assertIn('files', six.text_type(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', six.text_type(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', six.text_type(ex))
diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py
index b4ee79ac..b748dc6e 100644
--- a/novaclient/tests/unit/v2/test_shell.py
+++ b/novaclient/tests/unit/v2/test_shell.py
@@ -1155,6 +1155,113 @@ class ShellTest(utils.TestCase):
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']
+ }},
+ )
+
def test_flavor_list(self):
out, _ = self.run_command('flavor-list')
self.assert_called_anytime('GET', '/flavors/detail')
@@ -1664,6 +1771,148 @@ class ShellTest(utils.TestCase):
self.assertIn("Cannot specify '--user-data-unset' with "
"'--user-data'.", six.text_type(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'",
+ six.text_type(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'",
+ six.text_type(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_start(self):
self.run_command('start sample-server')
self.assert_called('POST', '/servers/1234/action', {'os-start': None})
@@ -3479,6 +3728,48 @@ class ShellTest(utils.TestCase):
{'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",
+ six.text_type(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')
@@ -3509,6 +3800,9 @@ class ShellTest(utils.TestCase):
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
@@ -3547,6 +3841,7 @@ class ShellTest(utils.TestCase):
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.
])
versions_supported = set(range(0,
novaclient.API_MAX_VERSION.ver_minor + 1))
diff --git a/novaclient/v2/server_groups.py b/novaclient/v2/server_groups.py
index 62a5c9a0..e8839ae5 100644
--- a/novaclient/v2/server_groups.py
+++ b/novaclient/v2/server_groups.py
@@ -17,7 +17,10 @@
Server group interface.
"""
+from novaclient import api_versions
from novaclient import base
+from novaclient import exceptions
+from novaclient.i18n import _
class ServerGroup(base.Resource):
@@ -80,6 +83,7 @@ class ServerGroupsManager(base.ManagerWithFind):
"""
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.
@@ -92,3 +96,29 @@ class ServerGroupsManager(base.ManagerWithFind):
body = {'server_group': {'name': name,
'policies': policies}}
return self._create('/os-server-groups', body, 'server_group')
+
+ @api_versions.wraps("2.64")
+ def create(self, name, policy, rules=None):
+ """Create (allocate) a server group.
+
+ :param name: The name of the server group.
+ :param policy: Policy name to associate with the server group.
+ :param rules: The rules of policy which is a dict, can be applied to
+ the policy, now only ``max_server_per_host`` for ``anti-affinity``
+ policy would be supported (optional).
+ :rtype: list of :class:`ServerGroup`
+ """
+ body = {'server_group': {
+ 'name': name, 'policy': policy
+ }}
+ if rules:
+ key = 'max_server_per_host'
+ try:
+ if key in rules:
+ rules[key] = int(rules[key])
+ except ValueError:
+ msg = _("Invalid '%(key)s' value: %(value)s")
+ raise exceptions.CommandError(msg % {
+ 'key': key, 'value': rules[key]})
+ body['server_group']['rules'] = rules
+ return self._create('/os-server-groups', body, 'server_group')
diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py
index a7b3eda1..d872fa01 100644
--- a/novaclient/v2/servers.py
+++ b/novaclient/v2/servers.py
@@ -650,7 +650,7 @@ class ServerManager(base.BootingManagerWithFind):
block_device_mapping_v2=None, nics=None, scheduler_hints=None,
config_drive=None, admin_pass=None, disk_config=None,
access_ip_v4=None, access_ip_v6=None, description=None,
- tags=None, **kwargs):
+ tags=None, trusted_image_certificates=None, **kwargs):
"""
Create (boot) a new server.
"""
@@ -768,6 +768,10 @@ class ServerManager(base.BootingManagerWithFind):
if tags:
body['server']['tags'] = tags
+ if trusted_image_certificates:
+ body['server']['trusted_image_certificates'] = (
+ trusted_image_certificates)
+
return self._create('/servers', body, response_key,
return_raw=return_raw, **kwargs)
@@ -1191,7 +1195,8 @@ class ServerManager(base.BootingManagerWithFind):
block_device_mapping=None, block_device_mapping_v2=None,
nics=None, scheduler_hints=None,
config_drive=None, disk_config=None, admin_pass=None,
- access_ip_v4=None, access_ip_v6=None, **kwargs):
+ access_ip_v4=None, access_ip_v6=None,
+ trusted_image_certificates=None, **kwargs):
# TODO(anthony): indicate in doc string if param is an extension
# and/or optional
"""
@@ -1252,6 +1257,8 @@ class ServerManager(base.BootingManagerWithFind):
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)
"""
if not min_count:
min_count = 1
@@ -1292,6 +1299,12 @@ class ServerManager(base.BootingManagerWithFind):
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")
+
boot_kwargs = dict(
meta=meta, files=files, userdata=userdata,
reservation_id=reservation_id, min_count=min_count,
@@ -1299,7 +1312,8 @@ class ServerManager(base.BootingManagerWithFind):
key_name=key_name, availability_zone=availability_zone,
scheduler_hints=scheduler_hints, config_drive=config_drive,
disk_config=disk_config, admin_pass=admin_pass,
- access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6, **kwargs)
+ access_ip_v4=access_ip_v4, access_ip_v6=access_ip_v6,
+ trusted_image_certificates=trusted_image_certificates, **kwargs)
if block_device_mapping:
boot_kwargs['block_device_mapping'] = block_device_mapping
@@ -1416,6 +1430,9 @@ class ServerManager(base.BootingManagerWithFind):
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 (allowed since microversion 2.63)
:returns: :class:`Server`
"""
descr_microversion = api_versions.APIVersion("2.19")
@@ -1436,6 +1453,15 @@ class ServerManager(base.BootingManagerWithFind):
if 'userdata' in kwargs and self.api_version < files_and_userdata:
raise exceptions.UnsupportedAttribute('userdata', '2.57')
+ trusted_certs_microversion = api_versions.APIVersion("2.63")
+ # trusted_image_certificates is intentionally *not* a named kwarg
+ # so that trusted_image_certificates=None is not confused with an
+ # intentional unset/reset request.
+ if ("trusted_image_certificates" in kwargs and
+ self.api_version < trusted_certs_microversion):
+ raise exceptions.UnsupportedAttribute("trusted_image_certificates",
+ "2.63")
+
body = {'imageRef': base.getid(image)}
if password is not None:
body['adminPass'] = password
@@ -1449,6 +1475,9 @@ class ServerManager(base.BootingManagerWithFind):
body["description"] = kwargs["description"]
if 'key_name' in kwargs:
body['key_name'] = kwargs['key_name']
+ if "trusted_image_certificates" in kwargs:
+ body["trusted_image_certificates"] = kwargs[
+ "trusted_image_certificates"]
if meta:
body['metadata'] = meta
if files:
diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py
index 15beadd8..4f8b695c 100644
--- a/novaclient/v2/shell.py
+++ b/novaclient/v2/shell.py
@@ -510,6 +510,19 @@ def _boot(cs, args):
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")
+
return boot_args, boot_kwargs
@@ -874,6 +887,18 @@ def _boot(cs, args):
action="store_true",
default=False,
help=_("Return a reservation id bound to created servers."))
+@utils.arg(
+ '--trusted-image-certificate-id',
+ metavar='<trusted-image-certificate-id>',
+ 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")
def do_boot(cs, args):
"""Boot a new server."""
boot_args, boot_kwargs = _boot(cs, args)
@@ -1807,6 +1832,25 @@ def do_reboot(cs, args):
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='<trusted-image-certificate-id>',
+ 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")
def do_rebuild(cs, args):
"""Shutdown, re-image, and re-boot a server."""
server = _find_server(cs, args.server)
@@ -1861,6 +1905,34 @@ def do_rebuild(cs, args):
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")
+
server = server.rebuild(image, _password, **kwargs)
_print_server(cs, args, server)
@@ -4453,16 +4525,15 @@ def do_availability_zone_list(cs, _args):
sortby_index=None)
-@api_versions.wraps("2.0", "2.12")
def _print_server_group_details(cs, server_group):
- columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata']
- utils.print_list(server_group, columns)
-
-
-@api_versions.wraps("2.13")
-def _print_server_group_details(cs, server_group): # noqa
- columns = ['Id', 'Name', 'Project Id', 'User Id',
- 'Policies', 'Members', 'Metadata']
+ if cs.api_version < api_versions.APIVersion('2.13'):
+ columns = ['Id', 'Name', 'Policies', 'Members', 'Metadata']
+ elif cs.api_version < api_versions.APIVersion('2.64'):
+ columns = ['Id', 'Name', 'Project Id', 'User Id',
+ 'Policies', 'Members', 'Metadata']
+ else:
+ columns = ['Id', 'Name', 'Project Id', 'User Id',
+ 'Policy', 'Rules', 'Members']
utils.print_list(server_group, columns)
@@ -4497,6 +4568,7 @@ def do_server_group_list(cs, args):
_print_server_group_details(cs, server_groups)
+@api_versions.wraps("2.0", "2.63")
@utils.arg('name', metavar='<name>', help=_('Server group name.'))
@utils.arg(
'policy',
@@ -4509,6 +4581,30 @@ def do_server_group_create(cs, args):
_print_server_group_details(cs, [server_group])
+@api_versions.wraps("2.64")
+@utils.arg('name', metavar='<name>', help=_('Server group name.'))
+@utils.arg(
+ 'policy',
+ metavar='<policy>',
+ help=_('Policy for the server group.'))
+@utils.arg(
+ '--rule',
+ metavar="<key=value>",
+ 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='<id>',
diff --git a/releasenotes/notes/bug-1744118-0b064d7062117317.yaml b/releasenotes/notes/bug-1744118-0b064d7062117317.yaml
new file mode 100644
index 00000000..3a9688c2
--- /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-1778536-a1b5d65a0d4ad622.yaml b/releasenotes/notes/bug-1778536-a1b5d65a0d4ad622.yaml
new file mode 100644
index 00000000..3e5410aa
--- /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/microversion-v2_63-cd058a9145550cae.yaml b/releasenotes/notes/microversion-v2_63-cd058a9145550cae.yaml
new file mode 100644
index 00000000..f8299656
--- /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 00000000..0bd5d4a5
--- /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/requirements.txt b/requirements.txt
index 502c9df1..9ac07a62 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,7 +7,7 @@ iso8601>=0.1.11 # MIT
oslo.i18n>=3.15.3 # Apache-2.0
oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
oslo.utils>=3.33.0 # Apache-2.0
-PrettyTable<0.8,>=0.7.1 # BSD
+PrettyTable<0.8,>=0.7.2 # BSD
simplejson>=3.5.1 # MIT
six>=1.10.0 # MIT
Babel!=2.4.0,>=2.3.4 # BSD
diff --git a/test-requirements.txt b/test-requirements.txt
index e4062860..3c35fec9 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -5,6 +5,7 @@ hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
bandit>=1.1.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
+ddt>=1.0.1 # MIT
fixtures>=3.0.0 # Apache-2.0/BSD
keyring>=5.5.1 # MIT/PSF
mock>=2.0.0 # BSD
@@ -14,9 +15,8 @@ python-glanceclient>=2.8.0 # Apache-2.0
python-neutronclient>=6.7.0 # Apache-2.0
requests-mock>=1.2.0 # Apache-2.0
os-client-config>=1.28.0 # Apache-2.0
-os-testr>=1.0.0 # Apache-2.0
osprofiler>=1.4.0 # Apache-2.0
-testrepository>=0.0.18 # Apache-2.0/BSD
+stestr>=2.0.0 # Apache-2.0
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=2.2.0 # MIT
tempest>=17.1.0 # Apache-2.0
diff --git a/tools/pretty_tox.sh b/tools/pretty_tox.sh
deleted file mode 100755
index 799ac184..00000000
--- a/tools/pretty_tox.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env bash
-
-set -o pipefail
-
-TESTRARGS=$1
-
-# --until-failure is not compatible with --subunit see:
-#
-# https://bugs.launchpad.net/testrepository/+bug/1411804
-#
-# this work around exists until that is addressed
-if [[ "$TESTARGS" =~ "until-failure" ]]; then
- python setup.py testr --slowest --testr-args="$TESTRARGS"
-else
- python setup.py testr --slowest --testr-args="--subunit $TESTRARGS" | subunit-trace -f
-fi
diff --git a/tox.ini b/tox.ini
index b4dfa8f4..b3666b18 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,8 +7,9 @@ skipsdist = True
[testenv]
usedevelop = True
# tox is silly... these need to be separated by a newline....
-whitelist_externals = find
- bash
+whitelist_externals =
+ find
+ rm
passenv = ZUUL_CACHE_DIR
REQUIREMENTS_PIP_LOCATION
install_command = pip install {opts} {packages}
@@ -18,17 +19,18 @@ deps =
-r{toxinidir}/requirements.txt
commands =
find . -type f -name "*.pyc" -delete
- bash tools/pretty_tox.sh '{posargs}'
- # there is also secret magic in pretty_tox.sh which lets you run in a fail only
- # mode. To do this define the TRACE_FAILONLY environmental variable.
+ stestr run {posargs}
[testenv:pep8]
+basepython = python3
commands = flake8 {posargs}
[testenv:bandit]
+basepython = python3
commands = bandit -r novaclient -n5 -x tests
[testenv:venv]
+basepython = python3
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/test-requirements.txt
@@ -37,14 +39,17 @@ deps =
commands = {posargs}
[testenv:docs]
+basepython = python3
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
-r{toxinidir}/doc/requirements.txt
commands =
+ rm -rf doc/build
sphinx-build -b html doc/source doc/build/html
[testenv:releasenotes]
+basepython = python3
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
@@ -55,25 +60,27 @@ commands =
[testenv:functional]
basepython = python2.7
passenv = OS_NOVACLIENT_TEST_NETWORK
-setenv =
- OS_TEST_PATH = ./novaclient/tests/functional
commands =
- bash tools/pretty_tox.sh '--concurrency=1 {posargs}'
+ stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs}
python novaclient/tests/functional/hooks/check_resources.py
[testenv:functional-py35]
basepython = python3.5
passenv = OS_NOVACLIENT_TEST_NETWORK
-setenv =
- OS_TEST_PATH = ./novaclient/tests/functional
commands =
- bash tools/pretty_tox.sh '--concurrency=1 {posargs}'
+ stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs}
python novaclient/tests/functional/hooks/check_resources.py
[testenv:cover]
+basepython = python3
+setenv =
+ PYTHON=coverage run --source novaclient --parallel-mode
commands =
- python setup.py testr --coverage --testr-args='{posargs}'
- coverage report
+ stestr run {posargs}
+ coverage combine
+ coverage html -d cover
+ coverage xml -o cover/coverage.xml
+ coverage report
[flake8]
# Following checks should be enabled in the future.
@@ -92,6 +99,7 @@ exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build,doc/source/conf.py,releasen
import_exceptions = novaclient.i18n
[testenv:bindep]
+basepython = python3
# 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