diff options
27 files changed, 871 insertions, 77 deletions
@@ -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 @@ -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 @@ -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 |