Add new APIs and deprecate old API for migrations

This patch does two things:
1. Add two APIs /servers/migrations:index/show for server migrations.
Two new novaclient commands server-migration-list and
server-migration-show will also be added.
ref: I071198fa9ba0699383bdebf4fab54714a435e6c3

2. Add ref link for /os-migrations
The old top-level resource `/os-migrations` won't be extended anymore.
It is deprecated.
Adding migration_type for it, also add ref link to
/servers/{uuid}/migrations/{id} for it when the migration is an
in progress migration.

Partially implements blueprint live-migration-progress-report

Change-Id: Ia92ecbe3c99082e3a34adf4fd29041b1a95ef21e
Co-authored-by: ShaoHe Feng <shaohe.feng@intel.com>
This commit is contained in:
ShaoHe Feng 2015-12-24 19:51:43 +08:00
parent 058e21ec98
commit 98e4a64ad3
29 changed files with 1040 additions and 42 deletions

View File

@ -0,0 +1,74 @@
{
"migrations": [
{
"created_at": "2016-01-29T13:42:02.000000",
"dest_compute": "compute2",
"dest_host": "1.2.3.4",
"dest_node": "node2",
"id": 1,
"instance_uuid": "8600d31b-d1a1-4632-b2ff-45c2be1a70ff",
"links": [
{
"href": "http://openstack.example.com/v2.1/openstack/servers/8600d31b-d1a1-4632-b2ff-45c2be1a70ff/migrations/1",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/8600d31b-d1a1-4632-b2ff-45c2be1a70ff/migrations/1",
"rel": "bookmark"
}
],
"new_instance_type_id": 2,
"old_instance_type_id": 1,
"source_compute": "compute1",
"source_node": "node1",
"status": "running",
"migration_type": "live-migration",
"updated_at": "2016-01-29T13:42:02.000000"
},
{
"created_at": "2016-01-29T13:42:02.000000",
"dest_compute": "compute2",
"dest_host": "1.2.3.4",
"dest_node": "node2",
"id": 2,
"instance_uuid": "8600d31b-d1a1-4632-b2ff-45c2be1a70ff",
"new_instance_type_id": 2,
"old_instance_type_id": 1,
"source_compute": "compute1",
"source_node": "node1",
"status": "error",
"migration_type": "live-migration",
"updated_at": "2016-01-29T13:42:02.000000"
},
{
"created_at": "2016-01-22T13:42:02.000000",
"dest_compute": "compute20",
"dest_host": "5.6.7.8",
"dest_node": "node20",
"id": 3,
"instance_uuid": "9128d044-7b61-403e-b766-7547076ff6c1",
"new_instance_type_id": 6,
"old_instance_type_id": 5,
"source_compute": "compute10",
"source_node": "node10",
"status": "error",
"migration_type": "resize",
"updated_at": "2016-01-22T13:42:02.000000"
},
{
"created_at": "2016-01-22T13:42:02.000000",
"dest_compute": "compute20",
"dest_host": "5.6.7.8",
"dest_node": "node20",
"id": 4,
"instance_uuid": "9128d044-7b61-403e-b766-7547076ff6c1",
"new_instance_type_id": 6,
"old_instance_type_id": 5,
"source_compute": "compute10",
"source_node": "node10",
"status": "migrating",
"migration_type": "resize",
"updated_at": "2016-01-22T13:42:02.000000"
}
]
}

View File

@ -0,0 +1,20 @@
{
"migration": {
"created_at": "2016-01-29T13:42:02.000000",
"dest_compute": "compute2",
"dest_host": "1.2.3.4",
"dest_node": "node2",
"id": 1,
"server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe",
"source_compute": "compute1",
"source_node": "node1",
"status": "running",
"memory_total_bytes": 123456,
"memory_processed_bytes": 12345,
"memory_remaining_bytes": 120000,
"disk_total_bytes": 234567,
"disk_processed_bytes": 23456,
"disk_remaining_bytes": 230000,
"updated_at": "2016-01-29T13:42:02.000000"
}
}

View File

@ -0,0 +1,22 @@
{
"migrations": [
{
"created_at": "2016-01-29T13:42:02.000000",
"dest_compute": "compute2",
"dest_host": "1.2.3.4",
"dest_node": "node2",
"id": 1,
"server_uuid": "4cfba335-03d8-49b2-8c52-e69043d1e8fe",
"source_compute": "compute1",
"source_node": "node1",
"status": "running",
"memory_total_bytes": 123456,
"memory_processed_bytes": 12345,
"memory_remaining_bytes": 120000,
"disk_total_bytes": 234567,
"disk_processed_bytes": 23456,
"disk_remaining_bytes": 230000,
"updated_at": "2016-01-29T13:42:02.000000"
}
]
}

View File

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.22",
"version": "2.23",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.22",
"version": "2.23",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -272,6 +272,8 @@
"os_compute_api:servers:trigger_crash_dump": "rule:admin_or_owner",
"os_compute_api:servers:migrations:force_complete": "rule:admin_api",
"os_compute_api:servers:discoverable": "@",
"os_compute_api:servers:migrations:index": "rule:admin_api",
"os_compute_api:servers:migrations:show": "rule:admin_api",
"os_compute_api:os-access-ips:discoverable": "@",
"os_compute_api:os-access-ips": "rule:admin_or_owner",
"os_compute_api:os-admin-actions": "rule:admin_api",

View File

@ -65,6 +65,9 @@ REST_API_VERSION_HISTORY = """REST API Version History:
and shelved_offloaded state
* 2.21 - Make os-instance-actions read deleted instances
* 2.22 - Add API to force live migration to complete
* 2.23 - Add index/show API for server migrations.
Also add migration_type for /os-migrations and add ref link for it
when the migration is an in progress live migration.
"""
# The minimum and maximum versions of the API supported
@ -73,7 +76,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.22"
_MAX_API_VERSION = "2.23"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -10,6 +10,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from nova.api.openstack import api_version_request
from nova.api.openstack import common
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
from nova import compute
@ -23,42 +25,64 @@ def authorize(context, action_name):
extensions.os_compute_authorizer(ALIAS)(context, action=action_name)
def output(migrations_obj):
"""Returns the desired output of the API from an object.
From a MigrationsList's object this method returns a list of
primitive objects with the only necessary fields.
"""
detail_keys = ['memory_total', 'memory_processed', 'memory_remaining',
'disk_total', 'disk_processed', 'disk_remaining']
# Note(Shaohe Feng): We need to leverage the oslo.versionedobjects.
# Then we can pass the target version to it's obj_to_primitive.
objects = obj_base.obj_to_primitive(migrations_obj)
objects = [x for x in objects if not x['hidden']]
for obj in objects:
del obj['deleted']
del obj['deleted_at']
del obj['migration_type']
del obj['hidden']
if 'memory_total' in obj:
for key in detail_keys:
del obj[key]
return objects
class MigrationsController(wsgi.Controller):
"""Controller for accessing migrations in OpenStack API."""
_view_builder_class = common.ViewBuilder
_collection_name = "servers/%s/migrations"
def __init__(self):
super(MigrationsController, self).__init__()
self.compute_api = compute.API()
def _output(self, req, migrations_obj, add_link=False):
"""Returns the desired output of the API from an object.
From a MigrationsList's object this method returns a list of
primitive objects with the only necessary fields.
"""
detail_keys = ['memory_total', 'memory_processed', 'memory_remaining',
'disk_total', 'disk_processed', 'disk_remaining']
# TODO(Shaohe Feng) we should share the in-progress list.
live_migration_in_progress = ['queued', 'preparing',
'running', 'post-migrating']
# Note(Shaohe Feng): We need to leverage the oslo.versionedobjects.
# Then we can pass the target version to it's obj_to_primitive.
objects = obj_base.obj_to_primitive(migrations_obj)
objects = [x for x in objects if not x['hidden']]
for obj in objects:
del obj['deleted']
del obj['deleted_at']
del obj['hidden']
if 'memory_total' in obj:
for key in detail_keys:
del obj[key]
# NOTE(Shaohe Feng) above version 2.23, add migration_type for all
# kinds of migration, but we only add links just for in-progress
# live-migration.
if add_link and obj['migration_type'] == "live-migration" and (
obj["status"] in live_migration_in_progress):
obj["links"] = self._view_builder._get_links(
req, obj["id"],
self._collection_name % obj['instance_uuid'])
elif add_link is False:
del obj['migration_type']
return objects
@extensions.expected_errors(())
def index(self, req):
"""Return all migrations in progress."""
context = req.environ['nova.context']
authorize(context, "index")
migrations = self.compute_api.get_migrations(context, req.GET)
return {'migrations': output(migrations)}
if api_version_request.is_supported(req, min_version='2.23'):
return {'migrations': self._output(req, migrations, True)}
return {'migrations': self._output(req, migrations)}
class Migrations(extensions.V21APIExtensionBase):

View File

@ -22,11 +22,39 @@ from nova.api.openstack import wsgi
from nova.api import validation
from nova import compute
from nova import exception
from nova.i18n import _
ALIAS = 'servers:migrations'
authorize = extensions.os_compute_authorizer(ALIAS)
def output(migration):
"""Returns the desired output of the API from an object.
From a Migrations's object this method returns the primitive
object with the only necessary and expected fields.
"""
return {
"created_at": migration.created_at,
"dest_compute": migration.dest_compute,
"dest_host": migration.dest_host,
"dest_node": migration.dest_node,
"disk_processed_bytes": migration.disk_processed,
"disk_remaining_bytes": migration.disk_remaining,
"disk_total_bytes": migration.disk_total,
"id": migration.id,
"memory_processed_bytes": migration.memory_processed,
"memory_remaining_bytes": migration.memory_remaining,
"memory_total_bytes": migration.memory_total,
"server_uuid": migration.instance_uuid,
"source_compute": migration.source_compute,
"source_node": migration.source_node,
"status": migration.status,
"updated_at": migration.updated_at
}
class ServerMigrationsController(wsgi.Controller):
"""The server migrations API controller for the OpenStack API."""
@ -58,6 +86,55 @@ class ServerMigrationsController(wsgi.Controller):
common.raise_http_conflict_for_instance_invalid_state(
state_error, 'force_complete', server_id)
@wsgi.Controller.api_version("2.23")
@extensions.expected_errors(404)
def index(self, req, server_id):
"""Return all migrations of an instance in progress."""
context = req.environ['nova.context']
authorize(context, action="index")
# NOTE(Shaohe Feng) just check the instance is available. To keep
# consistency with other API, check it before get migrations.
common.get_instance(self.compute_api, context, server_id)
migrations = self.compute_api.get_migrations_in_progress_by_instance(
context, server_id, 'live-migration')
return {'migrations': [output(migration) for migration in migrations]}
@wsgi.Controller.api_version("2.23")
@extensions.expected_errors(404)
def show(self, req, server_id, id):
"""Return the migration of an instance in progress by id."""
context = req.environ['nova.context']
authorize(context, action="show")
# NOTE(Shaohe Feng) just check the instance is available. To keep
# consistency with other API, check it before get migrations.
common.get_instance(self.compute_api, context, server_id)
try:
migration = self.compute_api.get_migration_by_id_and_instance(
context, id, server_id)
except exception.MigrationNotFoundForInstance:
msg = _("In-progress live migration %(id)s is not found for"
" server %(uuid)s.") % {"id": id, "uuid": server_id}
raise exc.HTTPNotFound(explanation=msg)
if migration.get("migration_type") != "live-migration":
msg = _("Migration %(id)s for server %(uuid)s is not"
" live-migration.") % {"id": id, "uuid": server_id}
raise exc.HTTPNotFound(explanation=msg)
# TODO(Shaohe Feng) we should share the in-progress list.
in_progress = ['queued', 'preparing', 'running', 'post-migrating']
if migration.get("status") not in in_progress:
msg = _("Live migration %(id)s for server %(uuid)s is not in"
" progress.") % {"id": id, "uuid": server_id}
raise exc.HTTPNotFound(explanation=msg)
return {'migration': output(migration)}
class ServerMigrations(extensions.V21APIExtensionBase):
"""Server Migrations API."""

