382 lines
14 KiB
Python
382 lines
14 KiB
Python
# 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
|
|
|
|
import mock
|
|
from oslo_utils.fixture import uuidsentinel as uuids
|
|
import six
|
|
from webob import exc
|
|
|
|
from nova.api.openstack.compute import migrations as migrations_v21
|
|
from nova import context
|
|
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
|
|
|
|
|
|
fake_migrations = [
|
|
# in-progress live migration
|
|
{
|
|
'id': 1,
|
|
'source_node': 'node1',
|
|
'dest_node': 'node2',
|
|
'source_compute': 'compute1',
|
|
'dest_compute': 'compute2',
|
|
'dest_host': '1.2.3.4',
|
|
'status': 'running',
|
|
'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': 111111,
|
|
'disk_total': 234567,
|
|
'disk_processed': 23456,
|
|
'disk_remaining': 211111,
|
|
'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,
|
|
'uuid': uuids.migration1,
|
|
},
|
|
# non in-progress live migration
|
|
{
|
|
'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': 111111,
|
|
'disk_total': 234567,
|
|
'disk_processed': 23456,
|
|
'disk_remaining': 211111,
|
|
'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,
|
|
'uuid': uuids.migration2,
|
|
},
|
|
# in-progress resize
|
|
{
|
|
'id': 4,
|
|
'source_node': 'node10',
|
|
'dest_node': 'node20',
|
|
'source_compute': 'compute10',
|
|
'dest_compute': 'compute20',
|
|
'dest_host': '5.6.7.8',
|
|
'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,
|
|
'uuid': uuids.migration3,
|
|
},
|
|
# 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',
|
|
'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,
|
|
'uuid': uuids.migration4,
|
|
}
|
|
]
|
|
|
|
migrations_obj = base.obj_make_list(
|
|
'fake-context',
|
|
objects.MigrationList(),
|
|
objects.Migration,
|
|
fake_migrations)
|
|
|
|
|
|
class FakeRequest(object):
|
|
environ = {"nova.context": context.RequestContext('fake_user', 'fake',
|
|
is_admin=True)}
|
|
GET = {}
|
|
|
|
|
|
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 = fakes.HTTPRequest.blank('', use_admin_context=True)
|
|
self.context = self.req.environ['nova.context']
|
|
|
|
def test_index(self):
|
|
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',
|
|
'instance_uuid': uuids.instance1,
|
|
'source_compute': 'host1', 'hidden': '0',
|
|
'migration_type': 'resize'}
|
|
|
|
# python-novaclient actually supports sending this even though it's
|
|
# not used in the DB API layer and is totally useless. This lets us,
|
|
# however, test that additionalProperties=True allows it.
|
|
unknown_filter = {'cell_name': 'ChildCell'}
|
|
|
|
self.req.GET.update(filters)
|
|
self.req.GET.update(unknown_filter)
|
|
|
|
with mock.patch.object(self.controller.compute_api,
|
|
'get_migrations',
|
|
return_value=migrations_obj) as (
|
|
mock_get_migrations
|
|
):
|
|
response = self.controller.index(self.req)
|
|
self.assertEqual(migrations_in_progress, response)
|
|
# Only with the filters, and the unknown filter is stripped
|
|
mock_get_migrations.assert_called_once_with(self.context, filters)
|
|
|
|
def test_index_query_allow_negative_int_as_string(self):
|
|
migrations = {'migrations': self._migrations_output()}
|
|
filters = ['host', 'status', 'cell_name', 'instance_uuid',
|
|
'source_compute', 'hidden', 'migration_type']
|
|
|
|
with mock.patch.object(self.controller.compute_api,
|
|
'get_migrations',
|
|
return_value=migrations_obj):
|
|
for fl in filters:
|
|
req = fakes.HTTPRequest.blank('/os-migrations',
|
|
use_admin_context=True,
|
|
query_string='%s=-1' % fl)
|
|
response = self.controller.index(req)
|
|
self.assertEqual(migrations, response)
|
|
|
|
def test_index_query_duplicate_query_parameters(self):
|
|
migrations = {'migrations': self._migrations_output()}
|
|
params = {'host': 'host1', 'status': 'migrating',
|
|
'cell_name': 'ChildCell', 'instance_uuid': uuids.instance1,
|
|
'source_compute': 'host1', 'hidden': '0',
|
|
'migration_type': 'resize'}
|
|
|
|
with mock.patch.object(self.controller.compute_api,
|
|
'get_migrations',
|
|
return_value=migrations_obj):
|
|
for k, v in params.items():
|
|
req = fakes.HTTPRequest.blank(
|
|
'/os-migrations', use_admin_context=True,
|
|
query_string='%s=%s&%s=%s' % (k, v, k, v))
|
|
response = self.controller.index(req)
|
|
self.assertEqual(migrations, response)
|
|
|
|
|
|
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 MigrationsTestCaseV259(MigrationsTestCaseV223):
|
|
wsgi_api_version = '2.59'
|
|
|
|
def test_index(self):
|
|
migrations = {'migrations': self.controller._output(
|
|
self.req, migrations_obj, True, 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.assertIn('uuid', mig)
|
|
self.assertNotIn('deleted', mig)
|
|
self.assertNotIn('deleted_at', mig)
|
|
|
|
with mock.patch.object(self.controller.compute_api,
|
|
'get_migrations_sorted') 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])
|
|
|
|
@mock.patch('nova.compute.api.API.get_migrations_sorted')
|
|
def test_index_with_invalid_marker(self, mock_migrations_get):
|
|
"""Tests detail paging with an invalid marker (not found)."""
|
|
mock_migrations_get.side_effect = exception.MarkerNotFound(
|
|
marker=uuids.invalid_marker)
|
|
req = fakes.HTTPRequest.blank(
|
|
'/os-migrations?marker=%s' % uuids.invalid_marker,
|
|
version=self.wsgi_api_version, use_admin_context=True)
|
|
e = self.assertRaises(exc.HTTPBadRequest,
|
|
self.controller.index, req)
|
|
self.assertEqual(
|
|
"Marker %s could not be found." % uuids.invalid_marker,
|
|
six.text_type(e))
|
|
|
|
def test_index_with_invalid_limit(self):
|
|
"""Tests detail paging with an invalid limit."""
|
|
req = fakes.HTTPRequest.blank(
|
|
'/os-migrations?limit=x', version=self.wsgi_api_version,
|
|
use_admin_context=True)
|
|
self.assertRaises(exception.ValidationError,
|
|
self.controller.index, req)
|
|
req = fakes.HTTPRequest.blank(
|
|
'/os-migrations?limit=-1', version=self.wsgi_api_version,
|
|
use_admin_context=True)
|
|
self.assertRaises(exception.ValidationError,
|
|
self.controller.index, req)
|
|
|
|
def test_index_with_invalid_changes_since(self):
|
|
"""Tests detail paging with an invalid changes-since value."""
|
|
req = fakes.HTTPRequest.blank(
|
|
'/os-migrations?changes-since=wrong_time',
|
|
version=self.wsgi_api_version, use_admin_context=True)
|
|
self.assertRaises(exception.ValidationError,
|
|
self.controller.index, req)
|
|
|
|
def test_index_with_unknown_query_param(self):
|
|
"""Tests detail paging with an unknown query parameter."""
|
|
req = fakes.HTTPRequest.blank(
|
|
'/os-migrations?foo=bar',
|
|
version=self.wsgi_api_version, use_admin_context=True)
|
|
ex = self.assertRaises(exception.ValidationError,
|
|
self.controller.index, req)
|
|
self.assertIn('Additional properties are not allowed',
|
|
six.text_type(ex))
|
|
|
|
@mock.patch('nova.compute.api.API.get_migrations',
|
|
return_value=objects.MigrationList())
|
|
def test_index_with_changes_since_old_microversion(self, get_migrations):
|
|
"""Tests that the changes-since query parameteris ignored before
|
|
microversion 2.59.
|
|
"""
|
|
# Also use a valid filter (instance_uuid) to make sure only
|
|
# changes-since is removed.
|
|
req = fakes.HTTPRequest.blank(
|
|
'/os-migrations?changes-since=2018-01-10T16:59:24.138939&'
|
|
'instance_uuid=%s' % uuids.instance_uuid,
|
|
version='2.58', use_admin_context=True)
|
|
result = self.controller.index(req)
|
|
self.assertEqual({'migrations': []}, result)
|
|
get_migrations.assert_called_once_with(
|
|
req.environ['nova.context'],
|
|
{'instance_uuid': uuids.instance_uuid})
|
|
|
|
|
|
class MigrationsPolicyEnforcement(test.NoDBTestCase):
|
|
def setUp(self):
|
|
super(MigrationsPolicyEnforcement, self).setUp()
|
|
self.controller = migrations_v21.MigrationsController()
|
|
self.req = fakes.HTTPRequest.blank('')
|
|
|
|
def test_list_policy_failed(self):
|
|
rule_name = "os_compute_api:os-migrations:index"
|
|
self.policy.set_rules({rule_name: "project_id:non_fake"})
|
|
exc = self.assertRaises(
|
|
exception.PolicyNotAuthorized,
|
|
self.controller.index, self.req)
|
|
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)
|
|
|
|
|
|
class MigrationsPolicyEnforcementV259(MigrationsPolicyEnforcementV223):
|
|
wsgi_api_version = '2.59'
|