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'