View File

@ -195,3 +195,13 @@ user documentation.
{
"force_complete": null
}
2.23
----
From this version of the API users can get the migration summary list by
index API or the information of a specific migration by get API.
And the old top-level resource `/os-migrations` won't be extended anymore.
Add migration_type for old /os-migrations API, also add ref link to the
/servers/{uuid}/migrations/{id} for it when the migration is an in-progress
live-migration.

View File

@ -3395,6 +3395,18 @@ class API(base.Base):
"""Get all migrations for the given filters."""
return objects.MigrationList.get_by_filters(context, filters)
def get_migrations_in_progress_by_instance(self, context, instance_uuid,
migration_type=None):
"""Get all migrations of an instance in progress."""
return objects.MigrationList.get_in_progress_by_instance(
context, instance_uuid, migration_type)
def get_migration_by_id_and_instance(self, context,
migration_id, instance_uuid):
"""Get the migration of an instance by id."""
return objects.Migration.get_by_id_and_instance(
context, migration_id, instance_uuid)
@wrap_check_policy
def volume_snapshot_create(self, context, volume_id, create_info):
bdm = objects.BlockDeviceMapping.get_by_volume(

View File

@ -511,6 +511,13 @@ def migration_get_all_by_filters(context, filters):
return IMPL.migration_get_all_by_filters(context, filters)
def migration_get_in_progress_by_instance(context, instance_uuid,
migration_type=None):
"""Finds all migrations of an instance in progress."""
return IMPL.migration_get_in_progress_by_instance(context, instance_uuid,
migration_type)
####################

View File

@ -4547,6 +4547,23 @@ def migration_get_in_progress_by_host_and_node(context, host, node):
all()
@main_context_manager.reader
def migration_get_in_progress_by_instance(context, instance_uuid,
migration_type=None):
# TODO(Shaohe Feng) we should share the in-progress list.
# TODO(Shaohe Feng) will also summarize all status to a new
# MigrationStatus class.
query = model_query(context, models.Migration).\
filter_by(instance_uuid=instance_uuid).\
filter(models.Migration.status.in_(['queued', 'preparing',
'running',
'post-migrating']))
if migration_type:
query = query.filter(models.Migration.migration_type == migration_type)
return query.all()
@main_context_manager.reader
def migration_get_all_by_filters(context, filters):
query = model_query(context, models.Migration)

View File

@ -157,7 +157,9 @@ class MigrationList(base.ObjectListBase, base.NovaObject):
# Migration <= 1.1
# Version 1.1: Added use_slave to get_unconfirmed_by_dest_compute
# Version 1.2: Migration version 1.2
VERSION = '1.2'
# Version 1.3: Added a new function to get in progress migrations
# for an instance.
VERSION = '1.3'
fields = {
'objects': fields.ListOfObjectsField('Migration'),
@ -190,3 +192,11 @@ class MigrationList(base.ObjectListBase, base.NovaObject):
db_migrations = db.migration_get_all_by_filters(context, filters)
return base.obj_make_list(context, cls(context), objects.Migration,
db_migrations)
@base.remotable_classmethod
def get_in_progress_by_instance(cls, context, instance_uuid,
migration_type=None):
db_migrations = db.migration_get_in_progress_by_instance(
context, instance_uuid, migration_type)
return base.obj_make_list(context, cls(context), objects.Migration,
db_migrations)

View File

@ -0,0 +1,74 @@
{
"migrations": [
{
"created_at": "2016-01-29T13:42:02.000000",
"dest_compute": "compute2",
"dest_host": "1.2.3.4",
"dest_node": "node2",
"id": 1,
"instance_uuid": "%(instance_1)s",
"links": [
{
"href": "%(host)s/v2.1/openstack/servers/%(instance_1)s/migrations/1",
"rel": "self"
},
{
"href": "%(host)s/openstack/servers/%(instance_1)s/migrations/1",
"rel": "bookmark"
}
],
"new_instance_type_id": 2,
"old_instance_type_id": 1,
"source_compute": "compute1",
"source_node": "node1",
"migration_type": "live-migration",
"status": "running",
"updated_at": "2016-01-29T13:42:02.000000"
},
{
"created_at": "2016-01-29T13:42:02.000000",
"dest_compute": "compute2",
"dest_host": "1.2.3.4",
"dest_node": "node2",
"id": 2,
"instance_uuid": "%(instance_1)s",
"new_instance_type_id": 2,
"old_instance_type_id": 1,
"source_compute": "compute1",
"source_node": "node1",
"migration_type": "live-migration",
"status": "error",
"updated_at": "2016-01-29T13:42:02.000000"
},
{
"created_at": "2016-01-22T13:42:02.000000",
"dest_compute": "compute20",
"dest_host": "5.6.7.8",
"dest_node": "node20",
"id": 3,
"instance_uuid": "%(instance_2)s",
"new_instance_type_id": 6,
"old_instance_type_id": 5,
"source_compute": "compute10",
"source_node": "node10",
"migration_type": "resize",
"status": "error",
"updated_at": "2016-01-22T13:42:02.000000"
},
{
"created_at": "2016-01-22T13:42:02.000000",
"dest_compute": "compute20",
"dest_host": "5.6.7.8",
"dest_node": "node20",
"id": 4,
"instance_uuid": "%(instance_2)s",
"new_instance_type_id": 6,
"old_instance_type_id": 5,
"source_compute": "compute10",
"source_node": "node10",
"migration_type": "resize",
"status": "migrating",
"updated_at": "2016-01-22T13:42:02.000000"
}
]
}

View File

@ -0,0 +1,20 @@
{
"migration": {
"created_at": "2016-01-29T13:42:02.000000",
"dest_compute": "compute2",
"dest_host": "1.2.3.4",
"dest_node": "node2",
"id": 1,
"server_uuid": "%(server_uuid)s",
"source_compute": "compute1",
"source_node": "node1",
"status": "running",
"memory_total_bytes": 123456,
"memory_processed_bytes": 12345,
"memory_remaining_bytes": 120000,
"disk_total_bytes": 234567,
"disk_processed_bytes": 23456,
"disk_remaining_bytes": 230000,
"updated_at": "2016-01-29T13:42:02.000000"
}
}

View File

@ -0,0 +1,22 @@
{
"migrations": [
{
"created_at": "2016-01-29T13:42:02.000000",
"dest_compute": "compute2",
"dest_host": "1.2.3.4",
"dest_node": "node2",
"id": 1,
"server_uuid": "%(server_uuid_1)s",
"source_compute": "compute1",
"source_node": "node1",
"status": "running",
"memory_total_bytes": 123456,
"memory_processed_bytes": 12345,
"memory_remaining_bytes": 120000,
"disk_total_bytes": 234567,
"disk_processed_bytes": 23456,
"disk_remaining_bytes": 230000,
"updated_at": "2016-01-29T13:42:02.000000"
}
]
}

View File

@ -17,6 +17,8 @@ import datetime
from oslo_config import cfg
from nova import context
from nova import objects
from nova.tests.functional.api_sample_tests import api_sample_base
CONF = cfg.CONF
@ -24,6 +26,12 @@ CONF.import_opt('osapi_compute_extension',
'nova.api.openstack.compute.legacy_v2.extensions')
# NOTE(ShaoHe Feng) here I can not use uuidsentinel, it generate a random
# UUID. The uuid in doc/api_samples files is fixed.
INSTANCE_UUID_1 = "8600d31b-d1a1-4632-b2ff-45c2be1a70ff"
INSTANCE_UUID_2 = "9128d044-7b61-403e-b766-7547076ff6c1"
class MigrationsSamplesJsonTest(api_sample_base.ApiSampleTestBaseV21):
ADMIN_API = True
extension_name = "os-migrations"
@ -86,3 +94,101 @@ class MigrationsSamplesJsonTest(api_sample_base.ApiSampleTestBaseV21):
self.assertEqual(200, response.status_code)
self._verify_response('migrations-get', {}, response, 200)
class MigrationsSamplesJsonTestV2_23(api_sample_base.ApiSampleTestBaseV21):
ADMIN_API = True
extension_name = "os-migrations"
microversion = '2.23'
scenarios = [('v2_23', {'api_major_version': 'v2.1'})]
fake_migrations = [
# in-progress live-migration.
{
'source_node': 'node1',
'dest_node': 'node2',
'source_compute': 'compute1',
'dest_compute': 'compute2',
'dest_host': '1.2.3.4',
'status': 'running',
'instance_uuid': INSTANCE_UUID_1,
'old_instance_type_id': 1,
'new_instance_type_id': 2,
'migration_type': 'live-migration',
'hidden': False,
'created_at': datetime.datetime(2016, 0o1, 29, 13, 42, 2),
'updated_at': datetime.datetime(2016, 0o1, 29, 13, 42, 2),
'deleted_at': None,
'deleted': False
},
# non in-progress live-migration.
{
'source_node': 'node1',
'dest_node': 'node2',
'source_compute': 'compute1',
'dest_compute': 'compute2',
'dest_host': '1.2.3.4',
'status': 'error',
'instance_uuid': INSTANCE_UUID_1,
'old_instance_type_id': 1,
'new_instance_type_id': 2,
'migration_type': 'live-migration',
'hidden': False,
'created_at': datetime.datetime(2016, 0o1, 29, 13, 42, 2),
'updated_at': datetime.datetime(2016, 0o1, 29, 13, 42, 2),
'deleted_at': None,
'deleted': False
},
# non in-progress resize.
{
'source_node': 'node10',
'dest_node': 'node20',
'source_compute': 'compute10',
'dest_compute': 'compute20',
'dest_host': '5.6.7.8',
'status': 'error',
'instance_uuid': INSTANCE_UUID_2,
'old_instance_type_id': 5,
'new_instance_type_id': 6,
'migration_type': 'resize',
'hidden': False,
'created_at': datetime.datetime(2016, 0o1, 22, 13, 42, 2),
'updated_at': datetime.datetime(2016, 0o1, 22, 13, 42, 2),
'deleted_at': None,
'deleted': False
},
# in-progress resize.
{
'source_node': 'node10',
'dest_node': 'node20',
'source_compute': 'compute10',
'dest_compute': 'compute20',
'dest_host': '5.6.7.8',
'status': 'migrating',
'instance_uuid': INSTANCE_UUID_2,
'old_instance_type_id': 5,
'new_instance_type_id': 6,
'migration_type': 'resize',
'hidden': False,
'created_at': datetime.datetime(2016, 0o1, 22, 13, 42, 2),
'updated_at': datetime.datetime(2016, 0o1, 22, 13, 42, 2),
'deleted_at': None,
'deleted': False
}
]
def setUp(self):
super(MigrationsSamplesJsonTestV2_23, self).setUp()
fake_context = context.RequestContext('fake', 'fake')
for mig in self.fake_migrations:
mig_obj = objects.Migration(context=fake_context, **mig)
mig_obj.create()
def test_get_migrations_v2_23(self):
response = self._do_get('os-migrations')
self.assertEqual(200, response.status_code)
self._verify_response(
'migrations-get',
{"instance_1": INSTANCE_UUID_1, "instance_2": INSTANCE_UUID_2},
response, 200)

View File

@ -13,12 +13,15 @@
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import mock
from nova.conductor import manager as conductor_manager
from nova import context
from nova import db
from nova import objects
from nova.tests.functional.api_sample_tests import test_servers
from nova.tests.unit import fake_instance
class ServerMigrationsSampleJsonTest(test_servers.ServersSampleBase):
@ -50,3 +53,108 @@ class ServerMigrationsSampleJsonTest(test_servers.ServersSampleBase):
response = self._do_post('servers/%s/migrations/%s/action'
% (self.uuid, '3'), 'force_complete', {})
self.assertEqual(202, response.status_code)
def test_get_migration(self):
response = self._do_get('servers/fake_id/migrations/1234')
self.assertEqual(404, response.status_code)
def test_list_migrations(self):
response = self._do_get('servers/fake_id/migrations')
self.assertEqual(404, response.status_code)
class ServerMigrationsSamplesJsonTestV2_23(test_servers.ServersSampleBase):
ADMIN_API = True
extension_name = "server-migrations"
microversion = '2.23'
scenarios = [('v2_23', {'api_major_version': 'v2.1'})]
UUID_1 = '4cfba335-03d8-49b2-8c52-e69043d1e8fe'
UUID_2 = '058fc419-a8a8-4e08-b62c-a9841ef9cd3f'
fake_migrations = [
{
'source_node': 'node1',
'dest_node': 'node2',
'source_compute': 'compute1',
'dest_compute': 'compute2',
'dest_host': '1.2.3.4',
'status': 'running',
'instance_uuid': UUID_1,
'migration_type': 'live-migration',
'hidden': False,
'memory_total': 123456,
'memory_processed': 12345,
'memory_remaining': 120000,
'disk_total': 234567,
'disk_processed': 23456,
'disk_remaining': 230000,
'created_at': datetime.datetime(2016, 0o1, 29, 13, 42, 2),
'updated_at': datetime.datetime(2016, 0o1, 29, 13, 42, 2),
'deleted_at': None,
'deleted': False
},
{
'source_node': 'node10',
'dest_node': 'node20',
'source_compute': 'compute10',
'dest_compute': 'compute20',
'dest_host': '5.6.7.8',
'status': 'migrating',
'instance_uuid': UUID_2,
'migration_type': 'resize',
'hidden': False,
'memory_total': 456789,
'memory_processed': 56789,
'memory_remaining': 45000,
'disk_total': 96789,
'disk_processed': 6789,
'disk_remaining': 96000,
'created_at': datetime.datetime(2016, 0o1, 22, 13, 42, 2),
'updated_at': datetime.datetime(2016, 0o1, 22, 13, 42, 2),
'deleted_at': None,
'deleted': False
}
]
def setUp(self):
super(ServerMigrationsSamplesJsonTestV2_23, self).setUp()
fake_context = context.RequestContext('fake', 'fake')
self.mig1 = objects.Migration(
context=fake_context, **self.fake_migrations[0])
self.mig1.create()
self.mig2 = objects.Migration(
context=fake_context, **self.fake_migrations[1])
self.mig2.create()
fake_ins = fake_instance.fake_db_instance(uuid=self.UUID_1)
fake_ins.pop("pci_devices")
fake_ins.pop("security_groups")
fake_ins.pop("services")
fake_ins.pop("tags")
fake_ins.pop("info_cache")
fake_ins.pop("id")
self.instance = objects.Instance(
context=fake_context,
**fake_ins)
self.instance.create()
def test_get_migration(self):
response = self._do_get('servers/%s/migrations/%s' %
(self.fake_migrations[0]["instance_uuid"],
self.mig1.id))
self.assertEqual(200, response.status_code)
self._verify_response('migrations-get',
{"server_uuid": self.UUID_1},
response, 200)
def test_list_migrations(self):
response = self._do_get('servers/%s/migrations' %
self.fake_migrations[0]["instance_uuid"])
self.assertEqual(200, response.status_code)
self._verify_response('migrations-index',
{"server_uuid_1": self.UUID_1},
response, 200)

View File

@ -13,6 +13,7 @@
# under the License.
import datetime
import mock
from oslotest import moxstubout
@ -25,21 +26,23 @@ from nova import objects
from nova.objects import base
from nova import test
from nova.tests.unit.api.openstack import fakes
from nova.tests import uuidsentinel as uuids
fake_migrations = [
# in-progress live migration
{
'id': 1234,
'id': 1,
'source_node': 'node1',
'dest_node': 'node2',
'source_compute': 'compute1',
'dest_compute': 'compute2',
'dest_host': '1.2.3.4',
'status': 'Done',
'instance_uuid': 'instance_id_123',
'status': 'running',
'instance_uuid': uuids.instance1,
'old_instance_type_id': 1,
'new_instance_type_id': 2,
'migration_type': 'resize',
'migration_type': 'live-migration',
'hidden': False,
'memory_total': 123456,
'memory_processed': 12345,
@ -52,15 +55,66 @@ fake_migrations = [
'deleted_at': None,
'deleted': False
},
# non in-progress live migration
{
'id': 5678,
'id': 2,
'source_node': 'node1',
'dest_node': 'node2',
'source_compute': 'compute1',
'dest_compute': 'compute2',
'dest_host': '1.2.3.4',
'status': 'error',
'instance_uuid': uuids.instance1,
'old_instance_type_id': 1,
'new_instance_type_id': 2,
'migration_type': 'live-migration',
'hidden': False,
'memory_total': 123456,
'memory_processed': 12345,
'memory_remaining': 120000,
'disk_total': 234567,
'disk_processed': 23456,
'disk_remaining': 230000,
'created_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
'deleted_at': None,
'deleted': False
},
# in-progress resize
{
'id': 4,
'source_node': 'node10',
'dest_node': 'node20',
'source_compute': 'compute10',
'dest_compute': 'compute20',
'dest_host': '5.6.7.8',
'status': 'Done',
'instance_uuid': 'instance_id_456',
'status': 'migrating',
'instance_uuid': uuids.instance2,
'old_instance_type_id': 5,
'new_instance_type_id': 6,
'migration_type': 'resize',
'hidden': False,
'memory_total': 456789,
'memory_processed': 56789,
'memory_remaining': 45000,
'disk_total': 96789,
'disk_processed': 6789,
'disk_remaining': 96000,
'created_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
'updated_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
'deleted_at': None,
'deleted': False
},
# non in-progress resize
{
'id': 5,
'source_node': 'node10',
'dest_node': 'node20',
'source_compute': 'compute10',
'dest_compute': 'compute20',
'dest_host': '5.6.7.8',
'status': 'error',
'instance_uuid': uuids.instance2,
'old_instance_type_id': 5,
'new_instance_type_id': 6,
'migration_type': 'resize',
@ -94,23 +148,26 @@ class FakeRequest(object):
class MigrationsTestCaseV21(test.NoDBTestCase):
migrations = migrations_v21
def _migrations_output(self):
return self.controller._output(self.req, migrations_obj)
def setUp(self):
"""Run before each test."""
super(MigrationsTestCaseV21, self).setUp()
self.controller = self.migrations.MigrationsController()
self.req = FakeRequest()
self.req = fakes.HTTPRequest.blank('', use_admin_context=True)
self.context = self.req.environ['nova.context']
mox_fixture = self.useFixture(moxstubout.MoxStubout())
self.mox = mox_fixture.mox
def test_index(self):
migrations_in_progress = {
'migrations': self.migrations.output(migrations_obj)}
migrations_in_progress = {'migrations': self._migrations_output()}
for mig in migrations_in_progress['migrations']:
self.assertIn('id', mig)
self.assertNotIn('deleted', mig)
self.assertNotIn('deleted_at', mig)
self.assertNotIn('links', mig)
filters = {'host': 'host1', 'status': 'migrating',
'cell_name': 'ChildCell'}
@ -129,9 +186,11 @@ class MigrationsTestCaseV21(test.NoDBTestCase):
class MigrationsTestCaseV2(MigrationsTestCaseV21):
migrations = migrations_v2
def _migrations_output(self):
return self.migrations.output(migrations_obj)
def setUp(self):
super(MigrationsTestCaseV2, self).setUp()
self.req = fakes.HTTPRequest.blank('', use_admin_context=True)
self.context = self.req.environ['nova.context']
def test_index_needs_authorization(self):
@ -146,6 +205,40 @@ class MigrationsTestCaseV2(MigrationsTestCaseV21):
self.req)
class MigrationsTestCaseV223(MigrationsTestCaseV21):
wsgi_api_version = '2.23'
def setUp(self):
"""Run before each test."""
super(MigrationsTestCaseV223, self).setUp()
self.req = fakes.HTTPRequest.blank(
'', version=self.wsgi_api_version, use_admin_context=True)
def test_index(self):
migrations = {'migrations': self.controller._output(
self.req, migrations_obj, True)}
for i, mig in enumerate(migrations['migrations']):
# first item is in-progress live migration
if i == 0:
self.assertIn('links', mig)
else:
self.assertNotIn('links', mig)
self.assertIn('migration_type', mig)
self.assertIn('id', mig)
self.assertNotIn('deleted', mig)
self.assertNotIn('deleted_at', mig)
with mock.patch.object(self.controller.compute_api,
'get_migrations') as m_get:
m_get.return_value = migrations_obj
response = self.controller.index(self.req)
self.assertEqual(migrations, response)
self.assertIn('links', response['migrations'][0])
self.assertIn('migration_type', response['migrations'][0])
class MigrationsPolicyEnforcement(test.NoDBTestCase):
def setUp(self):
super(MigrationsPolicyEnforcement, self).setUp()
@ -161,3 +254,11 @@ class MigrationsPolicyEnforcement(test.NoDBTestCase):
self.assertEqual(
"Policy doesn't allow %s to be performed." % rule_name,
exc.format_message())
class MigrationsPolicyEnforcementV223(MigrationsPolicyEnforcement):
wsgi_api_version = '2.23'
def setUp(self):
super(MigrationsPolicyEnforcementV223, self).setUp()
self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version)

View File

@ -13,13 +13,77 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import datetime
import mock
import webob
from nova.api.openstack.compute import server_migrations
from nova import exception
from nova import objects
from nova.objects import base
from nova import test
from nova.tests.unit.api.openstack import fakes
from nova.tests import uuidsentinel as uuids
SERVER_UUID = uuids.server_uuid
fake_migrations = [
{
'id': 1234,
'source_node': 'node1',
'dest_node': 'node2',
'source_compute': 'compute1',
'dest_compute': 'compute2',
'dest_host': '1.2.3.4',
'status': 'running',
'instance_uuid': SERVER_UUID,
'old_instance_type_id': 1,
'new_instance_type_id': 2,
'migration_type': 'live-migration',
'hidden': False,
'memory_total': 123456,
'memory_processed': 12345,
'memory_remaining': 120000,
'disk_total': 234567,
'disk_processed': 23456,
'disk_remaining': 230000,
'created_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2),
'deleted_at': None,
'deleted': False
},
{
'id': 5678,
'source_node': 'node10',
'dest_node': 'node20',
'source_compute': 'compute10',
'dest_compute': 'compute20',
'dest_host': '5.6.7.8',
'status': 'running',
'instance_uuid': SERVER_UUID,
'old_instance_type_id': 5,
'new_instance_type_id': 6,
'migration_type': 'live-migration',
'hidden': False,
'memory_total': 456789,
'memory_processed': 56789,
'memory_remaining': 45000,
'disk_total': 96789,
'disk_processed': 6789,
'disk_remaining': 96000,
'created_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
'updated_at': datetime.datetime(2013, 10, 22, 13, 42, 2),
'deleted_at': None,
'deleted': False
}
]
migrations_obj = base.obj_make_list(
'fake-context',
objects.MigrationList(),
objects.Migration,
fake_migrations)
class ServerMigrationsTestsV21(test.NoDBTestCase):
@ -86,6 +150,117 @@ class ServerMigrationsTestsV21(test.NoDBTestCase):
exception.NovaException(), webob.exc.HTTPInternalServerError)
class ServerMigrationsTestsV223(ServerMigrationsTestsV21):
wsgi_api_version = '2.23'
def setUp(self):
super(ServerMigrationsTestsV223, self).setUp()
self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version,
use_admin_context=True)
self.context = self.req.environ['nova.context']
@mock.patch('nova.compute.api.API.get_migrations_in_progress_by_instance')
@mock.patch('nova.compute.api.API.get')
def test_index(self, m_get_instance, m_get_mig):
migrations = [server_migrations.output(mig) for mig in migrations_obj]
migrations_in_progress = {'migrations': migrations}
for mig in migrations_in_progress['migrations']:
self.assertIn('id', mig)
self.assertNotIn('deleted', mig)
self.assertNotIn('deleted_at', mig)
m_get_mig.return_value = migrations_obj
response = self.controller.index(self.req, SERVER_UUID)
self.assertEqual(migrations_in_progress, response)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get')
def test_index_invalid_instance(self, m_get_instance):
m_get_instance.side_effect = exception.InstanceNotFound(instance_id=1)
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.index,
self.req, SERVER_UUID)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get_migration_by_id_and_instance')
@mock.patch('nova.compute.api.API.get')
def test_show(self, m_get_instance, m_get_mig):
migrations = [server_migrations.output(mig) for mig in migrations_obj]
m_get_mig.return_value = migrations_obj[0]
response = self.controller.show(self.req, SERVER_UUID,
migrations_obj[0].id)
self.assertEqual(migrations[0], response['migration'])
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get_migration_by_id_and_instance')
@mock.patch('nova.compute.api.API.get')
def test_show_migration_non_progress(self, m_get_instance, m_get_mig):
non_progress_mig = copy.deepcopy(migrations_obj[0])
non_progress_mig.status = "reverted"
m_get_mig.return_value = non_progress_mig
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
self.req, SERVER_UUID,
non_progress_mig.id)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get_migration_by_id_and_instance')
@mock.patch('nova.compute.api.API.get')
def test_show_migration_not_live_migration(self, m_get_instance,
m_get_mig):
non_progress_mig = copy.deepcopy(migrations_obj[0])
non_progress_mig.migration_type = "resize"
m_get_mig.return_value = non_progress_mig
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
self.req, SERVER_UUID,
non_progress_mig.id)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get_migration_by_id_and_instance')
@mock.patch('nova.compute.api.API.get')
def test_show_migration_not_exist(self, m_get_instance, m_get_mig):
m_get_mig.side_effect = exception.MigrationNotFoundForInstance(
migration_id=migrations_obj[0].id,
instance_id=SERVER_UUID)
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
self.req, SERVER_UUID,
migrations_obj[0].id)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
@mock.patch('nova.compute.api.API.get')
def test_show_migration_invalid_instance(self, m_get_instance):
m_get_instance.side_effect = exception.InstanceNotFound(instance_id=1)
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
self.req, SERVER_UUID,
migrations_obj[0].id)
m_get_instance.assert_called_once_with(self.context, SERVER_UUID,
expected_attrs=None,
want_objects=True)
class ServerMigrationsPolicyEnforcementV21(test.NoDBTestCase):
wsgi_api_version = '2.22'
@ -106,3 +281,30 @@ class ServerMigrationsPolicyEnforcementV21(test.NoDBTestCase):
self.assertEqual(
"Policy doesn't allow %s to be performed." % rule_name,
exc.format_message())
class ServerMigrationsPolicyEnforcementV223(
ServerMigrationsPolicyEnforcementV21):
wsgi_api_version = '2.23'
def setUp(self):
super(ServerMigrationsPolicyEnforcementV223, self).setUp()
def test_migration_index_failed(self):
rule_name = "os_compute_api:servers:migrations:index"
self.policy.set_rules({rule_name: "project:non_fake"})
exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.index, self.req,
fakes.FAKE_UUID)
self.assertEqual("Policy doesn't allow %s to be performed." %
rule_name, exc.format_message())
def test_migration_show_failed(self):
rule_name = "os_compute_api:servers:migrations:show"
self.policy.set_rules({rule_name: "project:non_fake"})
exc = self.assertRaises(exception.PolicyNotAuthorized,
self.controller.show, self.req,
fakes.FAKE_UUID, 1)
self.assertEqual("Policy doesn't allow %s to be performed." %
rule_name, exc.format_message())

