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'