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 <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane 2021-10-15 11:58:04 +01:00
parent 6c96faa7d1
commit 6d1321f5dd
8 changed files with 400 additions and 0 deletions

View File

@ -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

View File

@ -11,6 +11,7 @@ Compute Resources
v2/limits
v2/server
v2/server_interface
v2/server_migration
v2/server_ip
v2/hypervisor
v2/quota_set

View File

@ -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:

View File

@ -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):

View File

@ -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)

View File

@ -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',

View File

@ -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,
)

View File

@ -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.