View File

@ -10179,6 +10179,26 @@ class ComputeAPITestCase(BaseTestCase):
self.assertEqual(1, len(migrations))
self.assertEqual(migrations[0].id, migration['id'])
@mock.patch("nova.db.migration_get_in_progress_by_instance")
def test_get_migrations_in_progress_by_instance(self, mock_get):
migration = test_migration.fake_db_migration(instance_uuid="1234")
mock_get.return_value = [migration]
db.migration_get_in_progress_by_instance(self.context, "1234")
migrations = self.compute_api.get_migrations_in_progress_by_instance(
self.context, "1234")
self.assertEqual(1, len(migrations))
self.assertEqual(migrations[0].id, migration['id'])
@mock.patch("nova.db.migration_get_by_id_and_instance")
def test_get_migration_by_id_and_instance(self, mock_get):
migration = test_migration.fake_db_migration(instance_uuid="1234")
mock_get.return_value = migration
db.migration_get_by_id_and_instance(
self.context, migration['id'], uuid)
res = self.compute_api.get_migration_by_id_and_instance(
self.context, migration['id'], "1234")
self.assertEqual(res.id, migration['id'])
class ComputeAPIIpFilterTestCase(test.NoDBTestCase):
'''Verifies the IP filtering in the compute API.'''

