summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.zuul.yaml5
-rw-r--r--doc/source/cli/nova.rst39
-rw-r--r--novaclient/__init__.py2
-rw-r--r--novaclient/tests/functional/v2/test_migrations.py13
-rw-r--r--novaclient/tests/unit/fixture_data/aggregates.py7
-rw-r--r--novaclient/tests/unit/v2/fakes.py36
-rw-r--r--novaclient/tests/unit/v2/test_aggregates.py40
-rw-r--r--novaclient/tests/unit/v2/test_migrations.py57
-rw-r--r--novaclient/tests/unit/v2/test_shell.py122
-rw-r--r--novaclient/v2/aggregates.py22
-rw-r--r--novaclient/v2/images.py37
-rw-r--r--novaclient/v2/migrations.py47
-rw-r--r--novaclient/v2/shell.py66
-rw-r--r--playbooks/legacy/novaclient-dsvm-functional/run.yaml1
-rw-r--r--releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml4
-rw-r--r--releasenotes/notes/microversion-v2_80-c2394316f9212865.yaml20
-rw-r--r--releasenotes/notes/microversion-v2_81-3ddd8e2fc7e45030.yaml10
-rw-r--r--setup.cfg2
-rw-r--r--tox.ini20
19 files changed, 514 insertions, 36 deletions
diff --git a/.zuul.yaml b/.zuul.yaml
index 0880f056..95eabd35 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -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
diff --git a/setup.cfg b/setup.cfg
index 42b82ac2..d2048632 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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
diff --git a/tox.ini b/tox.ini
index ce99fb0b..2b9cb62d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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