| ######### |
| # Copyright (c) 2014 GigaSpaces Technologies Ltd. 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. |
| |
| from os import path |
| import tempfile |
| |
| import unittest |
| import mock |
| |
| import nova_plugin |
| from cloudify.test_utils import workflow_test |
| |
| from openstack_plugin_common import NeutronClientWithSugar, \ |
| OPENSTACK_TYPE_PROPERTY, OPENSTACK_ID_PROPERTY |
| from neutron_plugin.network import NETWORK_OPENSTACK_TYPE |
| from neutron_plugin.port import PORT_OPENSTACK_TYPE |
| from nova_plugin.tests.test_relationships import RelationshipsTestBase |
| from nova_plugin.server import _prepare_server_nics |
| from cinder_plugin.volume import VOLUME_OPENSTACK_TYPE |
| from cloudify.exceptions import NonRecoverableError |
| from cloudify.state import current_ctx |
| |
| from cloudify.utils import setup_logger |
| |
| from cloudify.mocks import ( |
| MockNodeContext, |
| MockCloudifyContext, |
| MockNodeInstanceContext, |
| MockRelationshipContext, |
| MockRelationshipSubjectContext |
| ) |
| |
| |
| class TestServer(unittest.TestCase): |
| |
| blueprint_path = path.join('resources', |
| 'test-start-operation-retry-blueprint.yaml') |
| |
| @mock.patch('nova_plugin.server.create') |
| @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties') |
| @workflow_test(blueprint_path, copy_plugin_yaml=True) |
| def test_nova_server_lifecycle_start(self, cfy_local, *_): |
| |
| test_vars = { |
| 'counter': 0, |
| 'server': mock.MagicMock() |
| } |
| |
| def mock_get_server_by_context(*_): |
| s = test_vars['server'] |
| if test_vars['counter'] == 0: |
| s.status = nova_plugin.server.SERVER_STATUS_BUILD |
| else: |
| s.status = nova_plugin.server.SERVER_STATUS_ACTIVE |
| test_vars['counter'] += 1 |
| return s |
| |
| with mock.patch('nova_plugin.server.get_server_by_context', |
| new=mock_get_server_by_context): |
| cfy_local.execute('install', task_retries=3) |
| |
| self.assertEqual(2, test_vars['counter']) |
| self.assertEqual(0, test_vars['server'].start.call_count) |
| |
| @workflow_test(blueprint_path, copy_plugin_yaml=True) |
| @mock.patch('nova_plugin.server.create') |
| @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties') |
| def test_nova_server_lifecycle_start_after_stop(self, cfy_local, *_): |
| |
| test_vars = { |
| 'counter': 0, |
| 'server': mock.MagicMock() |
| } |
| |
| def mock_get_server_by_context(_): |
| s = test_vars['server'] |
| if test_vars['counter'] == 0: |
| s.status = nova_plugin.server.SERVER_STATUS_SHUTOFF |
| elif test_vars['counter'] == 1: |
| setattr(s, |
| nova_plugin.server.OS_EXT_STS_TASK_STATE, |
| nova_plugin.server.SERVER_TASK_STATE_POWERING_ON) |
| else: |
| s.status = nova_plugin.server.SERVER_STATUS_ACTIVE |
| test_vars['counter'] += 1 |
| test_vars['server'] = s |
| return s |
| |
| with mock.patch('nova_plugin.server.get_server_by_context', |
| new=mock_get_server_by_context): |
| cfy_local.execute('install', task_retries=3) |
| |
| self.assertEqual(1, test_vars['server'].start.call_count) |
| self.assertEqual(3, test_vars['counter']) |
| |
| @workflow_test(blueprint_path, copy_plugin_yaml=True) |
| @mock.patch('nova_plugin.server.create') |
| @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties') |
| def test_nova_server_lifecycle_start_unknown_status(self, cfy_local, *_): |
| test_vars = { |
| 'counter': 0, |
| 'server': mock.MagicMock() |
| } |
| |
| def mock_get_server_by_context(_): |
| s = test_vars['server'] |
| if test_vars['counter'] == 0: |
| s.status = '### unknown-status ###' |
| test_vars['counter'] += 1 |
| test_vars['server'] = s |
| return s |
| |
| with mock.patch('nova_plugin.server.get_server_by_context', |
| new=mock_get_server_by_context): |
| self.assertRaisesRegexp(RuntimeError, |
| 'Unexpected server state', |
| cfy_local.execute, |
| 'install') |
| |
| self.assertEqual(0, test_vars['server'].start.call_count) |
| self.assertEqual(1, test_vars['counter']) |
| |
| @workflow_test(blueprint_path, copy_plugin_yaml=True) |
| @mock.patch('nova_plugin.server.start') |
| @mock.patch('nova_plugin.server._handle_image_or_flavor') |
| @mock.patch('nova_plugin.server._fail_on_missing_required_parameters') |
| @mock.patch('openstack_plugin_common.nova_client') |
| def test_nova_server_creation_param_integrity( |
| self, cfy_local, mock_nova, *args): |
| cfy_local.execute('install', task_retries=0) |
| calls = mock_nova.Client.return_value.servers.method_calls |
| self.assertEqual(1, len(calls)) |
| kws = calls[0][2] |
| self.assertIn('scheduler_hints', kws) |
| self.assertEqual(kws['scheduler_hints'], |
| {'group': 'affinity-group-id'}, |
| 'expecting \'scheduler_hints\' value to exist') |
| |
| @workflow_test(blueprint_path, copy_plugin_yaml=True, |
| inputs={'use_password': True}) |
| @mock.patch('nova_plugin.server.create') |
| @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties') |
| @mock.patch( |
| 'nova_plugin.server.get_single_connected_node_by_openstack_type', |
| autospec=True, return_value=None) |
| def test_nova_server_with_use_password(self, cfy_local, *_): |
| |
| test_vars = { |
| 'counter': 0, |
| 'server': mock.MagicMock() |
| } |
| |
| tmp_path = tempfile.NamedTemporaryFile(prefix='key_name') |
| key_path = tmp_path.name |
| |
| def mock_get_server_by_context(_): |
| s = test_vars['server'] |
| if test_vars['counter'] == 0: |
| s.status = nova_plugin.server.SERVER_STATUS_BUILD |
| else: |
| s.status = nova_plugin.server.SERVER_STATUS_ACTIVE |
| test_vars['counter'] += 1 |
| |
| def check_agent_key_path(private_key): |
| self.assertEqual(private_key, key_path) |
| return private_key |
| |
| s.get_password = check_agent_key_path |
| return s |
| |
| with mock.patch('nova_plugin.server.get_server_by_context', |
| mock_get_server_by_context): |
| with mock.patch( |
| 'cloudify.context.BootstrapContext.' |
| 'CloudifyAgent.agent_key_path', |
| new_callable=mock.PropertyMock, return_value=key_path): |
| cfy_local.execute('install', task_retries=5) |
| |
| |
| class TestMergeNICs(unittest.TestCase): |
| def test_merge_prepends_management_network(self): |
| """When the mgmt network isnt in a relationship, its the 1st nic.""" |
| mgmt_network_id = 'management network' |
| nics = [{'net-id': 'other network'}] |
| |
| merged = nova_plugin.server._merge_nics(mgmt_network_id, nics) |
| |
| self.assertEqual(len(merged), 2) |
| self.assertEqual(merged[0]['net-id'], 'management network') |
| |
| def test_management_network_in_relationships(self): |
| """When the mgmt network was in a relationship, it's not prepended.""" |
| mgmt_network_id = 'management network' |
| nics = [{'net-id': 'other network'}, {'net-id': 'management network'}] |
| |
| merged = nova_plugin.server._merge_nics(mgmt_network_id, nics) |
| |
| self.assertEqual(nics, merged) |
| |
| |
| class TestNormalizeNICs(unittest.TestCase): |
| def test_normalize_port_priority(self): |
| """Whe there's both net-id and port-id, port-id is used.""" |
| nics = [{'net-id': '1'}, {'port-id': '2'}, {'net-id': 3, 'port-id': 4}] |
| normalized = nova_plugin.server._normalize_nics(nics) |
| expected = [{'net-id': '1'}, {'port-id': '2'}, {'port-id': 4}] |
| self.assertEqual(expected, normalized) |
| |
| |
| class MockNeutronClient(NeutronClientWithSugar): |
| """A fake neutron client with hard-coded test data.""" |
| |
| @mock.patch('openstack_plugin_common.OpenStackClient.__init__', |
| new=mock.Mock()) |
| def __init__(self): |
| super(MockNeutronClient, self).__init__() |
| |
| @staticmethod |
| def _search_filter(objs, search_params): |
| """Mock neutron's filtering by attributes in list_* methods. |
| |
| list_* methods (list_networks, list_ports) |
| """ |
| def _matches(obj, search_params): |
| return all(obj[k] == v for k, v in search_params.items()) |
| return [obj for obj in objs if _matches(obj, search_params)] |
| |
| def list_networks(self, **search_params): |
| networks = [ |
| {'name': 'network1', 'id': '1'}, |
| {'name': 'network2', 'id': '2'}, |
| {'name': 'network3', 'id': '3'}, |
| {'name': 'network4', 'id': '4'}, |
| {'name': 'network5', 'id': '5'}, |
| {'name': 'network6', 'id': '6'}, |
| {'name': 'other', 'id': 'other'} |
| ] |
| return {'networks': self._search_filter(networks, search_params)} |
| |
| def list_ports(self, **search_params): |
| ports = [ |
| {'name': 'port1', 'id': '1', 'network_id': '1'}, |
| {'name': 'port2', 'id': '2', 'network_id': '1'}, |
| {'name': 'port3', 'id': '3', 'network_id': '2'}, |
| {'name': 'port4', 'id': '4', 'network_id': '2'}, |
| ] |
| return {'ports': self._search_filter(ports, search_params)} |
| |
| def show_port(self, port_id): |
| ports = self.list_ports(id=port_id) |
| return {'port': ports['ports'][0]} |
| |
| |
| class NICTestBase(RelationshipsTestBase): |
| """Base test class for the NICs tests. |
| |
| It comes with helper methods to create a mock cloudify context, with |
| the specified relationships. |
| """ |
| mock_neutron = MockNeutronClient() |
| |
| def _relationship_spec(self, obj, objtype): |
| return {'node': {'properties': obj}, |
| 'instance': { |
| 'runtime_properties': {OPENSTACK_TYPE_PROPERTY: objtype, |
| OPENSTACK_ID_PROPERTY: obj['id']}}} |
| |
| def _make_vm_ctx_with_ports(self, management_network_name, ports): |
| port_specs = [self._relationship_spec(obj, PORT_OPENSTACK_TYPE) |
| for obj in ports] |
| vm_properties = {'management_network_name': management_network_name} |
| return self._make_vm_ctx_with_relationships(port_specs, |
| vm_properties) |
| |
| def _make_vm_ctx_with_networks(self, management_network_name, networks): |
| network_specs = [self._relationship_spec(obj, NETWORK_OPENSTACK_TYPE) |
| for obj in networks] |
| vm_properties = {'management_network_name': management_network_name} |
| return self._make_vm_ctx_with_relationships(network_specs, |
| vm_properties) |
| |
| |
| class TestServerNICs(NICTestBase): |
| """Test preparing the NICs list from server<->network relationships. |
| |
| Each test creates a cloudify context that represents a openstack VM |
| with relationships to networks. Then, examine the NICs list produced from |
| the relationships. |
| """ |
| def test_nova_server_creation_nics_ordering(self): |
| """NIC list keeps the order of the relationships. |
| |
| The nics= list passed to nova.server.create should be ordered |
| depending on the relationships to the networks (as defined in the |
| blueprint). |
| """ |
| ctx = self._make_vm_ctx_with_networks( |
| management_network_name='network1', |
| networks=[ |
| {'id': '1'}, |
| {'id': '2'}, |
| {'id': '3'}, |
| {'id': '4'}, |
| {'id': '5'}, |
| {'id': '6'}, |
| ]) |
| server = {'meta': {}} |
| |
| _prepare_server_nics( |
| self.mock_neutron, ctx, server) |
| |
| self.assertEqual( |
| ['1', '2', '3', '4', '5', '6'], |
| [n['net-id'] for n in server['nics']]) |
| |
| def test_server_creation_prepends_mgmt_network(self): |
| """If the management network isn't in a relation, it's the first NIC. |
| |
| Creating the server examines the relationships, and if it doesn't find |
| a relationship to the management network, it adds the management |
| network to the NICs list, as the first element. |
| """ |
| ctx = self._make_vm_ctx_with_networks( |
| management_network_name='other', |
| networks=[ |
| {'id': '1'}, |
| {'id': '2'}, |
| {'id': '3'}, |
| {'id': '4'}, |
| {'id': '5'}, |
| {'id': '6'}, |
| ]) |
| server = {'meta': {}} |
| |
| _prepare_server_nics( |
| self.mock_neutron, ctx, server) |
| |
| first_nic = server['nics'][0] |
| self.assertEqual('other', first_nic['net-id']) |
| self.assertEqual(7, len(server['nics'])) |
| |
| def test_server_creation_uses_relation_mgmt_nic(self): |
| """If the management network is in a relation, it isn't prepended. |
| |
| If the server has a relationship to the management network, |
| a new NIC isn't prepended to the list. |
| """ |
| ctx = self._make_vm_ctx_with_networks( |
| management_network_name='network1', |
| networks=[ |
| {'id': '1'}, |
| {'id': '2'}, |
| {'id': '3'}, |
| {'id': '4'}, |
| {'id': '5'}, |
| {'id': '6'}, |
| ]) |
| server = {'meta': {}} |
| |
| _prepare_server_nics( |
| self.mock_neutron, ctx, server) |
| self.assertEqual(6, len(server['nics'])) |
| |
| |
| class TestServerPortNICs(NICTestBase): |
| """Test preparing the NICs list from server<->port relationships. |
| |
| Create a cloudify ctx representing a vm with relationships to |
| openstack ports. Then examine the resulting NICs list: check that it |
| contains the networks that the ports were connected to, and that each |
| connection uses the port that was provided. |
| """ |
| |
| def test_network_with_port(self): |
| """Port on the management network is used to connect to it. |
| |
| The NICs list entry for the management network contains the |
| port-id of the port from the relationship, but doesn't contain net-id. |
| """ |
| ports = [{'id': '1'}] |
| ctx = self._make_vm_ctx_with_ports('network1', ports) |
| server = {'meta': {}} |
| |
| _prepare_server_nics( |
| self.mock_neutron, ctx, server) |
| |
| self.assertEqual([{'port-id': '1'}], server['nics']) |
| |
| def test_port_not_to_mgmt_network(self): |
| """A NICs list entry is added with the network and the port. |
| |
| A relationship to a port must not only add a NIC, but the NIC must |
| also make sure to use that port. |
| """ |
| ports = [{'id': '1'}] |
| ctx = self._make_vm_ctx_with_ports('other', ports) |
| server = {'meta': {}} |
| |
| _prepare_server_nics( |
| self.mock_neutron, ctx, server) |
| expected = [ |
| {'net-id': 'other'}, |
| {'port-id': '1'} |
| ] |
| self.assertEqual(expected, server['nics']) |
| |
| |
| class TestBootFromVolume(unittest.TestCase): |
| |
| @mock.patch('nova_plugin.server._get_boot_volume_relationships', |
| autospec=True) |
| def test_handle_boot_volume(self, mock_get_rels): |
| mock_get_rels.return_value.runtime_properties = { |
| 'external_id': 'test-id', |
| 'availability_zone': 'test-az', |
| } |
| server = {} |
| ctx = mock.MagicMock() |
| nova_plugin.server._handle_boot_volume(server, ctx) |
| self.assertEqual({'vda': 'test-id:::0'}, |
| server['block_device_mapping']) |
| self.assertEqual('test-az', |
| server['availability_zone']) |
| |
| @mock.patch('nova_plugin.server._get_boot_volume_relationships', |
| autospec=True, return_value=[]) |
| def test_handle_boot_volume_no_boot_volume(self, *_): |
| server = {} |
| ctx = mock.MagicMock() |
| nova_plugin.server._handle_boot_volume(server, ctx) |
| self.assertNotIn('block_device_mapping', server) |
| |
| |
| class TestImageFromRelationships(unittest.TestCase): |
| |
| @mock.patch('glance_plugin.image.' |
| 'get_openstack_ids_of_connected_nodes_by_openstack_type', |
| autospec=True, return_value=['test-id']) |
| def test_handle_boot_image(self, *_): |
| server = {} |
| ctx = mock.MagicMock() |
| nova_plugin.server.handle_image_from_relationship(server, 'image', ctx) |
| self.assertEqual({'image': 'test-id'}, server) |
| |
| @mock.patch('glance_plugin.image.' |
| 'get_openstack_ids_of_connected_nodes_by_openstack_type', |
| autospec=True, return_value=[]) |
| def test_handle_boot_image_no_image(self, *_): |
| server = {} |
| ctx = mock.MagicMock() |
| nova_plugin.server.handle_image_from_relationship(server, 'image', ctx) |
| self.assertNotIn('image', server) |
| |
| |
| class TestServerRelationships(unittest.TestCase): |
| |
| def _get_ctx_mock(self, instance_id, boot): |
| rel_specs = [MockRelationshipContext( |
| target=MockRelationshipSubjectContext(node=MockNodeContext( |
| properties={'boot': boot}), instance=MockNodeInstanceContext( |
| runtime_properties={ |
| OPENSTACK_TYPE_PROPERTY: VOLUME_OPENSTACK_TYPE, |
| OPENSTACK_ID_PROPERTY: instance_id |
| })))] |
| ctx = mock.MagicMock() |
| ctx.instance = MockNodeInstanceContext(relationships=rel_specs) |
| ctx.logger = setup_logger('mock-logger') |
| return ctx |
| |
| def test_boot_volume_relationship(self): |
| instance_id = 'test-id' |
| ctx = self._get_ctx_mock(instance_id, True) |
| result = nova_plugin.server._get_boot_volume_relationships( |
| VOLUME_OPENSTACK_TYPE, ctx) |
| self.assertEqual( |
| instance_id, |
| result.runtime_properties['external_id']) |
| |
| def test_no_boot_volume_relationship(self): |
| instance_id = 'test-id' |
| ctx = self._get_ctx_mock(instance_id, False) |
| result = nova_plugin.server._get_boot_volume_relationships( |
| VOLUME_OPENSTACK_TYPE, ctx) |
| self.assertFalse(result) |
| |
| |
| class TestServerNetworkRuntimeProperties(unittest.TestCase): |
| |
| @property |
| def mock_ctx(self): |
| return MockCloudifyContext( |
| node_id='test', |
| deployment_id='test', |
| properties={}, |
| operation={'retry_number': 0}, |
| provider_context={'resources': {}} |
| ) |
| |
| def test_server_networks_runtime_properties_empty_server(self): |
| ctx = self.mock_ctx |
| current_ctx.set(ctx=ctx) |
| server = mock.MagicMock() |
| setattr(server, 'networks', {}) |
| with self.assertRaisesRegexp( |
| NonRecoverableError, |
| 'The server was created but not attached to a network.'): |
| nova_plugin.server._set_network_and_ip_runtime_properties(server) |
| |
| def test_server_networks_runtime_properties_valid_networks(self): |
| ctx = self.mock_ctx |
| current_ctx.set(ctx=ctx) |
| server = mock.MagicMock() |
| network_id = 'management_network' |
| network_ips = ['good', 'bad1', 'bad2'] |
| setattr(server, |
| 'networks', |
| {network_id: network_ips}) |
| nova_plugin.server._set_network_and_ip_runtime_properties(server) |
| self.assertIn('networks', ctx.instance.runtime_properties.keys()) |
| self.assertIn('ip', ctx.instance.runtime_properties.keys()) |
| self.assertEquals(ctx.instance.runtime_properties['ip'], 'good') |
| self.assertEquals(ctx.instance.runtime_properties['networks'], |
| {network_id: network_ips}) |
| |
| def test_server_networks_runtime_properties_empty_networks(self): |
| ctx = self.mock_ctx |
| current_ctx.set(ctx=ctx) |
| server = mock.MagicMock() |
| network_id = 'management_network' |
| network_ips = [] |
| setattr(server, |
| 'networks', |
| {network_id: network_ips}) |
| nova_plugin.server._set_network_and_ip_runtime_properties(server) |
| self.assertIn('networks', ctx.instance.runtime_properties.keys()) |
| self.assertIn('ip', ctx.instance.runtime_properties.keys()) |
| self.assertEquals(ctx.instance.runtime_properties['ip'], None) |
| self.assertEquals(ctx.instance.runtime_properties['networks'], |
| {network_id: network_ips}) |