diff --git a/doc/api_samples/os-migrations/v2.23/migrations-get.json b/doc/api_samples/os-migrations/v2.23/migrations-get.json new file mode 100644 index 000000000000..ece3ac6eaf78 --- /dev/null +++ b/doc/api_samples/os-migrations/v2.23/migrations-get.json @@ -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" + } + ] +} diff --git a/doc/api_samples/server-migrations/v2.23/migrations-get.json b/doc/api_samples/server-migrations/v2.23/migrations-get.json new file mode 100644 index 000000000000..8be7a017ed5f --- /dev/null +++ b/doc/api_samples/server-migrations/v2.23/migrations-get.json @@ -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" + } +} diff --git a/doc/api_samples/server-migrations/v2.23/migrations-index.json b/doc/api_samples/server-migrations/v2.23/migrations-index.json new file mode 100644 index 000000000000..b4279e354275 --- /dev/null +++ b/doc/api_samples/server-migrations/v2.23/migrations-index.json @@ -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" + } + ] +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 2f23b09a08f8..efbbf344babb 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.22", + "version": "2.23", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index c37948ab98cd..6b4eaada1ac2 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.22", + "version": "2.23", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/etc/nova/policy.json b/etc/nova/policy.json index d6b072d50ba0..3877c99edbef 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -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", diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 5a316257733a..77a358cc4bec 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -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 diff --git a/nova/api/openstack/compute/migrations.py b/nova/api/openstack/compute/migrations.py index 265f23b059cf..29a9de66a92d 100644 --- a/nova/api/openstack/compute/migrations.py +++ b/nova/api/openstack/compute/migrations.py @@ -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): diff --git a/nova/api/openstack/compute/server_migrations.py b/nova/api/openstack/compute/server_migrations.py index caa37c1e0a7d..7acf6c4e4f08 100644 --- a/nova/api/openstack/compute/server_migrations.py +++ b/nova/api/openstack/compute/server_migrations.py @@ -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.""" diff --git a/nova/api/openstack/rest_api_version_history.rst b/nova/api/openstack/rest_api_version_history.rst index 5cd44edb59d7..85c45087becf 100644 --- a/nova/api/openstack/rest_api_version_history.rst +++ b/nova/api/openstack/rest_api_version_history.rst @@ -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. diff --git a/nova/compute/api.py b/nova/compute/api.py index e0805b92584a..c4e0ee221817 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -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( diff --git a/nova/db/api.py b/nova/db/api.py index 39b04c731f5c..894d84a4784a 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -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) + + #################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index b8c7ae25c3b7..94d8a6050129 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -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) diff --git a/nova/objects/migration.py b/nova/objects/migration.py index 67b7ae5d6b14..9baec5e38ec7 100644 --- a/nova/objects/migration.py +++ b/nova/objects/migration.py @@ -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) diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-migrations/v2.23/migrations-get.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-migrations/v2.23/migrations-get.json.tpl new file mode 100644 index 000000000000..0251b1392a72 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-migrations/v2.23/migrations-get.json.tpl @@ -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" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/server-migrations/v2.23/migrations-get.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/server-migrations/v2.23/migrations-get.json.tpl new file mode 100644 index 000000000000..0db052a29553 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/server-migrations/v2.23/migrations-get.json.tpl @@ -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" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/server-migrations/v2.23/migrations-index.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/server-migrations/v2.23/migrations-index.json.tpl new file mode 100644 index 000000000000..24f60beb6ab8 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/server-migrations/v2.23/migrations-index.json.tpl @@ -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" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/test_migrations.py b/nova/tests/functional/api_sample_tests/test_migrations.py index a4b266ac2cee..f284eb4cd31f 100644 --- a/nova/tests/functional/api_sample_tests/test_migrations.py +++ b/nova/tests/functional/api_sample_tests/test_migrations.py @@ -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) diff --git a/nova/tests/functional/api_sample_tests/test_server_migrations.py b/nova/tests/functional/api_sample_tests/test_server_migrations.py index 0b063f584319..88d5279d72fd 100644 --- a/nova/tests/functional/api_sample_tests/test_server_migrations.py +++ b/nova/tests/functional/api_sample_tests/test_server_migrations.py @@ -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) diff --git a/nova/tests/unit/api/openstack/compute/test_migrations.py b/nova/tests/unit/api/openstack/compute/test_migrations.py index 07f1d92127d8..de206a339811 100644 --- a/nova/tests/unit/api/openstack/compute/test_migrations.py +++ b/nova/tests/unit/api/openstack/compute/test_migrations.py @@ -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) diff --git a/nova/tests/unit/api/openstack/compute/test_server_migrations.py b/nova/tests/unit/api/openstack/compute/test_server_migrations.py index 0d9037fbe2df..a63b1d5dc224 100644 --- a/nova/tests/unit/api/openstack/compute/test_server_migrations.py +++ b/nova/tests/unit/api/openstack/compute/test_server_migrations.py @@ -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()) diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py index c256765a7c1f..0311b19f220c 100644 --- a/nova/tests/unit/compute/test_compute.py +++ b/nova/tests/unit/compute/test_compute.py @@ -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.''' diff --git a/nova/tests/unit/db/test_db_api.py b/nova/tests/unit/db/test_db_api.py index b830eac10355..12dd492873be 100644 --- a/nova/tests/unit/db/test_db_api.py +++ b/nova/tests/unit/db/test_db_api.py @@ -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, {}) diff --git a/nova/tests/unit/fake_policy.py b/nova/tests/unit/fake_policy.py index 2444c2196b1a..42cae2140007 100644 --- a/nova/tests/unit/fake_policy.py +++ b/nova/tests/unit/fake_policy.py @@ -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": "", diff --git a/nova/tests/unit/objects/test_migration.py b/nova/tests/unit/objects/test_migration.py index 2c3c5ee51efe..74fa70e909ae 100644 --- a/nova/tests/unit/objects/test_migration.py +++ b/nova/tests/unit/objects/test_migration.py @@ -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() diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 94691935b3ee..2e3862f8411c 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -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', diff --git a/nova/tests/unit/test_policy.py b/nova/tests/unit/test_policy.py index 1e86629eed28..1d8da7914e58 100644 --- a/nova/tests/unit/test_policy.py +++ b/nova/tests/unit/test_policy.py @@ -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", diff --git a/releasenotes/notes/os-migrations-ef225e5b309d5497.yaml b/releasenotes/notes/os-migrations-ef225e5b309d5497.yaml new file mode 100644 index 000000000000..96c842c648c9 --- /dev/null +++ b/releasenotes/notes/os-migrations-ef225e5b309d5497.yaml @@ -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. diff --git a/releasenotes/notes/server_migrations-30519b35d3ea6763.yaml b/releasenotes/notes/server_migrations-30519b35d3ea6763.yaml new file mode 100644 index 000000000000..aa6feadc513b --- /dev/null +++ b/releasenotes/notes/server_migrations-30519b35d3ea6763.yaml @@ -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.