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:
parent
6c96faa7d1
commit
6d1321f5dd
@ -152,7 +152,16 @@ Extension Operations
|
|||||||
|
|
||||||
QuotaSet Operations
|
QuotaSet Operations
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
.. autoclass:: openstack.compute.v2._proxy.Proxy
|
.. autoclass:: openstack.compute.v2._proxy.Proxy
|
||||||
:noindex:
|
:noindex:
|
||||||
:members: get_quota_set, get_quota_set_defaults,
|
:members: get_quota_set, get_quota_set_defaults,
|
||||||
revert_quota_set, update_quota_set
|
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
|
||||||
|
@ -11,6 +11,7 @@ Compute Resources
|
|||||||
v2/limits
|
v2/limits
|
||||||
v2/server
|
v2/server
|
||||||
v2/server_interface
|
v2/server_interface
|
||||||
|
v2/server_migration
|
||||||
v2/server_ip
|
v2/server_ip
|
||||||
v2/hypervisor
|
v2/hypervisor
|
||||||
v2/quota_set
|
v2/quota_set
|
||||||
|
13
doc/source/user/resources/compute/v2/server_migration.rst
Normal file
13
doc/source/user/resources/compute/v2/server_migration.rst
Normal 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:
|
@ -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_group as _server_group
|
||||||
from openstack.compute.v2 import server_interface as _server_interface
|
from openstack.compute.v2 import server_interface as _server_interface
|
||||||
from openstack.compute.v2 import server_ip
|
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 server_remote_console as _src
|
||||||
from openstack.compute.v2 import service as _service
|
from openstack.compute.v2 import service as _service
|
||||||
from openstack.compute.v2 import volume_attachment as _volume_attachment
|
from openstack.compute.v2 import volume_attachment as _volume_attachment
|
||||||
@ -1717,6 +1718,118 @@ class Proxy(proxy.Proxy):
|
|||||||
block_migration=block_migration,
|
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 ==========
|
# ========== Server diagnostics ==========
|
||||||
|
|
||||||
def get_server_diagnostics(self, server):
|
def get_server_diagnostics(self, server):
|
||||||
|
98
openstack/compute/v2/server_migration.py
Normal file
98
openstack/compute/v2/server_migration.py
Normal 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)
|
@ -26,6 +26,7 @@ from openstack.compute.v2 import server
|
|||||||
from openstack.compute.v2 import server_group
|
from openstack.compute.v2 import server_group
|
||||||
from openstack.compute.v2 import server_interface
|
from openstack.compute.v2 import server_interface
|
||||||
from openstack.compute.v2 import server_ip
|
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 server_remote_console
|
||||||
from openstack.compute.v2 import service
|
from openstack.compute.v2 import service
|
||||||
from openstack import resource
|
from openstack import resource
|
||||||
@ -1004,6 +1005,53 @@ class TestCompute(TestComputeProxy):
|
|||||||
expected_args=[self.proxy, "host1"],
|
expected_args=[self.proxy, "host1"],
|
||||||
expected_kwargs={'force': False, 'block_migration': None})
|
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):
|
def test_fetch_security_groups(self):
|
||||||
self._verify(
|
self._verify(
|
||||||
'openstack.compute.v2.server.Server.fetch_security_groups',
|
'openstack.compute.v2.server.Server.fetch_security_groups',
|
||||||
|
112
openstack/tests/unit/compute/v2/test_server_migration.py
Normal file
112
openstack/tests/unit/compute/v2/test_server_migration.py
Normal 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,
|
||||||
|
)
|
@ -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.
|
Loading…
x
Reference in New Issue
Block a user