diff options
-rw-r--r-- | .zuul.yaml | 5 | ||||
-rw-r--r-- | doc/source/cli/nova.rst | 39 | ||||
-rw-r--r-- | novaclient/__init__.py | 2 | ||||
-rw-r--r-- | novaclient/tests/functional/v2/test_migrations.py | 13 | ||||
-rw-r--r-- | novaclient/tests/unit/fixture_data/aggregates.py | 7 | ||||
-rw-r--r-- | novaclient/tests/unit/v2/fakes.py | 36 | ||||
-rw-r--r-- | novaclient/tests/unit/v2/test_aggregates.py | 40 | ||||
-rw-r--r-- | novaclient/tests/unit/v2/test_migrations.py | 57 | ||||
-rw-r--r-- | novaclient/tests/unit/v2/test_shell.py | 122 | ||||
-rw-r--r-- | novaclient/v2/aggregates.py | 22 | ||||
-rw-r--r-- | novaclient/v2/images.py | 37 | ||||
-rw-r--r-- | novaclient/v2/migrations.py | 47 | ||||
-rw-r--r-- | novaclient/v2/shell.py | 66 | ||||
-rw-r--r-- | playbooks/legacy/novaclient-dsvm-functional/run.yaml | 1 | ||||
-rw-r--r-- | releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml | 4 | ||||
-rw-r--r-- | releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml | 20 | ||||
-rw-r--r-- | releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml | 10 | ||||
-rw-r--r-- | setup.cfg | 2 | ||||
-rw-r--r-- | tox.ini | 20 |
19 files changed, 514 insertions, 36 deletions
@@ -1,4 +1,5 @@ - job: + # TODO(efried): Cut over to zuulv3 name: novaclient-dsvm-functional parent: legacy-dsvm-base run: playbooks/legacy/novaclient-dsvm-functional/run.yaml @@ -16,12 +17,10 @@ - project: templates: - check-requirements - - lib-forward-testing - lib-forward-testing-python3 - openstack-cover-jobs - openstack-lower-constraints-jobs - - openstack-python-jobs - - openstack-python3-train-jobs + - openstack-python3-ussuri-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: diff --git a/doc/source/cli/nova.rst b/doc/source/cli/nova.rst index cada3d62..84c441bd 100644 --- a/doc/source/cli/nova.rst +++ b/doc/source/cli/nova.rst @@ -73,6 +73,10 @@ nova usage ``aggregate-add-host`` Add the host to the specified aggregate. +``aggregate-cache-images`` + Request images be pre-cached on hosts within an aggregate. + (Supported by API versions '2.81' - '2.latest') + ``aggregate-create`` Create a new aggregate with the specified details. @@ -463,6 +467,7 @@ nova usage ``server-topology`` Retrieve NUMA topology of the given server. + (Supported by API versions '2.78' - '2.latest') ``service-delete`` Delete the service. @@ -756,6 +761,28 @@ Add the host to the specified aggregate. ``<host>`` The host to add to the aggregate. +.. _nova_aggregate-cache-images: + +nova aggregate-cache-images +--------------------------- + +.. code-block:: console + + usage: nova aggregate-cache-images <aggregate> <image> [<image> ..] + +Request image(s) be pre-cached on hosts within the aggregate. +(Supported by API versions '2.81' - '2.latest') + +.. versionadded:: 16.0.0 + +**Positional arguments:** + +``<aggregate>`` + Name or ID of aggregate. + +``<image>`` + Name or ID of image(s) to cache. + .. _nova_aggregate-create: nova aggregate-create @@ -2516,6 +2543,8 @@ nova migration-list [--limit <limit>] [--changes-since <changes_since>] [--changes-before <changes_before>] + [--project-id <project_id>] + [--user-id <user_id>] Print a list of migrations. @@ -2573,6 +2602,14 @@ To see the list of evacuation operations *from* a compute service host: point of time. The provided time should be an ISO 8061 formatted time. e.g. 2016-03-04T06:27:59Z . (Supported by API versions '2.66' - '2.latest') +``--project-id <project_id>`` + Filter the migrations by the given project ID. + (Supported by API versions '2.80' - '2.latest') + +``--user-id <user_id>`` + Filter the migrations by the given user ID. + (Supported by API versions '2.80' - '2.latest') + .. _nova_pause: nova pause @@ -3325,7 +3362,7 @@ Retrieve server NUMA topology information. Host specific fields are only visible to users with the administrative role. (Supported by API versions '2.78' - '2.latest') -.. versionadded:: 16.0.0 +.. versionadded:: 15.1.0 **Positional arguments:** diff --git a/novaclient/__init__.py b/novaclient/__init__.py index a4f9b4ee..6855e2f2 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.79") +API_MAX_VERSION = api_versions.APIVersion("2.81") diff --git a/novaclient/tests/functional/v2/test_migrations.py b/novaclient/tests/functional/v2/test_migrations.py index 855d8ce5..34a03695 100644 --- a/novaclient/tests/functional/v2/test_migrations.py +++ b/novaclient/tests/functional/v2/test_migrations.py @@ -41,6 +41,8 @@ class TestMigrationList(base.ClientTestBase): # Find the source compute by getting OS-EXT-SRV-ATTR:host from the # nova show output. server = self.nova('show', params='%s' % server_id) + server_user_id = self._get_value_from_the_table(server, 'user_id') + tenant_id = self._get_value_from_the_table(server, 'tenant_id') source_compute = self._get_value_from_the_table( server, 'OS-EXT-SRV-ATTR:host') # now resize up @@ -97,3 +99,14 @@ class TestMigrationList(base.ClientTestBase): migrations = self._filter_migrations( '2.66', 'resize', uuidutils.generate_uuid()) self.assertNotIn(server_id, migrations) + + # Listing migrations with v2.80 and make sure there are the User ID + # and Project ID values in the output. + migrations = self.nova('migration-list', + flags='--os-compute-api-version 2.80') + user_id = self._get_column_value_from_single_row_table( + migrations, 'User ID') + self.assertEqual(server_user_id, user_id) + project_id = self._get_column_value_from_single_row_table( + migrations, 'Project ID') + self.assertEqual(tenant_id, project_id) diff --git a/novaclient/tests/unit/fixture_data/aggregates.py b/novaclient/tests/unit/fixture_data/aggregates.py index 3ea64b8b..b3ab88c5 100644 --- a/novaclient/tests/unit/fixture_data/aggregates.py +++ b/novaclient/tests/unit/fixture_data/aggregates.py @@ -51,3 +51,10 @@ class Fixture(base.Fixture): self.requests_mock.delete(self.url(1), status_code=202, headers=self.json_headers) + + self.requests_mock.register_uri('POST', self.url(1), + json={}, + headers=self.json_headers) + self.requests_mock.post(self.url(1, 'images'), + json={}, + headers=self.json_headers) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 3430679b..62d5e727 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1141,7 +1141,7 @@ class FakeSessionClient(base_client.SessionClient): # Images # def get_images(self, **kw): - return (200, {}, {'images': [ + images = [ { "id": FAKE_IMAGE_UUID_SNAPSHOT, "name": "My Server Backup", @@ -1191,7 +1191,16 @@ class FakeSessionClient(base_client.SessionClient): "progress": 80, "links": {}, }, - ]}) + ] + + if 'id' in kw: + requested = kw['id'].replace('in:', '').split(',') + images = [i for i in images if i['id'] in requested] + if 'names' in kw: + requested = kw['names'].replace('in:', '').split(',') + images = [i for i in images if i['name'] in requested] + + return (200, {}, {'images': images}) def get_images_555cae93_fb41_4145_9c52_f5b923538a26(self, **kw): return (200, {}, {'image': self.get_images()[2]['images'][0]}) @@ -1725,6 +1734,9 @@ class FakeSessionClient(base_client.SessionClient): def delete_os_aggregates_1(self, **kw): return (202, {}, None) + def post_os_aggregates_1_images(self, body, **kw): + return (202, {}, None) + # # Services # @@ -2288,6 +2300,14 @@ class FakeSessionClient(base_client.SessionClient): migration1.update({"uuid": "11111111-07d5-11e1-90e3-e3dffe0c5983"}) migration2.update({"uuid": "22222222-07d5-11e1-90e3-e3dffe0c5983"}) + if self.api_version >= api_versions.APIVersion("2.80"): + migration1.update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + migration2.update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + migration_list = [] instance_uuid = kw.get('instance_uuid', None) if instance_uuid == migration1['instance_uuid']: @@ -2379,6 +2399,12 @@ class FakeSessionClient(base_client.SessionClient): "disk_remaining_bytes": 230000, "updated_at": "2016-01-29T13:42:02.000000" }} + + if self.api_version >= api_versions.APIVersion("2.80"): + migration['migration'].update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + return (200, FAKE_RESPONSE_HEADERS, migration) @api_versions.wraps(start_version="2.23") @@ -2402,6 +2428,12 @@ class FakeSessionClient(base_client.SessionClient): "disk_remaining_bytes": 230000, "updated_at": "2016-01-29T13:42:02.000000" }]} + + if self.api_version >= api_versions.APIVersion("2.80"): + migrations['migrations'][0].update({ + "project_id": "b59c18e5fa284fd384987c5cb25a1853", + "user_id": "13cc0930d27c4be0acc14d7c47a3e1f7"}) + return (200, FAKE_RESPONSE_HEADERS, migrations) def delete_servers_1234_migrations_1(self): diff --git a/novaclient/tests/unit/v2/test_aggregates.py b/novaclient/tests/unit/v2/test_aggregates.py index 1de12823..4f3eecdf 100644 --- a/novaclient/tests/unit/v2/test_aggregates.py +++ b/novaclient/tests/unit/v2/test_aggregates.py @@ -13,11 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions +from novaclient import exceptions from novaclient.tests.unit.fixture_data import aggregates as data from novaclient.tests.unit.fixture_data import client from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes from novaclient.v2 import aggregates +from novaclient.v2 import images class AggregatesTest(utils.FixturedTestCase): @@ -161,3 +164,40 @@ class AggregatesTest(utils.FixturedTestCase): result3 = self.cs.aggregates.delete(aggregate) self.assert_request_id(result3, fakes.FAKE_REQUEST_ID_LIST) self.assert_called('DELETE', '/os-aggregates/1') + + +class AggregatesV281Test(utils.FixturedTestCase): + api_version = "2.81" + data_fixture_class = data.Fixture + + scenarios = [('original', {'client_fixture_class': client.V1}), + ('session', {'client_fixture_class': client.SessionV1})] + + def setUp(self): + super(AggregatesV281Test, self).setUp() + self.cs.api_version = api_versions.APIVersion(self.api_version) + + def test_cache_images(self): + aggregate = self.cs.aggregates.list()[0] + _images = [images.Image(self.cs.aggregates, {'id': '1'}), + images.Image(self.cs.aggregates, {'id': '2'})] + aggregate.cache_images(_images) + expected_body = {'cache': [{'id': image.id} + for image in _images]} + self.assert_called('POST', '/os-aggregates/1/images', + expected_body) + + def test_cache_images_just_ids(self): + aggregate = self.cs.aggregates.list()[0] + _images = ['1'] + aggregate.cache_images(_images) + expected_body = {'cache': [{'id': '1'}]} + self.assert_called('POST', '/os-aggregates/1/images', + expected_body) + + def test_cache_images_pre281(self): + self.cs.api_version = api_versions.APIVersion('2.80') + aggregate = self.cs.aggregates.list()[0] + _images = [images.Image(self.cs.aggregates, {'id': '1'})] + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + aggregate.cache_images, _images) diff --git a/novaclient/tests/unit/v2/test_migrations.py b/novaclient/tests/unit/v2/test_migrations.py index fcafa6d2..09d09405 100644 --- a/novaclient/tests/unit/v2/test_migrations.py +++ b/novaclient/tests/unit/v2/test_migrations.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import six + from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes @@ -112,3 +114,58 @@ class MigrationsV266Test(MigrationsV259Test): '2012-02-29T06%3A23%3A22') for m in ms: self.assertIsInstance(m, migrations.Migration) + + +class MigrationsV280Test(MigrationsV266Test): + def setUp(self): + super(MigrationsV280Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.80") + + def test_list_migrations_with_user_id(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + params = {'user_id': user_id} + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-migrations?user_id=%s' % user_id) + for m in ms: + self.assertIsInstance(m, migrations.Migration) + + def test_list_migrations_with_project_id(self): + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + params = {'project_id': project_id} + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', '/os-migrations?project_id=%s' + % project_id) + for m in ms: + self.assertIsInstance(m, migrations.Migration) + + def test_list_migrations_with_user_and_project_id(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + params = {'user_id': user_id, 'project_id': project_id} + ms = self.cs.migrations.list(**params) + self.assert_request_id(ms, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called('GET', + '/os-migrations?project_id=%s&user_id=%s' + % (project_id, user_id)) + for m in ms: + self.assertIsInstance(m, migrations.Migration) + + def test_list_migrations_with_user_id_pre_v280(self): + self.cs.api_version = api_versions.APIVersion('2.79') + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + ex = self.assertRaises(TypeError, + self.cs.migrations.list, + user_id=user_id) + self.assertIn("unexpected keyword argument 'user_id'", + six.text_type(ex)) + + def test_list_migrations_with_project_id_pre_v280(self): + self.cs.api_version = api_versions.APIVersion('2.79') + project_id = '23cc0930d27c4be0acc14d7c47a3e1f7' + ex = self.assertRaises(TypeError, + self.cs.migrations.list, + project_id=project_id) + self.assertIn("unexpected keyword argument 'project_id'", + six.text_type(ex)) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 65240a40..dbd2b3be 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2884,6 +2884,30 @@ class ShellTest(utils.TestCase): self.run_command('aggregate-show test') self.assert_called('GET', '/os-aggregates') + def test_aggregate_cache_images(self): + self.run_command( + 'aggregate-cache-images 1 %s %s' % ( + FAKE_UUID_1, FAKE_UUID_2), + api_version='2.81') + body = { + 'cache': [{'id': FAKE_UUID_1}, + {'id': FAKE_UUID_2}], + } + self.assert_called('POST', '/os-aggregates/1/images', body) + + def test_aggregate_cache_images_no_images(self): + self.assertRaises(SystemExit, + self.run_command, + 'aggregate-cache-images 1', + api_version='2.81') + + def test_aggregate_cache_images_pre281(self): + self.assertRaises(SystemExit, + self.run_command, + 'aggregate-cache-images 1 %s %s' % ( + FAKE_UUID_1, FAKE_UUID_2), + api_version='2.80') + def test_live_migration(self): self.run_command('live-migration sample-server hostname') self.assert_called('POST', '/servers/1234/action', @@ -2955,11 +2979,39 @@ class ShellTest(utils.TestCase): api_version='2.23') self.assert_called('GET', '/servers/1234/migrations') + def test_list_migrations_pre_v280(self): + out = self.run_command('server-migration-list sample-server', + api_version='2.79')[0] + self.assert_called('GET', '/servers/1234/migrations') + self.assertNotIn('User ID', out) + self.assertNotIn('Project ID', out) + + def test_list_migrations_v280(self): + out = self.run_command('server-migration-list sample-server', + api_version='2.80')[0] + self.assert_called('GET', '/servers/1234/migrations') + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + def test_get_migration(self): self.run_command('server-migration-show sample-server 1', api_version='2.23') self.assert_called('GET', '/servers/1234/migrations/1') + def test_get_migration_pre_v280(self): + out = self.run_command('server-migration-show sample-server 1', + api_version='2.79')[0] + self.assert_called('GET', '/servers/1234/migrations/1') + self.assertNotIn('user_id', out) + self.assertNotIn('project_id', out) + + def test_get_migration_v280(self): + out = self.run_command('server-migration-show sample-server 1', + api_version='2.80')[0] + self.assert_called('GET', '/servers/1234/migrations/1') + self.assertIn('user_id', out) + self.assertIn('project_id', out) + def test_live_migration_abort(self): self.run_command('live-migration-abort sample-server 1', api_version='2.24') @@ -4063,6 +4115,52 @@ class ShellTest(utils.TestCase): self.assertRaises(SystemExit, self.run_command, cmd, api_version='2.65') + def test_migration_list_with_user_id_v280(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + out = self.run_command('migration-list --user-id %s' % user_id, + api_version='2.80')[0] + self.assert_called('GET', '/os-migrations?user_id=%s' % user_id) + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + + def test_migration_list_with_project_id_v280(self): + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + out = self.run_command('migration-list --project-id %s' % project_id, + api_version='2.80')[0] + self.assert_called('GET', '/os-migrations?project_id=%s' % project_id) + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + + def test_migration_list_with_user_and_project_id_v280(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + out = self.run_command('migration-list --project-id %(project_id)s ' + '--user-id %(user_id)s' % + {'user_id': user_id, 'project_id': project_id}, + api_version='2.80')[0] + self.assert_called('GET', '/os-migrations?project_id=%s&user_id=%s' + % (project_id, user_id)) + self.assertIn('User ID', out) + self.assertIn('Project ID', out) + + def test_migration_list_with_user_id_pre_v280_not_allowed(self): + user_id = '13cc0930d27c4be0acc14d7c47a3e1f7' + cmd = 'migration-list --user-id %s' % user_id + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.79') + + def test_migration_list_with_project_id_pre_v280_not_allowed(self): + project_id = 'b59c18e5fa284fd384987c5cb25a1853' + cmd = 'migration-list --project-id %s' % project_id + self.assertRaises(SystemExit, self.run_command, cmd, + api_version='2.79') + + def test_migration_list_pre_v280(self): + out = self.run_command('migration-list', api_version='2.79')[0] + self.assert_called('GET', '/os-migrations') + self.assertNotIn('User ID', out) + self.assertNotIn('Project ID', out) + @mock.patch('novaclient.v2.shell._find_server') @mock.patch('os.system') def test_ssh(self, mock_system, mock_find_server): @@ -4605,3 +4703,27 @@ class PollForStatusTestCase(utils.TestCase): action=action, show_progress=True, silent=False) + + +class TestUtilMethods(utils.TestCase): + def setUp(self): + super(TestUtilMethods, self).setUp() + self.shell = self.useFixture(ShellFixture()).shell + # NOTE(danms): Get a client that we can use to call things outside of + # the shell main + self.shell.cs = fakes.FakeClient('2.1') + + def test_find_images(self): + """Test find_images() with a name and id.""" + images = novaclient.v2.shell._find_images(self.shell.cs, + [FAKE_UUID_1, + 'back1']) + self.assertEqual(2, len(images)) + self.assertEqual(FAKE_UUID_1, images[0].id) + self.assertEqual(fakes.FAKE_IMAGE_UUID_BACKUP, images[1].id) + + def test_find_images_missing(self): + """Test find_images() where one of the images is not found.""" + self.assertRaises(exceptions.CommandError, + novaclient.v2.shell._find_images, + self.shell.cs, [FAKE_UUID_1, 'foo']) diff --git a/novaclient/v2/aggregates.py b/novaclient/v2/aggregates.py index 9d4dff82..d2cbaa85 100644 --- a/novaclient/v2/aggregates.py +++ b/novaclient/v2/aggregates.py @@ -15,6 +15,7 @@ """Aggregate interface.""" +from novaclient import api_versions from novaclient import base @@ -45,6 +46,10 @@ class Aggregate(base.Resource): """ return self.manager.delete(self) + @api_versions.wraps("2.81") + def cache_images(self, images): + return self.manager.cache_images(self, images) + class AggregateManager(base.ManagerWithFind): resource_class = Aggregate @@ -103,3 +108,20 @@ class AggregateManager(base.ManagerWithFind): :returns: An instance of novaclient.base.TupleWithMeta """ return self._delete('/os-aggregates/%s' % (base.getid(aggregate))) + + @api_versions.wraps("2.81") + def cache_images(self, aggregate, images): + """ + Request images be cached on a given aggregate. + + :param aggregate: The aggregate to target + :param images: A list of image IDs to request caching + :returns: An instance of novaclient.base.TupleWithMeta + """ + body = { + 'cache': [{'id': base.getid(image)} for image in images], + } + resp, body = self.api.client.post( + "/os-aggregates/%s/images" % base.getid(aggregate), + body=body) + return self.convert_into_with_meta(body, resp) diff --git a/novaclient/v2/images.py b/novaclient/v2/images.py index e83d9b2f..0aced5c7 100644 --- a/novaclient/v2/images.py +++ b/novaclient/v2/images.py @@ -66,6 +66,43 @@ class GlanceManager(base.Manager): matches[0].append_request_ids(matches.request_ids) return matches[0] + def find_images(self, names_or_ids): + """Find multiple images by name or id (user provided input). + + :param names_or_ids: A list of strings to use to find images. + :returns: novaclient.v2.images.Image objects for each images found + :raises exceptions.NotFound: If one or more images is not found + :raises exceptions.ClientException: If the image service returns any + unexpected images. + + NOTE: This method always makes two calls to the image service, even if + only one image is provided by ID and is returned in the first query. + """ + with self.alternate_service_type( + 'image', allowed_types=('image',)): + matches = self._list('/v2/images?id=in:%s' % ','.join( + names_or_ids), 'images') + matches.extend(self._list('/v2/images?names=in:%s' % ','.join( + names_or_ids), 'images')) + missed = (set(names_or_ids) - + set(m.name for m in matches) - + set(m.id for m in matches)) + if missed: + msg = _("Unable to find image(s): %(images)s") % { + "images": ",".join(missed)} + raise exceptions.NotFound(404, msg) + for match in matches: + match.append_request_ids(matches.request_ids) + + additional = [] + for i in matches: + if i.name not in names_or_ids and i.id not in names_or_ids: + additional.append(i) + if additional: + msg = _('Additional images found in response') + raise exceptions.ClientException(500, msg) + return matches + def list(self): """ Get a detailed list of all images. diff --git a/novaclient/v2/migrations.py b/novaclient/v2/migrations.py index 3aecd1e7..1fe764b5 100644 --- a/novaclient/v2/migrations.py +++ b/novaclient/v2/migrations.py @@ -29,7 +29,7 @@ class MigrationManager(base.ManagerWithFind): def _list_base(self, host=None, status=None, instance_uuid=None, marker=None, limit=None, changes_since=None, changes_before=None, migration_type=None, - source_compute=None): + source_compute=None, user_id=None, project_id=None): opts = {} if host: opts['host'] = host @@ -49,6 +49,10 @@ class MigrationManager(base.ManagerWithFind): opts['migration_type'] = migration_type if source_compute: opts['source_compute'] = source_compute + if user_id: + opts['user_id'] = user_id + if project_id: + opts['project_id'] = project_id return self._list("/os-migrations", "migrations", filters=opts) @@ -99,7 +103,7 @@ class MigrationManager(base.ManagerWithFind): migration_type=migration_type, source_compute=source_compute) - @api_versions.wraps("2.66") + @api_versions.wraps("2.66", "2.79") def list(self, host=None, status=None, instance_uuid=None, marker=None, limit=None, changes_since=None, changes_before=None, migration_type=None, source_compute=None): @@ -132,3 +136,42 @@ class MigrationManager(base.ManagerWithFind): changes_before=changes_before, migration_type=migration_type, source_compute=source_compute) + + @api_versions.wraps("2.80") + def list(self, host=None, status=None, instance_uuid=None, + marker=None, limit=None, changes_since=None, + changes_before=None, migration_type=None, + source_compute=None, user_id=None, project_id=None): + """ + Get a list of migrations. + :param host: filter migrations by host name (optional). + :param status: filter migrations by status (optional). + :param instance_uuid: filter migrations by instance uuid (optional). + :param marker: Begin returning migrations that appear later in the + migrations list than that represented by this migration UUID + (optional). + :param limit: maximum number of migrations to return (optional). + Note the API server has a configurable default limit. If no limit is + specified here or limit is larger than default, the default limit will + be used. + :param changes_since: Only return migrations changed later or equal + to a certain point of time. The provided time should be an ISO 8061 + formatted time. e.g. 2016-03-04T06:27:59Z . (optional). + :param changes_before: Only return migrations changed earlier or + equal to a certain point of time. The provided time should be an ISO + 8061 formatted time. e.g. 2016-03-05T06:27:59Z . (optional). + :param migration_type: Filter migrations by type. Valid values are: + evacuation, live-migration, migration, resize + :param source_compute: Filter migrations by source compute host name. + :param user_id: filter migrations by user (optional). + :param project_id: filter migrations by project (optional). + """ + return self._list_base(host=host, status=status, + instance_uuid=instance_uuid, + marker=marker, limit=limit, + changes_since=changes_since, + changes_before=changes_before, + migration_type=migration_type, + source_compute=source_compute, + user_id=user_id, + project_id=project_id) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index b656baba..0f8b5ce6 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -2570,6 +2570,14 @@ def _find_image(cs, image): raise exceptions.CommandError(six.text_type(e)) +def _find_images(cs, images): + """Get images by name or ID.""" + try: + return cs.glance.find_images(images) + except (exceptions.NotFound, exceptions.NoUniqueMatch) as e: + raise exceptions.CommandError(six.text_type(e)) + + def _find_flavor(cs, flavor): """Get a flavor by name, ID, or RAM size.""" try: @@ -3518,6 +3526,21 @@ def _print_aggregate_details(cs, aggregate): utils.print_list([aggregate], columns, formatters=formatters) +@api_versions.wraps("2.81") +@utils.arg( + 'aggregate', metavar='<aggregate>', + help=_('Name or ID of the aggregate.')) +@utils.arg( + 'images', metavar='<image>', nargs='+', + help=_('Name or ID of image(s) to cache on the hosts within ' + 'the aggregate.')) +def do_aggregate_cache_images(cs, args): + """Request images be cached.""" + aggregate = _find_aggregate(cs, args.aggregate) + images = _find_images(cs, args.images) + cs.aggregates.cache_images(aggregate, images) + + @utils.arg('server', metavar='<server>', help=_('Name or ID of server.')) @utils.arg( 'host', metavar='<host>', default=None, nargs='?', @@ -3597,6 +3620,10 @@ def do_server_migration_list(cs, args): "memory_remaining_bytes", "disk_total_bytes", "disk_processed_bytes", "disk_remaining_bytes"] + if cs.api_version >= api_versions.APIVersion("2.80"): + fields.append("Project ID") + fields.append("User ID") + formatters = map(lambda field: utils.make_field_formatter(field)[1], format_key) formatters = dict(zip(format_name, formatters)) @@ -5391,6 +5418,10 @@ def _print_migrations(cs, migrations): fields.append("Type") formatters.update({"Type": migration_type}) + if cs.api_version >= api_versions.APIVersion("2.80"): + fields.append("Project ID") + fields.append("User ID") + utils.print_list(migrations, fields, formatters) @@ -5564,6 +5595,20 @@ def do_migration_list(cs, args): 'of time. The provided time should be an ISO 8061 formatted time. ' 'e.g. 2016-03-04T06:27:59Z .'), start_version="2.66") +@utils.arg( + '--project-id', + dest='project_id', + metavar='<project_id>', + default=None, + help=_('Filter the migrations by the given project ID.'), + start_version='2.80') +@utils.arg( + '--user-id', + dest='user_id', + metavar='<user_id>', + default=None, + help=_('Filter the migrations by the given user ID.'), + start_version='2.80') def do_migration_list(cs, args): """Print a list of migrations.""" if args.changes_since: @@ -5580,13 +5625,20 @@ def do_migration_list(cs, args): raise exceptions.CommandError(_('Invalid changes-before value: %s') % args.changes_before) - migrations = cs.migrations.list(args.host, args.status, - instance_uuid=args.instance_uuid, - marker=args.marker, limit=args.limit, - changes_since=args.changes_since, - changes_before=args.changes_before, - migration_type=args.migration_type, - source_compute=args.source_compute) + kwargs = dict( + instance_uuid=args.instance_uuid, + marker=args.marker, + limit=args.limit, + changes_since=args.changes_since, + changes_before=args.changes_before, + migration_type=args.migration_type, + source_compute=args.source_compute) + + if cs.api_version >= api_versions.APIVersion('2.80'): + kwargs['project_id'] = args.project_id + kwargs['user_id'] = args.user_id + + migrations = cs.migrations.list(args.host, args.status, **kwargs) _print_migrations(cs, migrations) diff --git a/playbooks/legacy/novaclient-dsvm-functional/run.yaml b/playbooks/legacy/novaclient-dsvm-functional/run.yaml index a3d1f497..183d3989 100644 --- a/playbooks/legacy/novaclient-dsvm-functional/run.yaml +++ b/playbooks/legacy/novaclient-dsvm-functional/run.yaml @@ -27,6 +27,7 @@ cmd: | set -e set -x + export DEVSTACK_GATE_USE_PYTHON3=true export PYTHONUNBUFFERED=true export BRANCH_OVERRIDE=default export DEVSTACK_PROJECT_FROM_GIT=python-novaclient diff --git a/releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml b/releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml new file mode 100644 index 00000000..9fd7dee1 --- /dev/null +++ b/releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + Python 2 is no longer supported. Python 3 is required. diff --git a/releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml b/releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml new file mode 100644 index 00000000..cace47d1 --- /dev/null +++ b/releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml @@ -0,0 +1,20 @@ +--- +features: + - | + Added support for `microversion 2.80`_ which adds ``user_id`` + and ``project_id`` filter parameters to the ``GET /os-migrations`` API. + + New kwargs ``project_id`` and ``user_id`` have been added to + the following python API binding: + + - novaclient.v2.migrations.MigrationManager.list + + The following CLI changes have been made: + + - The ``--project-id`` and ``--user-id`` options are added to the + ``nova migration-list`` CLI. + - The ``nova server-migration-list`` and ``nova server-migration-show`` + commands will show the ``Project ID`` and ``User ID`` values when + using microversion 2.80 or greater. + + .. _microversion 2.80: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id72 diff --git a/releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml b/releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml new file mode 100644 index 00000000..a51336d6 --- /dev/null +++ b/releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added support for `microversion 2.81`_ which adds image pre-caching support by + aggregate. + + - The ``aggregate-cache-images`` command is added to the CLI + - The ``cache_images()`` method is added to the python API binding + + .. _microversion 2.81: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id73 @@ -16,8 +16,6 @@ classifier = License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 @@ -1,9 +1,10 @@ [tox] -envlist = py27,py37,pep8,docs +envlist = py37,pep8,docs minversion = 2.0 skipsdist = True [testenv] +basepython = python3 usedevelop = True # tox is silly... these need to be separated by a newline.... whitelist_externals = @@ -22,15 +23,12 @@ commands = 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://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt @@ -39,7 +37,6 @@ deps = commands = {posargs} [testenv:docs] -basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt @@ -51,7 +48,6 @@ commands = whereto doc/build/html/.htaccess doc/test/redirect-tests.txt [testenv:pdf-docs] -basepython = python3 envdir = {toxworkdir}/docs deps = {[testenv:docs]deps} commands = @@ -60,7 +56,6 @@ commands = make -C doc/build/pdf [testenv:releasenotes] -basepython = python3 deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt @@ -69,21 +64,12 @@ commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:functional] -basepython = python2.7 -passenv = OS_NOVACLIENT_TEST_NETWORK -commands = - stestr --test-path=./novaclient/tests/functional run --concurrency=1 {posargs} - python novaclient/tests/functional/hooks/check_resources.py - -[testenv:functional-py36] -basepython = python3.6 passenv = OS_NOVACLIENT_TEST_NETWORK commands = 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 = @@ -110,7 +96,6 @@ 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 @@ -119,7 +104,6 @@ deps = bindep commands = bindep test [testenv:lower-constraints] -basepython = python3 deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt |