From 405ebb9028a62731599e711546c293517ae08ef2 Mon Sep 17 00:00:00 2001 From: Mahesh Panchaksharaiah Date: Wed, 15 May 2013 17:53:36 +0530 Subject: [PATCH] List migrations through Admin API The os-migrations extension exposes endpoint to fetch all migrations. The migrations can filtered by host and status. If cells are enabled migrations can be listed for all cells or can be filtered for a particular cell. The route for fetching migrations for a region is - v2/{tenant_id}/os-migrations. Filters can be passed as query parameters - v2/{tenant_id}/os-migrations?host=host1&status=finished&cell_name=Child DocImpact Change-Id: Id70dbece344a722b2dc8c593dd340ef747eb43d3 Implements: blueprint list-resizes-through-admin-api --- .../all_extensions/extensions-get-resp.json | 8 ++ .../all_extensions/extensions-get-resp.xml | 4 + .../os-migrations/migrations-get.json | 32 +++++ .../os-migrations/migrations-get.xml | 5 + etc/nova/policy.json | 1 + .../openstack/compute/contrib/migrations.py | 77 +++++++++++ nova/cells/manager.py | 16 +++ nova/cells/messaging.py | 35 +++++ nova/cells/rpcapi.py | 6 + nova/compute/api.py | 4 + nova/compute/cells_api.py | 3 + nova/db/api.py | 5 + nova/db/sqlalchemy/api.py | 12 ++ .../compute/contrib/test_migrations.py | 122 ++++++++++++++++++ nova/tests/cells/test_cells_manager.py | 30 +++++ nova/tests/cells/test_cells_messaging.py | 52 ++++++++ nova/tests/cells/test_cells_rpcapi.py | 10 ++ nova/tests/compute/test_compute.py | 12 ++ nova/tests/compute/test_compute_cells.py | 13 ++ nova/tests/db/test_db_api.py | 17 +++ nova/tests/fake_policy.py | 2 + .../extensions-get-resp.json.tpl | 8 ++ .../extensions-get-resp.xml.tpl | 3 + .../os-migrations/migrations-get.json.tpl | 32 +++++ .../os-migrations/migrations-get.xml.tpl | 5 + nova/tests/integrated/test_api_samples.py | 54 ++++++++ 26 files changed, 568 insertions(+) create mode 100644 doc/api_samples/os-migrations/migrations-get.json create mode 100644 doc/api_samples/os-migrations/migrations-get.xml create mode 100644 nova/api/openstack/compute/contrib/migrations.py create mode 100644 nova/tests/api/openstack/compute/contrib/test_migrations.py create mode 100644 nova/tests/integrated/api_samples/os-migrations/migrations-get.json.tpl create mode 100644 nova/tests/integrated/api_samples/os-migrations/migrations-get.xml.tpl diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json index 1f6d762dfd90..9d24626df4b9 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.json +++ b/doc/api_samples/all_extensions/extensions-get-resp.json @@ -559,6 +559,14 @@ "name": "Volumes", "namespace": "http://docs.openstack.org/compute/ext/volumes/api/v1.1", "updated": "2011-03-25T00:00:00+00:00" + }, + { + "alias": "os-migrations", + "description": "Provide data on migrations.", + "links": [], + "name": "Migrations", + "namespace": "http://docs.openstack.org/compute/ext/migrations/api/v2.0", + "updated": "2013-05-30T00:00:00+00:00" } ] } diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml index a2136fadaf96..36054fee8482 100644 --- a/doc/api_samples/all_extensions/extensions-get-resp.xml +++ b/doc/api_samples/all_extensions/extensions-get-resp.xml @@ -228,4 +228,8 @@ Volumes support. + + Provide data on migrations. + diff --git a/doc/api_samples/os-migrations/migrations-get.json b/doc/api_samples/os-migrations/migrations-get.json new file mode 100644 index 000000000000..91775be77581 --- /dev/null +++ b/doc/api_samples/os-migrations/migrations-get.json @@ -0,0 +1,32 @@ +{ + "migrations": [ + { + "created_at": "2012-10-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1234, + "instance_uuid": "instance_id_123", + "new_instance_type_id": 2, + "old_instance_type_id": 1, + "source_compute": "compute1", + "source_node": "node1", + "status": "Done", + "updated_at": "2012-10-29T13:42:02.000000" + }, + { + "created_at": "2013-10-22T13:42:02.000000", + "dest_compute": "compute20", + "dest_host": "5.6.7.8", + "dest_node": "node20", + "id": 5678, + "instance_uuid": "instance_id_456", + "new_instance_type_id": 6, + "old_instance_type_id": 5, + "source_compute": "compute10", + "source_node": "node10", + "status": "Done", + "updated_at": "2013-10-22T13:42:02.000000" + } + ] +} \ No newline at end of file diff --git a/doc/api_samples/os-migrations/migrations-get.xml b/doc/api_samples/os-migrations/migrations-get.xml new file mode 100644 index 000000000000..f5c59c7f1b02 --- /dev/null +++ b/doc/api_samples/os-migrations/migrations-get.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 5c3acd2ba3c9..0be6f043e040 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -129,6 +129,7 @@ "compute_extension:availability_zone:list": "", "compute_extension:availability_zone:detail": "rule:admin_api", "compute_extension:used_limits_for_admin": "rule:admin_api", + "compute_extension:migrations:index": "rule:admin_api", "volume:create": "", diff --git a/nova/api/openstack/compute/contrib/migrations.py b/nova/api/openstack/compute/contrib/migrations.py new file mode 100644 index 000000000000..2d4b7d418789 --- /dev/null +++ b/nova/api/openstack/compute/contrib/migrations.py @@ -0,0 +1,77 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import compute + + +XMLNS = "http://docs.openstack.org/compute/ext/migrations/api/v2.0" +ALIAS = "os-migrations" + + +def authorize(context, action_name): + action = 'migrations:%s' % action_name + extensions.extension_authorizer('compute', action)(context) + + +class MigrationsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('migrations') + elem = xmlutil.SubTemplateElement(root, 'migration', + selector='migrations') + elem.set('id') + elem.set('source_node') + elem.set('dest_node') + elem.set('source_compute') + elem.set('dest_compute') + elem.set('dest_host') + elem.set('status') + elem.set('instance_uuid') + elem.set('old_instance_type_id') + elem.set('new_instance_type_id') + elem.set('created_at') + elem.set('updated_at') + + return xmlutil.MasterTemplate(root, 1) + + +class MigrationsController(object): + """Controller for accessing migrations in OpenStack API.""" + def __init__(self): + self.compute_api = compute.API() + + @wsgi.serializers(xml=MigrationsTemplate) + 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': migrations} + + +class Migrations(extensions.ExtensionDescriptor): + """Provide data on migrations.""" + name = "Migrations" + alias = ALIAS + namespace = XMLNS + updated = "2013-05-30T00:00:00+00:00" + + def get_resources(self): + resources = [] + resource = extensions.ResourceExtension('os-migrations', + MigrationsController()) + resources.append(resource) + return resources diff --git a/nova/cells/manager.py b/nova/cells/manager.py index 4dc9f2d82dbd..ccf582acc78f 100644 --- a/nova/cells/manager.py +++ b/nova/cells/manager.py @@ -46,6 +46,7 @@ cell_manager_opts = [ CONF = cfg.CONF +CONF.import_opt('name', 'nova.cells.opts', group='cells') CONF.register_opts(cell_manager_opts, group='cells') @@ -401,3 +402,18 @@ class CellsManager(manager.Manager): self.msg_runner.bdm_destroy_at_top(ctxt, instance_uuid, device_name=device_name, volume_id=volume_id) + + def get_migrations(self, ctxt, filters): + """Fetch migrations applying the filters.""" + target_cell = None + if "cell_name" in filters: + _path_cell_sep = cells_utils._PATH_CELL_SEP + target_cell = '%s%s%s' % (CONF.cells.name, _path_cell_sep, + filters['cell_name']) + + responses = self.msg_runner.get_migrations(ctxt, target_cell, + False, filters) + migrations = [] + for response in responses: + migrations += response.value_or_raise() + return migrations diff --git a/nova/cells/messaging.py b/nova/cells/messaging.py index a74ac1d758a4..60005e53fb97 100644 --- a/nova/cells/messaging.py +++ b/nova/cells/messaging.py @@ -796,6 +796,10 @@ class _TargetedMessageMethods(_BaseMessageMethods): return self.compute_rpcapi.validate_console_port(message.ctxt, instance, console_port, console_type) + def get_migrations(self, message, filters): + context = message.ctxt + return self.compute_api.get_migrations(context, filters) + class _BroadcastMessageMethods(_BaseMessageMethods): """These are the methods that can be called as a part of a broadcast @@ -1027,6 +1031,10 @@ class _BroadcastMessageMethods(_BaseMessageMethods): self.db.block_device_mapping_destroy_by_instance_and_volume( message.ctxt, instance_uuid, volume_id) + def get_migrations(self, message, filters): + context = message.ctxt + return self.compute_api.get_migrations(context, filters) + _CELL_MESSAGE_TYPE_TO_MESSAGE_CLS = {'targeted': _TargetedMessage, 'broadcast': _BroadcastMessage, @@ -1126,6 +1134,19 @@ class MessageRunner(object): response_kwargs, direction, target_cell, response_uuid, **kwargs) + def _get_migrations_for_cell(self, ctxt, cell_name, filters): + method_kwargs = dict(filters=filters) + message = _TargetedMessage(self, ctxt, 'get_migrations', + method_kwargs, 'down', cell_name, + need_response=True) + + response = message.process() + if response.failure and isinstance(response.value[1], + exception.CellRoutingInconsistency): + return [] + + return [response] + def message_from_json(self, json_message): """Turns a message in JSON format into an appropriate Message instance. This is called when cells receive a message from @@ -1438,6 +1459,20 @@ class MessageRunner(object): 'up', run_locally=False) message.process() + def get_migrations(self, ctxt, cell_name, run_locally, filters): + """Fetch all migrations applying the filters for a given cell or all + cells. + """ + method_kwargs = dict(filters=filters) + if cell_name: + return self._get_migrations_for_cell(ctxt, cell_name, filters) + + message = _BroadcastMessage(self, ctxt, 'get_migrations', + method_kwargs, 'down', + run_locally=run_locally, + need_response=True) + return message.process() + @staticmethod def get_message_types(): return _CELL_MESSAGE_TYPE_TO_MESSAGE_CLS.keys() diff --git a/nova/cells/rpcapi.py b/nova/cells/rpcapi.py index 5a9ac99fc5f3..5ed2d60e5f21 100644 --- a/nova/cells/rpcapi.py +++ b/nova/cells/rpcapi.py @@ -66,6 +66,7 @@ class CellsAPI(rpc_proxy.RpcProxy): 1.8 - Adds build_instances(), deprecates schedule_run_instance() 1.9 - Adds get_capacities() 1.10 - Adds bdm_update_or_create_at_top(), and bdm_destroy_at_top() + 1.11 - Adds get_migrations() ''' BASE_RPC_API_VERSION = '1.0' @@ -351,3 +352,8 @@ class CellsAPI(rpc_proxy.RpcProxy): version='1.10') except Exception: LOG.exception(_("Failed to notify cells of BDM destroy.")) + + def get_migrations(self, ctxt, filters): + """Get all migrations applying the filters.""" + return self.call(ctxt, self.make_msg('get_migrations', + filters=filters), version='1.11') diff --git a/nova/compute/api.py b/nova/compute/api.py index f9cc0a4d2f42..8c0e85c350b0 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -2616,6 +2616,10 @@ class API(base.Base): on_shared_storage=on_shared_storage, host=host) + def get_migrations(self, context, filters): + """Get all migrations for the given filters.""" + return self.db.migration_get_all_by_filters(context, filters) + class HostAPI(base.Base): """Sub-set of the Compute Manager API for managing host operations.""" diff --git a/nova/compute/cells_api.py b/nova/compute/cells_api.py index ddf959ef3da9..00a9e30d0939 100644 --- a/nova/compute/cells_api.py +++ b/nova/compute/cells_api.py @@ -579,6 +579,9 @@ class ComputeCellsAPI(compute_api.API): self._cast_to_cells(context, instance, 'live_migrate', block_migration, disk_over_commit, host_name) + def get_migrations(self, context, filters): + return self.cells_rpcapi.get_migrations(context, filters) + class HostAPI(compute_api.HostAPI): """HostAPI() class for cells. diff --git a/nova/db/api.py b/nova/db/api.py index 7fe94caf2d62..643745129f5b 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -418,6 +418,11 @@ def migration_get_in_progress_by_host_and_node(context, host, node): return IMPL.migration_get_in_progress_by_host_and_node(context, host, node) +def migration_get_all_by_filters(context, filters): + """Finds all migrations in progress.""" + return IMPL.migration_get_all_by_filters(context, filters) + + #################### diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 9cc394f04b69..856240653a62 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -3693,6 +3693,18 @@ def migration_get_in_progress_by_host_and_node(context, host, node, all() +@require_admin_context +def migration_get_all_by_filters(context, filters): + query = model_query(context, models.Migration) + if "status" in filters: + query = query.filter(models.Migration.status == filters["status"]) + if "host" in filters: + host = filters["host"] + query = query.filter(or_(models.Migration.source_compute == host, + models.Migration.dest_compute == host)) + return query.all() + + ################## diff --git a/nova/tests/api/openstack/compute/contrib/test_migrations.py b/nova/tests/api/openstack/compute/contrib/test_migrations.py new file mode 100644 index 000000000000..8f2177818608 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_migrations.py @@ -0,0 +1,122 @@ +# vim: tabstop=5 shiftwidth=4 softtabstop=4 + +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime + +from lxml import etree + +from nova.api.openstack.compute.contrib import migrations +from nova import context +from nova import exception +from nova import test +from nova.test import MoxStubout + + +fake_migrations = [ + { + 'id': 1234, + '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', + 'old_instance_type_id': 1, + 'new_instance_type_id': 2, + 'created_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + }, + { + 'id': 5678, + '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', + 'old_instance_type_id': 5, + 'new_instance_type_id': 6, + 'created_at': datetime.datetime(2013, 10, 22, 13, 42, 2), + 'updated_at': datetime.datetime(2013, 10, 22, 13, 42, 2), + } +] + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + GET = {} + + +class MigrationsTestCase(test.TestCase): + def setUp(self): + """Run before each test.""" + super(MigrationsTestCase, self).setUp() + self.controller = migrations.MigrationsController() + self.context = context.get_admin_context() + self.req = FakeRequest() + self.req.environ['nova.context'] = self.context + mox_fixture = self.useFixture(MoxStubout()) + self.mox = mox_fixture.mox + + def test_index(self): + migrations_in_progress = {'migrations': fake_migrations} + filters = {'host': 'host1', 'status': 'migrating', + 'cell_name': 'ChildCell'} + self.req.GET = filters + self.mox.StubOutWithMock(self.controller.compute_api, + "get_migrations") + + self.controller.compute_api.\ + get_migrations(self.context, filters).\ + AndReturn(fake_migrations) + self.mox.ReplayAll() + + response = self.controller.index(self.req) + self.assertEqual(migrations_in_progress, response) + + def test_index_needs_authorization(self): + user_context = context.RequestContext(user_id=None, + project_id=None, + is_admin=False, + read_deleted="no", + overwrite=False) + self.req.environ['nova.context'] = user_context + + self.assertRaises(exception.PolicyNotAuthorized, self.controller.index, + self.req) + + +class MigrationsTemplateTest(test.TestCase): + def setUp(self): + super(MigrationsTemplateTest, self).setUp() + self.serializer = migrations.MigrationsTemplate() + + def test_index_serialization(self): + res_xml = self.serializer.serialize({'migrations': fake_migrations}) + + tree = etree.XML(res_xml) + children = tree.findall('migration') + self.assertEqual(tree.tag, 'migrations') + self.assertEqual(2, len(children)) + + for idx, child in enumerate(children): + self.assertEqual(child.tag, 'migration') + migration = fake_migrations[idx] + for attr in migration.keys(): + self.assertEqual(str(migration[attr]), + child.get(attr)) diff --git a/nova/tests/cells/test_cells_manager.py b/nova/tests/cells/test_cells_manager.py index 89a60cb35665..31a9c7751361 100644 --- a/nova/tests/cells/test_cells_manager.py +++ b/nova/tests/cells/test_cells_manager.py @@ -572,3 +572,33 @@ class CellsManagerClassTestCase(test.TestCase): 'fake_instance_uuid', device_name='fake_device_name', volume_id='fake_volume_id') + + def test_get_migrations(self): + filters = {'status': 'confirmed'} + cell1_migrations = [{'id': 123}] + cell2_migrations = [{'id': 456}] + fake_responses = [self._get_fake_response(cell1_migrations), + self._get_fake_response(cell2_migrations)] + self.mox.StubOutWithMock(self.msg_runner, + 'get_migrations') + self.msg_runner.get_migrations(self.ctxt, None, False, filters).\ + AndReturn(fake_responses) + self.mox.ReplayAll() + + response = self.cells_manager.get_migrations(self.ctxt, filters) + + self.assertEqual([cell1_migrations[0], cell2_migrations[0]], response) + + def test_get_migrations_for_a_given_cell(self): + filters = {'status': 'confirmed', 'cell_name': 'ChildCell1'} + target_cell = '%s%s%s' % (CONF.cells.name, '!', filters['cell_name']) + migrations = [{'id': 123}] + fake_responses = [self._get_fake_response(migrations)] + self.mox.StubOutWithMock(self.msg_runner, + 'get_migrations') + self.msg_runner.get_migrations(self.ctxt, target_cell, False, + filters).AndReturn(fake_responses) + self.mox.ReplayAll() + + response = self.cells_manager.get_migrations(self.ctxt, filters) + self.assertEqual(migrations, response) diff --git a/nova/tests/cells/test_cells_messaging.py b/nova/tests/cells/test_cells_messaging.py index 9689b26851d0..7ef1e3694792 100644 --- a/nova/tests/cells/test_cells_messaging.py +++ b/nova/tests/cells/test_cells_messaging.py @@ -1044,6 +1044,31 @@ class CellsTargetedMethodsTestCase(test.TestCase): result = response.value_or_raise() self.assertEqual('fake_result', result) + def test_get_migrations_for_a_given_cell(self): + filters = {'cell_name': 'child-cell2', 'status': 'confirmed'} + migrations_in_progress = [{'id': 123}] + self.mox.StubOutWithMock(self.tgt_compute_api, + 'get_migrations') + + self.tgt_compute_api.get_migrations(self.ctxt, filters).\ + AndReturn(migrations_in_progress) + self.mox.ReplayAll() + + responses = self.src_msg_runner.get_migrations( + self.ctxt, + self.tgt_cell_name, False, filters) + result = responses[0].value_or_raise() + self.assertEqual(migrations_in_progress, result) + + def test_get_migrations_for_an_invalid_cell(self): + filters = {'cell_name': 'invalid_Cell', 'status': 'confirmed'} + + responses = self.src_msg_runner.get_migrations( + self.ctxt, + 'api_cell!invalid_cell', False, filters) + + self.assertEqual(0, len(responses)) + class CellsBroadcastMethodsTestCase(test.TestCase): """Test case for _BroadcastMessageMethods class. Most of these @@ -1696,3 +1721,30 @@ class CellsBroadcastMethodsTestCase(test.TestCase): self.src_msg_runner.bdm_destroy_at_top(self.ctxt, fake_instance_uuid, device_name=fake_device_name) + + def test_get_migrations(self): + self._setup_attrs(up=False) + filters = {'status': 'confirmed'} + migrations_from_cell1 = [{'id': 123}] + migrations_from_cell2 = [{'id': 456}] + self.mox.StubOutWithMock(self.mid_compute_api, + 'get_migrations') + + self.mid_compute_api.get_migrations(self.ctxt, filters).\ + AndReturn(migrations_from_cell1) + + self.mox.StubOutWithMock(self.tgt_compute_api, + 'get_migrations') + + self.tgt_compute_api.get_migrations(self.ctxt, filters).\ + AndReturn(migrations_from_cell2) + + self.mox.ReplayAll() + + responses = self.src_msg_runner.get_migrations( + self.ctxt, + None, False, filters) + self.assertEquals(2, len(responses)) + for response in responses: + self.assertIn(response.value_or_raise(), [migrations_from_cell1, + migrations_from_cell2]) diff --git a/nova/tests/cells/test_cells_rpcapi.py b/nova/tests/cells/test_cells_rpcapi.py index 6eeff1730dd6..c86ecfb12cd9 100644 --- a/nova/tests/cells/test_cells_rpcapi.py +++ b/nova/tests/cells/test_cells_rpcapi.py @@ -451,3 +451,13 @@ class CellsAPITestCase(test.TestCase): 'volume_id': 'fake-vol'} self._check_result(call_info, 'bdm_destroy_at_top', expected_args, version='1.10') + + def test_get_migrations(self): + call_info = self._stub_rpc_method('call', None) + filters = {'cell_name': 'ChildCell', 'status': 'confirmed'} + + self.cells_rpcapi.get_migrations(self.fake_context, filters) + + expected_args = {'filters': filters} + self._check_result(call_info, 'get_migrations', expected_args, + version="1.11") diff --git a/nova/tests/compute/test_compute.py b/nova/tests/compute/test_compute.py index eced9abbd2ad..b28a18ebb1d2 100644 --- a/nova/tests/compute/test_compute.py +++ b/nova/tests/compute/test_compute.py @@ -8001,6 +8001,18 @@ class ComputeAPITestCase(BaseTestCase): admin_password=None) db.instance_destroy(self.context, instance['uuid']) + def test_get_migrations(self): + migration = {uuid: "1234"} + filters = {'host': 'host1'} + self.mox.StubOutWithMock(db, "migration_get_all_by_filters") + db.migration_get_all_by_filters(self.context, + filters).AndReturn([migration]) + self.mox.ReplayAll() + + migrations = self.compute_api.get_migrations(self.context, + filters) + self.assertEqual(migrations, [migration]) + def fake_rpc_method(context, topic, msg, do_cast=True): pass diff --git a/nova/tests/compute/test_compute_cells.py b/nova/tests/compute/test_compute_cells.py index ad4e6c754b9d..bcdff7a73ca8 100644 --- a/nova/tests/compute/test_compute_cells.py +++ b/nova/tests/compute/test_compute_cells.py @@ -214,6 +214,19 @@ class CellsComputeAPITestCase(test_compute.ComputeAPITestCase): self.mox.ReplayAll() self.compute_api.soft_delete(self.context, inst) + def test_get_migrations(self): + filters = {'cell_name': 'ChildCell', 'status': 'confirmed'} + migrations = {'migrations': [{'id': 1234}]} + cells_rpcapi = self.compute_api.cells_rpcapi + self.mox.StubOutWithMock(cells_rpcapi, 'get_migrations') + cells_rpcapi.get_migrations(self.context, + filters).AndReturn(migrations) + self.mox.ReplayAll() + + response = self.compute_api.get_migrations(self.context, filters) + + self.assertEqual(migrations, response) + class CellsComputePolicyTestCase(test_compute.ComputePolicyTestCase): def setUp(self): diff --git a/nova/tests/db/test_db_api.py b/nova/tests/db/test_db_api.py index 2b4538d8f9be..1d75a8b62aea 100644 --- a/nova/tests/db/test_db_api.py +++ b/nova/tests/db/test_db_api.py @@ -870,6 +870,23 @@ class MigrationTestCase(test.TestCase): instance = migration['instance'] self.assertEqual(migration['instance_uuid'], instance['uuid']) + def test_get_migrations_by_filters(self): + filters = {"status": "migrating", "host": "host3"} + migrations = db.migration_get_all_by_filters(self.ctxt, filters) + self.assertEqual(2, len(migrations)) + for migration in migrations: + self.assertEqual(filters["status"], migration['status']) + hosts = [migration['source_compute'], migration['dest_compute']] + self.assertIn(filters["host"], hosts) + + def test_only_admin_can_get_all_migrations_by_filters(self): + user_ctxt = context.RequestContext(user_id=None, project_id=None, + is_admin=False, read_deleted="no", + overwrite=False) + + self.assertRaises(exception.AdminRequired, + db.migration_get_all_by_filters, user_ctxt, {}) + class ModelsObjectComparatorMixin(object): def _dict_from_object(self, obj, ignored_keys): diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index 3d7c57e1867f..826769c97da0 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -205,6 +205,8 @@ policy_data = """ "compute_extension:availability_zone:list": "", "compute_extension:availability_zone:detail": "is_admin:True", "compute_extension:used_limits_for_admin": "is_admin:True", + "compute_extension:migrations:index": "is_admin:True", + "volume:create": "", "volume:get": "", diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl index b2aa73c6aee7..6b5eee37ccfa 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl @@ -559,6 +559,14 @@ "name": "InstanceActions", "namespace": "http://docs.openstack.org/compute/ext/instance-actions/api/v1.1", "updated": "%(timestamp)s" + }, + { + "alias": "os-migrations", + "description": "%(text)s", + "links": [], + "name": "Migrations", + "namespace": "http://docs.openstack.org/compute/ext/migrations/api/v2.0", + "updated": "%(timestamp)s" } ] } diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl index e9670d650381..cb01ad50b79c 100644 --- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl +++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl @@ -210,4 +210,7 @@ %(text)s + + %(text)s + diff --git a/nova/tests/integrated/api_samples/os-migrations/migrations-get.json.tpl b/nova/tests/integrated/api_samples/os-migrations/migrations-get.json.tpl new file mode 100644 index 000000000000..91775be77581 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-migrations/migrations-get.json.tpl @@ -0,0 +1,32 @@ +{ + "migrations": [ + { + "created_at": "2012-10-29T13:42:02.000000", + "dest_compute": "compute2", + "dest_host": "1.2.3.4", + "dest_node": "node2", + "id": 1234, + "instance_uuid": "instance_id_123", + "new_instance_type_id": 2, + "old_instance_type_id": 1, + "source_compute": "compute1", + "source_node": "node1", + "status": "Done", + "updated_at": "2012-10-29T13:42:02.000000" + }, + { + "created_at": "2013-10-22T13:42:02.000000", + "dest_compute": "compute20", + "dest_host": "5.6.7.8", + "dest_node": "node20", + "id": 5678, + "instance_uuid": "instance_id_456", + "new_instance_type_id": 6, + "old_instance_type_id": 5, + "source_compute": "compute10", + "source_node": "node10", + "status": "Done", + "updated_at": "2013-10-22T13:42:02.000000" + } + ] +} \ No newline at end of file diff --git a/nova/tests/integrated/api_samples/os-migrations/migrations-get.xml.tpl b/nova/tests/integrated/api_samples/os-migrations/migrations-get.xml.tpl new file mode 100644 index 000000000000..f5c59c7f1b02 --- /dev/null +++ b/nova/tests/integrated/api_samples/os-migrations/migrations-get.xml.tpl @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/nova/tests/integrated/test_api_samples.py b/nova/tests/integrated/test_api_samples.py index 253b2e07c48e..43815ef49a81 100644 --- a/nova/tests/integrated/test_api_samples.py +++ b/nova/tests/integrated/test_api_samples.py @@ -3994,3 +3994,57 @@ class VolumesSampleJsonTest(ServersSampleBase): class VolumesSampleXmlTest(VolumesSampleJsonTest): ctype = 'xml' + + +class MigrationsSamplesJsonTest(ApiSampleTestBase): + extension_name = ("nova.api.openstack.compute.contrib.migrations." + "Migrations") + + def _stub_migrations(self, context, filters): + fake_migrations = [ + { + 'id': 1234, + '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', + 'old_instance_type_id': 1, + 'new_instance_type_id': 2, + 'created_at': datetime.datetime(2012, 10, 29, 13, 42, 2), + 'updated_at': datetime.datetime(2012, 10, 29, 13, 42, 2) + }, + { + 'id': 5678, + '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', + 'old_instance_type_id': 5, + 'new_instance_type_id': 6, + 'created_at': datetime.datetime(2013, 10, 22, 13, 42, 2), + 'updated_at': datetime.datetime(2013, 10, 22, 13, 42, 2) + } + ] + return fake_migrations + + def setUp(self): + super(MigrationsSamplesJsonTest, self).setUp() + self.stubs.Set(compute_api.API, 'get_migrations', + self._stub_migrations) + + def test_get_migrations(self): + response = self._do_get('os-migrations') + subs = self._get_regexes() + + self.assertEqual(response.status, 200) + self._verify_response('migrations-get', subs, response, 200) + + +class MigrationsSamplesXmlTest(MigrationsSamplesJsonTest): + ctype = 'xml'