View File

@ -1355,6 +1355,7 @@ class MigrationTestCase(test.TestCase):
'dest_node': dest_node, 'instance_uuid': instance['uuid'],
'migration_type': migration_type}
db.migration_create(self.ctxt, values)
return values
def _assert_in_progress(self, migrations):
for migration in migrations:
@ -1475,6 +1476,37 @@ class MigrationTestCase(test.TestCase):
db.migration_update(self.ctxt, migration['id'],
{"status": "CONFIRMED"})
def test_migration_get_in_progress_by_instance(self):
values = self._create(status='running',
migration_type="live-migration")
results = db.migration_get_in_progress_by_instance(
self.ctxt, values["instance_uuid"], "live-migration")
self.assertEqual(1, len(results))
for key in values:
self.assertEqual(values[key], results[0][key])
self.assertEqual("running", results[0]["status"])
def test_migration_get_in_progress_by_instance_not_in_progress(self):
values = self._create(migration_type="live-migration")
results = db.migration_get_in_progress_by_instance(
self.ctxt, values["instance_uuid"], "live-migration")
self.assertEqual(0, len(results))
def test_migration_get_in_progress_by_instance_not_live_migration(self):
values = self._create(migration_type="resize")
results = db.migration_get_in_progress_by_instance(
self.ctxt, values["instance_uuid"], "live-migration")
self.assertEqual(0, len(results))
results = db.migration_get_in_progress_by_instance(
self.ctxt, values["instance_uuid"])
self.assertEqual(0, len(results))
def test_migration_update_not_found(self):
self.assertRaises(exception.MigrationNotFound,
db.migration_update, self.ctxt, 42, {})

