From 6d1321f5ddad18629d9b02cc77c54d76c465da08 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 15 Oct 2021 11:58:04 +0100 Subject: [PATCH] compute: Add support for server migrations API There are two migrations APIs in nova: the migrations API ('/os-migrations') and the *server* migrations API ('/servers/{id}/migrations'). The former is responsible for listing all migrations in a deployment, while the latter only lists those for a given server. In this change, we're adding support for the latter. Change-Id: Ideeca99a89c920a09cfc3799bbcc7e24046a5c43 Signed-off-by: Stephen Finucane --- doc/source/user/proxies/compute.rst | 9 ++ doc/source/user/resources/compute/index.rst | 1 + .../resources/compute/v2/server_migration.rst | 13 ++ openstack/compute/v2/_proxy.py | 113 ++++++++++++++++++ openstack/compute/v2/server_migration.py | 98 +++++++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 48 ++++++++ .../unit/compute/v2/test_server_migration.py | 112 +++++++++++++++++ ...dd-server-migrations-6e31183196f14deb.yaml | 6 + 8 files changed, 400 insertions(+) create mode 100644 doc/source/user/resources/compute/v2/server_migration.rst create mode 100644 openstack/compute/v2/server_migration.py create mode 100644 openstack/tests/unit/compute/v2/test_server_migration.py create mode 100644 releasenotes/notes/add-server-migrations-6e31183196f14deb.yaml diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index 8c9551212..fb9379802 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -152,7 +152,16 @@ Extension Operations QuotaSet Operations ^^^^^^^^^^^^^^^^^^^ + .. autoclass:: openstack.compute.v2._proxy.Proxy :noindex: :members: get_quota_set, get_quota_set_defaults, revert_quota_set, update_quota_set + +Server Migration Operations +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.compute.v2._proxy.Proxy + :noindex: + :members: abort_server_migration, force_complete_server_migration, + get_server_migration, server_migrations diff --git a/doc/source/user/resources/compute/index.rst b/doc/source/user/resources/compute/index.rst index 0db8a314b..aee022fdc 100644 --- a/doc/source/user/resources/compute/index.rst +++ b/doc/source/user/resources/compute/index.rst @@ -11,6 +11,7 @@ Compute Resources v2/limits v2/server v2/server_interface + v2/server_migration v2/server_ip v2/hypervisor v2/quota_set diff --git a/doc/source/user/resources/compute/v2/server_migration.rst b/doc/source/user/resources/compute/v2/server_migration.rst new file mode 100644 index 000000000..6587f596f --- /dev/null +++ b/doc/source/user/resources/compute/v2/server_migration.rst @@ -0,0 +1,13 @@ +openstack.compute.v2.server_migration +===================================== + +.. automodule:: openstack.compute.v2.server_migration + +The ServerMigration Class +------------------------- + +The ``ServerMigration`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.compute.v2.server_migration.ServerMigration + :members: diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index 1a9e87c24..c0df32926 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -26,6 +26,7 @@ from openstack.compute.v2 import server_diagnostics as _server_diagnostics from openstack.compute.v2 import server_group as _server_group from openstack.compute.v2 import server_interface as _server_interface from openstack.compute.v2 import server_ip +from openstack.compute.v2 import server_migration as _server_migration from openstack.compute.v2 import server_remote_console as _src from openstack.compute.v2 import service as _service from openstack.compute.v2 import volume_attachment as _volume_attachment @@ -1717,6 +1718,118 @@ class Proxy(proxy.Proxy): block_migration=block_migration, ) + def abort_server_migration( + self, server_migration, server, ignore_missing=True, + ): + """Abort an in-progress server migration + + :param server_migration: The value can be either the ID of a server + migration or a + :class:`~openstack.compute.v2.server_migration.ServerMigration` + instance. + :param server: This parameter needs to be specified when + ServerMigration ID is given as value. It can be either the ID of a + server or a :class:`~openstack.compute.v2.server.Server` instance + that the migration belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the volume attachment does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + volume attachment. + + :returns: ``None`` + """ + server_id = self._get_uri_attribute( + server_migration, server, 'server_id', + ) + server_migration = resource.Resource._get_id(server_migration) + + self._delete( + _server_migration.ServerMigration, + server_migration, + server_id=server_id, + ignore_missing=ignore_missing, + ) + + def force_complete_server_migration(self, server_migration, server=None): + """Force complete an in-progress server migration + + :param server_migration: The value can be either the ID of a server + migration or a + :class:`~openstack.compute.v2.server_migration.ServerMigration` + instance. + :param server: This parameter needs to be specified when + ServerMigration ID is given as value. It can be either the ID of a + server or a :class:`~openstack.compute.v2.server.Server` instance + that the migration belongs to. + + :returns: ``None`` + """ + server_id = self._get_uri_attribute( + server_migration, server, 'server_id', + ) + server_migration = self._get_resource( + _server_migration.ServerMigration, + server_migration, + server_id=server_id, + ) + server_migration.force_complete(self) + + def get_server_migration( + self, + server_migration, + server, + ignore_missing=True, + ): + """Get a single volume attachment + + :param server_migration: The value can be the ID of a server migration + or a + :class:`~openstack.compute.v2.server_migration.ServerMigration` + instance. + :param server: This parameter need to be specified when ServerMigration + ID is given as value. It can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance that the + migration belongs to. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the server migration does not exist. When set to ``True``, no + exception will be set when attempting to delete a nonexistent + server migration. + + :returns: One + :class:`~openstack.compute.v2.server_migration.ServerMigration` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + when no resource can be found. + """ + server_id = self._get_uri_attribute( + server_migration, server, 'server_id', + ) + server_migration = resource.Resource._get_id(server_migration) + + return self._get( + _server_migration.ServerMigration, + server_migration, + server_id=server_id, + ignore_missing=ignore_missing, + ) + + def server_migrations(self, server): + """Return a generator of migrations for a server. + + :param server: The server can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server`. + + :returns: A generator of ServerMigration objects + :rtype: + :class:`~openstack.compute.v2.server_migration.ServerMigration` + """ + server_id = resource.Resource._get_id(server) + return self._list( + _server_migration.ServerMigration, + server_id=server_id, + ) + # ========== Server diagnostics ========== def get_server_diagnostics(self, server): diff --git a/openstack/compute/v2/server_migration.py b/openstack/compute/v2/server_migration.py new file mode 100644 index 000000000..f10f9e163 --- /dev/null +++ b/openstack/compute/v2/server_migration.py @@ -0,0 +1,98 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack import exceptions +from openstack import resource +from openstack import utils + + +class ServerMigration(resource.Resource): + resource_key = 'migration' + resources_key = 'migrations' + base_path = '/servers/%(server_uuid)s/migrations' + + # capabilities + allow_fetch = True + allow_list = True + allow_delete = True + + #: The ID for the server. + server_id = resource.URI('server_uuid') + + #: The date and time when the resource was created. + created_at = resource.Body('created_at') + #: The target host of the migration. + dest_host = resource.Body('dest_host') + #: The target compute of the migration. + dest_compute = resource.Body('dest_compute') + #: The target node of the migration. + dest_node = resource.Body('dest_node') + #: The amount of disk, in bytes, that has been processed during the + #: migration. + disk_processed_bytes = resource.Body('disk_processed_bytes') + #: The amount of disk, in bytes, that still needs to be migrated. + disk_remaining_bytes = resource.Body('disk_remaining_bytes') + #: The total amount of disk, in bytes, that needs to be migrated. + disk_total_bytes = resource.Body('disk_total_bytes') + #: The amount of memory, in bytes, that has been processed during the + #: migration. + memory_processed_bytes = resource.Body('memory_processed_bytes') + #: The amount of memory, in bytes, that still needs to be migrated. + memory_remaining_bytes = resource.Body('memory_remaining_bytes') + #: The total amount of memory, in bytes, that needs to be migrated. + memory_total_bytes = resource.Body('memory_total_bytes') + #: The ID of the project that initiated the server migration (since + #: microversion 2.80) + project_id = resource.Body('project_id') + # FIXME(stephenfin): This conflicts since there is a server ID in the URI + # *and* in the body. We need a field that handles both or we need to use + # different names. + # #: The UUID of the server + # server_id = resource.Body('server_uuid') + #: The source compute of the migration. + source_compute = resource.Body('source_compute') + #: The source node of the migration. + source_node = resource.Body('source_node') + #: The current status of the migration. + status = resource.Body('status') + #: The date and time when the resource was last updated. + updated_at = resource.Body('updated_at') + #: The ID of the user that initiated the server migration (since + #: microversion 2.80) + user_id = resource.Body('user_id') + #: The UUID of the migration (since microversion 2.59) + uuid = resource.Body('uuid', alternate_id=True) + + _max_microversion = '2.80' + + @classmethod + def _get_microversion_for_action(cls, session): + return cls._get_microversion_for_list(session) + + def _action(self, session, body): + """Preform server migration actions given the message body.""" + session = self._get_session(session) + microversion = self._get_microversion_for_list(session) + + url = utils.urljoin( + self.base_path % {'server_uuid': self.server_id}, + self.id, + 'action', + ) + response = session.post(url, microversion=microversion, json=body) + exceptions.raise_from_response(response) + return response + + def force_complete(self, session): + """Force on-going live migration to complete.""" + body = {'force_complete': None} + self._action(session, body) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 313535760..5ecfa9cd6 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -26,6 +26,7 @@ from openstack.compute.v2 import server from openstack.compute.v2 import server_group from openstack.compute.v2 import server_interface from openstack.compute.v2 import server_ip +from openstack.compute.v2 import server_migration from openstack.compute.v2 import server_remote_console from openstack.compute.v2 import service from openstack import resource @@ -1004,6 +1005,53 @@ class TestCompute(TestComputeProxy): expected_args=[self.proxy, "host1"], expected_kwargs={'force': False, 'block_migration': None}) + def test_abort_server_migration(self): + self._verify( + 'openstack.proxy.Proxy._delete', + self.proxy.abort_server_migration, + method_args=['server_migration', 'server'], + expected_args=[ + server_migration.ServerMigration, + 'server_migration', + ], + expected_kwargs={ + 'server_id': 'server', + 'ignore_missing': True, + }, + ) + + def test_force_complete_server_migration(self): + self._verify( + 'openstack.compute.v2.server_migration.ServerMigration.force_complete', # noqa: E501 + self.proxy.force_complete_server_migration, + method_args=['server_migration', 'server'], + expected_args=[self.proxy], + ) + + def test_get_server_migration(self): + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_server_migration, + method_args=['server_migration', 'server'], + expected_args=[ + server_migration.ServerMigration, + 'server_migration', + ], + expected_kwargs={ + 'server_id': 'server', + 'ignore_missing': True, + }, + ) + + def test_server_migrations(self): + self._verify( + 'openstack.proxy.Proxy._list', + self.proxy.server_migrations, + method_args=['server'], + expected_args=[server_migration.ServerMigration], + expected_kwargs={'server_id': 'server'}, + ) + def test_fetch_security_groups(self): self._verify( 'openstack.compute.v2.server.Server.fetch_security_groups', diff --git a/openstack/tests/unit/compute/v2/test_server_migration.py b/openstack/tests/unit/compute/v2/test_server_migration.py new file mode 100644 index 000000000..b652d38c8 --- /dev/null +++ b/openstack/tests/unit/compute/v2/test_server_migration.py @@ -0,0 +1,112 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from openstack.compute.v2 import server_migration +from openstack.tests.unit import base + +EXAMPLE = { + 'id': 4, + 'server_uuid': '4cfba335-03d8-49b2-8c52-e69043d1e8fe', + 'user_id': '8dbaa0f0-ab95-4ffe-8cb4-9c89d2ac9d24', + 'project_id': '5f705771-3aa9-4f4c-8660-0d9522ffdbea', + 'created_at': '2016-01-29T13:42:02.000000', + 'updated_at': '2016-01-29T13:42:02.000000', + 'status': 'migrating', + 'source_compute': 'compute1', + 'source_node': 'node1', + 'dest_host': '1.2.3.4', + 'dest_compute': 'compute2', + 'dest_node': 'node2', + 'memory_processed_bytes': 12345, + 'memory_remaining_bytes': 111111, + 'memory_total_bytes': 123456, + 'disk_processed_bytes': 23456, + 'disk_remaining_bytes': 211111, + 'disk_total_bytes': 234567, +} + + +class TestServerMigration(base.TestCase): + + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.sess = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + + def test_basic(self): + sot = server_migration.ServerMigration() + self.assertEqual('migration', sot.resource_key) + self.assertEqual('migrations', sot.resources_key) + self.assertEqual('/servers/%(server_uuid)s/migrations', sot.base_path) + self.assertFalse(sot.allow_create) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_commit) + self.assertTrue(sot.allow_delete) + + def test_make_it(self): + sot = server_migration.ServerMigration(**EXAMPLE) + self.assertEqual(EXAMPLE['id'], sot.id) + # FIXME(stephenfin): This conflicts since there is a server ID in the + # URI *and* in the body. We need a field that handles both or we need + # to use different names. + # self.assertEqual(EXAMPLE['server_uuid'], sot.server_id) + self.assertEqual(EXAMPLE['user_id'], sot.user_id) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) + self.assertEqual(EXAMPLE['status'], sot.status) + self.assertEqual(EXAMPLE['source_compute'], sot.source_compute) + self.assertEqual(EXAMPLE['source_node'], sot.source_node) + self.assertEqual(EXAMPLE['dest_host'], sot.dest_host) + self.assertEqual(EXAMPLE['dest_compute'], sot.dest_compute) + self.assertEqual(EXAMPLE['dest_node'], sot.dest_node) + self.assertEqual( + EXAMPLE['memory_processed_bytes'], + sot.memory_processed_bytes, + ) + self.assertEqual( + EXAMPLE['memory_remaining_bytes'], + sot.memory_remaining_bytes, + ) + self.assertEqual(EXAMPLE['memory_total_bytes'], sot.memory_total_bytes) + self.assertEqual( + EXAMPLE['disk_processed_bytes'], + sot.disk_processed_bytes, + ) + self.assertEqual( + EXAMPLE['disk_remaining_bytes'], + sot.disk_remaining_bytes, + ) + self.assertEqual(EXAMPLE['disk_total_bytes'], sot.disk_total_bytes) + + @mock.patch.object( + server_migration.ServerMigration, '_get_session', lambda self, x: x, + ) + def test_force_complete(self): + sot = server_migration.ServerMigration(**EXAMPLE) + + self.assertIsNone(sot.force_complete(self.sess)) + + url = 'servers/%s/migrations/%s/action' % ( + EXAMPLE['server_uuid'], EXAMPLE['id'] + ) + body = {'force_complete': None} + self.sess.post.assert_called_with( + url, microversion=mock.ANY, json=body, + ) diff --git a/releasenotes/notes/add-server-migrations-6e31183196f14deb.yaml b/releasenotes/notes/add-server-migrations-6e31183196f14deb.yaml new file mode 100644 index 000000000..8e451ceda --- /dev/null +++ b/releasenotes/notes/add-server-migrations-6e31183196f14deb.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add support for the Compute service's server migrations API, allowing users + to list all migrations for a server as well as force complete or abort + in-progress migrations.