View File

@ -126,6 +126,8 @@ policy_data = """
"os_compute_api:servers:stop": "",
"os_compute_api:servers:trigger_crash_dump": "",
"os_compute_api:servers:migrations:force_complete": "",
"os_compute_api:servers:migrations:index": "rule:admin_api",
"os_compute_api:servers:migrations:show": "rule:admin_api",
"os_compute_api:os-access-ips": "",
"compute_extension:accounts": "",
"compute_extension:admin_actions:pause": "",

View File

@ -82,6 +82,20 @@ class _TestMigrationObject(object):
ctxt, fake_migration['id'], 'migrating')
self.compare_obj(mig, fake_migration)
@mock.patch('nova.db.migration_get_in_progress_by_instance')
def test_get_in_progress_by_instance(self, m_get_mig):
ctxt = context.get_admin_context()
fake_migration = fake_db_migration()
db_migrations = [fake_migration, dict(fake_migration, id=456)]
m_get_mig.return_value = db_migrations
migrations = migration.MigrationList.get_in_progress_by_instance(
ctxt, fake_migration['instance_uuid'])
self.assertEqual(2, len(migrations))
for index, db_migration in enumerate(db_migrations):
self.compare_obj(migrations[index], db_migration)
def test_create(self):
ctxt = context.get_admin_context()
fake_migration = fake_db_migration()

View File

@ -1151,7 +1151,7 @@ object_data = {
'KeyPairList': '1.2-58b94f96e776bedaf1e192ddb2a24c4e',
'Migration': '1.4-17979b9f2ae7f28d97043a220b2a8350',
'MigrationContext': '1.0-d8c2f10069e410f639c49082b5932c92',
'MigrationList': '1.2-02c0ec0c50b75ca86a2a74c5e8c911cc',
'MigrationList': '1.3-55595bfc1a299a5962614d0821a3567e',
'MonitorMetric': '1.1-53b1db7c4ae2c531db79761e7acc52ba',
'MonitorMetricList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'NotificationPublisher': '1.0-bbbc1402fb0e443a3eb227cc52b61545',

View File

@ -362,7 +362,10 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
"os_compute_api:os-assisted-volume-snapshots:delete",
"os_compute_api:os-console-auth-tokens",
"os_compute_api:os-quota-class-sets:update",
"os_compute_api:os-server-external-events:create")
"os_compute_api:os-server-external-events:create",
"os_compute_api:servers:migrations:index",
"os_compute_api:servers:migrations:show",
)
self.admin_or_owner_rules = (
"default",

View File

@ -0,0 +1,8 @@
---
deprecations:
-
The old top-level resource `/os-migrations` is deprecated, it won't be
extended anymore. And migration_type for /os-migrations, also add ref
link to the /servers/{uuid}/migrations/{id} for it when the migration
is an in-progress live-migration. This has been added in microversion
2.23.

View File

@ -0,0 +1,8 @@
---
features:
- |
Add two new list/show API for server-migration.
The list API will return the in progress live migratons
information of a server. The show API will return
a specified in progress live migration of a server.
This has been added in microversion 2.23.