From d5f2944f4c9e693b4016b5039002ca1438bac148 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Tue, 6 Dec 2016 10:27:36 +0800 Subject: [PATCH] Supports string values 'none' and 'auto' for networks Supports to specify string values 'none' and 'auto' for networks while creating nova server. auto: automatically allocate network resources if none are available; none: do not attach a NIC at all. Nova supports these string values since api version '2.37'. Change-Id: If65f193dfdaedec3e549a8bd7b39e5f1c02be658 Blueprint: support-auto-none-special-network Closes-Bug: #1646360 --- heat/common/exception.py | 4 + heat/engine/clients/os/nova.py | 4 +- .../engine/resources/openstack/nova/server.py | 80 ++++++-- .../openstack/nova/server_network_mixin.py | 133 ++++++++++--- heat/tests/openstack/nova/test_server.py | 188 ++++++++++++++++-- 5 files changed, 349 insertions(+), 60 deletions(-) diff --git a/heat/common/exception.py b/heat/common/exception.py index c6cc51f0ab..eb9165279c 100644 --- a/heat/common/exception.py +++ b/heat/common/exception.py @@ -530,3 +530,7 @@ class InvalidServiceVersion(HeatException): class InvalidTemplateVersions(HeatException): msg_fmt = _('A template version alias %(version)s was added for a ' 'template class that has no official YYYY-MM-DD version.') + + +class UnableToAutoAllocateNetwork(HeatException): + msg_fmt = _('Unable to automatically allocate a network: %(message)s') diff --git a/heat/engine/clients/os/nova.py b/heat/engine/clients/os/nova.py index 2a73ac2233..6886e4e119 100644 --- a/heat/engine/clients/os/nova.py +++ b/heat/engine/clients/os/nova.py @@ -59,9 +59,9 @@ class NovaClientPlugin(client_plugin.ClientPlugin): NOVA_API_VERSION = '2.1' validate_versions = [ - V2_2, V2_8, V2_10, V2_15, V2_26 + V2_2, V2_8, V2_10, V2_15, V2_26, V2_37 ] = [ - '2.2', '2.8', '2.10', '2.15', '2.26' + '2.2', '2.8', '2.10', '2.15', '2.26', '2.37' ] supported_versions = [NOVA_API_VERSION] + validate_versions diff --git a/heat/engine/resources/openstack/nova/server.py b/heat/engine/resources/openstack/nova/server.py index 6a44d13c6b..adeaf0568d 100644 --- a/heat/engine/resources/openstack/nova/server.py +++ b/heat/engine/resources/openstack/nova/server.py @@ -109,10 +109,12 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin, _NETWORK_KEYS = ( NETWORK_UUID, NETWORK_ID, NETWORK_FIXED_IP, NETWORK_PORT, - NETWORK_SUBNET, NETWORK_PORT_EXTRA, NETWORK_FLOATING_IP + NETWORK_SUBNET, NETWORK_PORT_EXTRA, NETWORK_FLOATING_IP, + ALLOCATE_NETWORK, ) = ( 'uuid', 'network', 'fixed_ip', 'port', - 'subnet', 'port_extra_properties', 'floating_ip' + 'subnet', 'port_extra_properties', 'floating_ip', + 'allocate_network', ) _SOFTWARE_CONFIG_FORMATS = ( @@ -127,6 +129,12 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin, 'POLL_SERVER_CFN', 'POLL_SERVER_HEAT', 'POLL_TEMP_URL', 'ZAQAR_MESSAGE' ) + _ALLOCATE_TYPES = ( + NETWORK_NONE, NETWORK_AUTO, + ) = ( + 'none', 'auto', + ) + ATTRIBUTES = ( NAME_ATTR, ADDRESSES, NETWORKS_ATTR, FIRST_ADDRESS, INSTANCE_NAME, ACCESSIPV4, ACCESSIPV6, CONSOLE_URLS, TAGS_ATTR @@ -405,6 +413,23 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin, constraints.CustomConstraint('neutron.network') ] ), + ALLOCATE_NETWORK: properties.Schema( + properties.Schema.STRING, + _('The special string values of network, ' + 'auto: means either a network that is already ' + 'available to the project will be used, or if one ' + 'does not exist, will be automatically created for ' + 'the project; none: means no networking will be ' + 'allocated for the created server. Supported by ' + 'Nova API since version "2.37". This property can ' + 'not be used with other network keys.'), + support_status=support.SupportStatus(version='9.0.0'), + constraints=[ + constraints.AllowedValues( + [NETWORK_NONE, NETWORK_AUTO]) + ], + update_allowed=True, + ), NETWORK_FIXED_IP: properties.Schema( properties.Schema.STRING, _('Fixed IP address to specify for the port ' @@ -754,7 +779,15 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin, server = None try: - server = self.client().servers.create( + # if 'auto' or 'none' is specified, we get the string type + # nics after self._build_nics(), and the string network + # is supported since nova microversion 2.37 + if isinstance(nics, six.string_types): + nc = self.client(version=self.client_plugin().V2_37) + else: + nc = self.client() + + server = nc.servers.create( name=self._server_name(), image=image, flavor=flavor, @@ -1370,16 +1403,38 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin, if image: self._validate_image_flavor(image, flavor) - # network properties 'uuid' and 'network' shouldn't be used - # both at once for all networks networks = self.properties[self.NETWORKS] or [] - # record if any networks include explicit ports - networks_with_port = False for network in networks: - networks_with_port = (networks_with_port or - network.get(self.NETWORK_PORT) is not None) self._validate_network(network) + has_str_net = self._str_network(networks) is not None + if has_str_net: + if len(networks) != 1: + msg = _('Property "%s" can not be specified if ' + 'multiple network interfaces set for ' + 'server.') % self.ALLOCATE_NETWORK + raise exception.StackValidationFailed(message=msg) + # Check if str_network is allowed to use + try: + self.client( + version=self.client_plugin().V2_37) + except exception.InvalidServiceVersion as ex: + msg = (_('Cannot use "%(prop)s" property - compute service ' + 'does not support the required api ' + 'microversion: %(ex)s') + % {'prop': self.ALLOCATE_NETWORK, + 'ex': six.text_type(ex)}) + raise exception.StackValidationFailed(message=msg) + + # record if any networks include explicit ports + has_port = any(n[self.NETWORK_PORT] is not None for n in networks) + # if 'security_groups' present for the server and explicit 'port' + # in one or more entries in 'networks', raise validation error + if has_port and self.properties[self.SECURITY_GROUPS]: + raise exception.ResourcePropertyConflict( + self.SECURITY_GROUPS, + "/".join([self.NETWORKS, self.NETWORK_PORT])) + # Check if tags is allowed to use if self.properties[self.TAGS]: try: @@ -1396,13 +1451,6 @@ class Server(server_base.BaseServer, sh.SchedulerHintsMixin, if metadata or personality: limits = self.client_plugin().absolute_limits() - # if 'security_groups' present for the server and explicit 'port' - # in one or more entries in 'networks', raise validation error - if networks_with_port and self.properties[self.SECURITY_GROUPS]: - raise exception.ResourcePropertyConflict( - self.SECURITY_GROUPS, - "/".join([self.NETWORKS, self.NETWORK_PORT])) - # verify that the number of metadata entries is not greater # than the maximum number allowed in the provider's absolute # limits diff --git a/heat/engine/resources/openstack/nova/server_network_mixin.py b/heat/engine/resources/openstack/nova/server_network_mixin.py index 88c5778f1b..a6eaeb62f8 100644 --- a/heat/engine/resources/openstack/nova/server_network_mixin.py +++ b/heat/engine/resources/openstack/nova/server_network_mixin.py @@ -35,16 +35,28 @@ class ServerNetworkMixin(object): subnet = network.get(self.NETWORK_SUBNET) fixed_ip = network.get(self.NETWORK_FIXED_IP) floating_ip = network.get(self.NETWORK_FLOATING_IP) + str_network = network.get(self.ALLOCATE_NETWORK) - if net_id is None and port is None and subnet is None: - msg = _('One of the properties "%(id)s", "%(port_id)s" ' - 'or "%(subnet)s" should be set for the ' + if (net_id is None and + port is None and + subnet is None and + not str_network): + msg = _('One of the properties "%(id)s", "%(port_id)s", ' + '"%(str_network)s" or "%(subnet)s" should be set for the ' 'specified network of server "%(server)s".' '') % dict(id=self.NETWORK_ID, port_id=self.NETWORK_PORT, subnet=self.NETWORK_SUBNET, + str_network=self.ALLOCATE_NETWORK, server=self.name) raise exception.StackValidationFailed(message=msg) + # can not specify str_network with other keys of networks + # at the same time + has_value_keys = [k for k, v in network.items() if v is not None] + if str_network and len(has_value_keys) != 1: + msg = _('Can not specify "%s" with other keys of networks ' + 'at the same time.') % self.ALLOCATE_NETWORK + raise exception.StackValidationFailed(message=msg) if port is not None and not self.is_using_neutron(): msg = _('Property "%s" is supported only for ' @@ -214,6 +226,11 @@ class ServerNetworkMixin(object): def _build_nics(self, networks, security_groups=None): if not networks: return None + + str_network = self._str_network(networks) + if str_network: + return str_network + nics = [] for idx, net in enumerate(networks): @@ -329,8 +346,42 @@ class ServerNetworkMixin(object): if net is not None: net['port'] = props['port'] - def calculate_networks(self, old_nets, new_nets, ifaces, - security_groups=None): + def _get_available_networks(self): + # first we get the private networks owned by the tenant + search_opts = {'tenant_id': self.context.tenant_id, 'shared': False, + 'admin_state_up': True, } + nc = self.client('neutron') + nets = nc.list_networks(**search_opts).get('networks', []) + # second we get the public shared networks + search_opts = {'shared': True} + nets += nc.list_networks(**search_opts).get('networks', []) + + ids = [net['id'] for net in nets] + + return ids + + def _auto_allocate_network(self): + topology = self.client('neutron').get_auto_allocated_topology( + self.context.tenant_id)['auto_allocated_topology'] + + return topology['id'] + + def _calculate_using_str_network(self, ifaces, str_net): + add_nets = [] + remove_ports = [iface.port_id for iface in ifaces or []] + if str_net == self.NETWORK_AUTO: + nets = self._get_available_networks() + if not nets: + nets = [self._auto_allocate_network()] + if len(nets) > 1: + msg = 'Multiple possible networks found.' + raise exception.UnableToAutoAllocateNetwork(message=msg) + + add_nets.append({'port_id': None, 'net_id': nets[0], 'fip': None}) + return remove_ports, add_nets + + def _calculate_using_list_networks(self, old_nets, new_nets, ifaces, + security_groups): remove_ports = [] add_nets = [] attach_first_free_port = False @@ -351,33 +402,38 @@ class ServerNetworkMixin(object): # 3. detach unmatched networks, which were present in old_nets # 4. attach unmatched networks, which were present in new_nets else: - # remove not updated networks from old and new networks lists, - # also get list these networks - not_updated_nets = self._exclude_not_updated_networks(old_nets, - new_nets) + # if old net is string net, remove the interfaces + if self._str_network(old_nets): + remove_ports = [iface.port_id for iface in ifaces or []] + else: + # remove not updated networks from old and new networks lists, + # also get list these networks + not_updated_nets = self._exclude_not_updated_networks( + old_nets, + new_nets) - self.update_networks_matching_iface_port( - old_nets + not_updated_nets, ifaces) + self.update_networks_matching_iface_port( + old_nets + not_updated_nets, ifaces) - # according to nova interface-detach command detached port - # will be deleted - inter_port_data = self._data_get_ports() - inter_port_ids = [p['id'] for p in inter_port_data] - for net in old_nets: - port_id = net.get(self.NETWORK_PORT) - # we can't match the port for some user case, like: - # the internal port was detached in nova first, then - # user update template to detach this nic. The internal - # port will remains till we delete the server resource. - if port_id: - remove_ports.append(port_id) - if port_id in inter_port_ids: - # if we have internal port with such id, remove it - # instantly. - self._delete_internal_port(port_id) - if net.get(self.NETWORK_FLOATING_IP): - self._floating_ip_disassociate( - net.get(self.NETWORK_FLOATING_IP)) + # according to nova interface-detach command detached port + # will be deleted + inter_port_data = self._data_get_ports() + inter_port_ids = [p['id'] for p in inter_port_data] + for net in old_nets: + port_id = net.get(self.NETWORK_PORT) + # we can't match the port for some user case, like: + # the internal port was detached in nova first, then + # user update template to detach this nic. The internal + # port will remains till we delete the server resource. + if port_id: + remove_ports.append(port_id) + if port_id in inter_port_ids: + # if we have internal port with such id, remove it + # instantly. + self._delete_internal_port(port_id) + if net.get(self.NETWORK_FLOATING_IP): + self._floating_ip_disassociate( + net.get(self.NETWORK_FLOATING_IP)) handler_kwargs = {'port_id': None, 'net_id': None, 'fip': None} # if new_nets is None, we should attach first free port, @@ -415,6 +471,23 @@ class ServerNetworkMixin(object): return remove_ports, add_nets + def _str_network(self, networks): + # if user specify 'allocate_network', return it + # otherwise we return None + for net in networks or []: + str_net = net.get(self.ALLOCATE_NETWORK) + if str_net: + return str_net + + def calculate_networks(self, old_nets, new_nets, ifaces, + security_groups=None): + new_str_net = self._str_network(new_nets) + if new_str_net: + return self._calculate_using_str_network(ifaces, new_str_net) + else: + return self._calculate_using_list_networks( + old_nets, new_nets, ifaces, security_groups) + def update_floating_ip_association(self, floating_ip, flip_associate): if self.is_using_neutron() and flip_associate.get('port_id'): self._floating_ip_neutron_associate(floating_ip, flip_associate) diff --git a/heat/tests/openstack/nova/test_server.py b/heat/tests/openstack/nova/test_server.py index 992925f50a..3ec9e45b24 100644 --- a/heat/tests/openstack/nova/test_server.py +++ b/heat/tests/openstack/nova/test_server.py @@ -507,6 +507,31 @@ class ServersTest(common.HeatTestCase): args, kwargs = mock_create.call_args self.assertEqual({}, kwargs['meta']) + def test_server_create_with_str_network(self): + stack_name = 'server_with_str_network' + return_server = self.fc.servers.list()[1] + (tmpl, stack) = self._setup_test_stack(stack_name) + mock_nc = self.patchobject(nova.NovaClientPlugin, '_create', + return_value=self.fc) + self.patchobject(glance.GlanceClientPlugin, 'get_image', + return_value=self.mock_image) + self.patchobject(nova.NovaClientPlugin, 'get_flavor', + return_value=self.mock_flavor) + self.patchobject(neutron.NeutronClientPlugin, + 'find_resourceid_by_name_or_id') + + props = tmpl['Resources']['WebServer']['Properties'] + props['networks'] = [{'allocate_network': 'none'}] + resource_defns = tmpl.resource_definitions(stack) + server = servers.Server('WebServer', + resource_defns['WebServer'], stack) + self.patchobject(server, 'store_external_ports') + create_mock = self.patchobject(self.fc.servers, 'create', + return_value=return_server) + scheduler.TaskRunner(server.create)() + mock_nc.assert_called_with(version='2.37') + self.assertEqual('none', create_mock.call_args[1]['nics']) + def test_server_create_with_image_id(self): return_server = self.fc.servers.list()[1] return_server.id = '5678' @@ -1299,8 +1324,9 @@ class ServersTest(common.HeatTestCase): 'find_resourceid_by_name_or_id') ex = self.assertRaises(exception.StackValidationFailed, server.validate) - self.assertIn(_('One of the properties "network", "port" or "subnet" ' - 'should be set for the specified network of ' + self.assertIn(_('One of the properties "network", "port", ' + '"allocate_network" or "subnet" should be set ' + 'for the specified network of ' 'server "%s".') % server.name, six.text_type(ex)) @@ -1329,6 +1355,30 @@ class ServersTest(common.HeatTestCase): 'corresponding port can not be retrieved.'), six.text_type(ex)) + def test_server_validate_with_networks_str_net(self): + stack_name = 'srv_networks_str_nets' + (tmpl, stack) = self._setup_test_stack(stack_name) + # create a server with 'uuid' and 'network' properties + tmpl['Resources']['WebServer']['Properties']['networks'] = ( + [{'network': '6b1688bb-18a0-4754-ab05-19daaedc5871', + 'allocate_network': 'auto'}]) + self.patchobject(nova.NovaClientPlugin, '_create', + return_value=self.fc) + resource_defns = tmpl.resource_definitions(stack) + server = servers.Server('server_validate_net_list_str', + resource_defns['WebServer'], stack) + self.patchobject(glance.GlanceClientPlugin, 'get_image', + return_value=self.mock_image) + self.patchobject(nova.NovaClientPlugin, 'get_flavor', + return_value=self.mock_flavor) + self.patchobject(neutron.NeutronClientPlugin, + 'find_resourceid_by_name_or_id') + ex = self.assertRaises(exception.StackValidationFailed, + server.validate) + self.assertIn(_('Can not specify "allocate_network" with ' + 'other keys of networks at the same time.'), + six.text_type(ex)) + def test_server_validate_port_fixed_ip(self): stack_name = 'port_with_fixed_ip' (tmpl, stack) = self._setup_test_stack(stack_name, @@ -3140,10 +3190,12 @@ class ServersTest(common.HeatTestCase): def create_old_net(self, port=None, net=None, ip=None, uuid=None, subnet=None, - port_extra_properties=None, floating_ip=None): + port_extra_properties=None, floating_ip=None, + str_network=None): return {'port': port, 'network': net, 'fixed_ip': ip, 'uuid': uuid, 'subnet': subnet, 'floating_ip': floating_ip, - 'port_extra_properties': port_extra_properties} + 'port_extra_properties': port_extra_properties, + 'allocate_network': str_network} def create_fake_iface(self, port, net, ip): class fake_interface(object): @@ -3213,7 +3265,8 @@ class ServersTest(common.HeatTestCase): old_nets_copy = copy.deepcopy(old_nets) for net in new_nets_copy: for key in ('port', 'network', 'fixed_ip', 'uuid', 'subnet', - 'port_extra_properties', 'floating_ip'): + 'port_extra_properties', 'floating_ip', + 'allocate_network'): net.setdefault(key) matched_nets = server._exclude_not_updated_networks(old_nets, @@ -3244,7 +3297,8 @@ class ServersTest(common.HeatTestCase): old_nets_copy = copy.deepcopy(old_nets) for net in new_nets_copy: for key in ('port', 'network', 'fixed_ip', 'uuid', 'subnet', - 'port_extra_properties', 'floating_ip'): + 'port_extra_properties', 'floating_ip', + 'allocate_network'): net.setdefault(key) matched_nets = server._exclude_not_updated_networks(old_nets, new_nets) @@ -3268,7 +3322,8 @@ class ServersTest(common.HeatTestCase): 'subnet': None, 'uuid': None, 'port_extra_properties': None, - 'floating_ip': None}] + 'floating_ip': None, + 'allocate_network': None}] new_nets_copy = copy.deepcopy(new_nets) matched_nets = server._exclude_not_updated_networks(old_nets, new_nets) @@ -3311,35 +3366,40 @@ class ServersTest(common.HeatTestCase): 'subnet': None, 'floating_ip': None, 'port_extra_properties': None, - 'uuid': None}, + 'uuid': None, + 'allocate_network': None}, {'port': 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'network': 'gggggggg-1111-1111-1111-gggggggggggg', 'fixed_ip': '1.2.3.4', 'subnet': None, 'port_extra_properties': None, 'floating_ip': None, - 'uuid': None}, + 'uuid': None, + 'allocate_network': None}, {'port': 'cccccccc-cccc-cccc-cccc-cccccccccccc', 'network': 'gggggggg-1111-1111-1111-gggggggggggg', 'fixed_ip': None, 'subnet': None, 'port_extra_properties': None, 'floating_ip': None, - 'uuid': None}, + 'uuid': None, + 'allocate_network': None}, {'port': 'dddddddd-dddd-dddd-dddd-dddddddddddd', 'network': None, 'fixed_ip': None, 'subnet': None, 'port_extra_properties': None, 'floating_ip': None, - 'uuid': None}, + 'uuid': None, + 'allocate_network': None}, {'port': 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'uuid': 'gggggggg-1111-1111-1111-gggggggggggg', 'fixed_ip': '5.6.7.8', 'subnet': None, 'port_extra_properties': None, 'floating_ip': None, - 'network': None}] + 'network': None, + 'allocate_network': None}] self.patchobject(neutron.NeutronClientPlugin, 'find_resourceid_by_name_or_id', @@ -3514,6 +3574,110 @@ class ServersTest(common.HeatTestCase): self.assertEqual(1, mock_detach_check.call_count) self.assertEqual(1, mock_attach_check.call_count) + def _test_server_update_to_auto(self, available_multi_nets=None): + multi_nets = available_multi_nets or [] + return_server = self.fc.servers.list()[1] + return_server.id = '5678' + server = self._create_test_server(return_server, 'networks_update') + + old_networks = [ + {'port': '95e25541-d26a-478d-8f36-ae1c8f6b74dc'}] + + before_props = self.server_props.copy() + before_props['networks'] = old_networks + update_props = self.server_props.copy() + update_props['networks'] = [{'allocate_network': 'auto'}] + update_template = server.t.freeze(properties=update_props) + server.t = server.t.freeze(properties=before_props) + + self.patchobject(self.fc.servers, 'get', return_value=return_server) + poor_interfaces = [ + self.create_fake_iface('95e25541-d26a-478d-8f36-ae1c8f6b74dc', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '11.12.13.14') + ] + + self.patchobject(return_server, 'interface_list', + return_value=poor_interfaces) + self.patchobject(server, '_get_available_networks', + return_value=multi_nets) + mock_detach = self.patchobject(return_server, 'interface_detach') + mock_attach = self.patchobject(return_server, 'interface_attach') + updater = scheduler.TaskRunner(server.update, update_template) + if not multi_nets: + self.patchobject(nova.NovaClientPlugin, 'check_interface_detach', + return_value=True) + self.patchobject(nova.NovaClientPlugin, + 'check_interface_attach', + return_value=True) + + auto_allocate_net = '9cfe6c74-c105-4906-9a1f-81d9064e9bca' + self.patchobject(server, '_auto_allocate_network', + return_value=[auto_allocate_net]) + updater() + self.assertEqual((server.UPDATE, server.COMPLETE), server.state) + self.assertEqual(1, mock_detach.call_count) + self.assertEqual(1, mock_attach.call_count) + mock_attach.called_once_with( + {'port_id': None, + 'net_id': auto_allocate_net, + 'fip': None}) + else: + self.assertRaises(exception.ResourceFailure, updater) + self.assertEqual(0, mock_detach.call_count) + self.assertEqual(0, mock_attach.call_count) + + def test_server_update_str_networks_auto(self): + self._test_server_update_to_auto() + + def test_server_update_str_networks_auto_multi_nets(self): + available_nets = ['net_1', 'net_2'] + self._test_server_update_to_auto(available_nets) + + def test_server_update_str_networks_none(self): + return_server = self.fc.servers.list()[1] + return_server.id = '5678' + server = self._create_test_server(return_server, 'networks_update') + + old_networks = [ + {'port': '95e25541-d26a-478d-8f36-ae1c8f6b74dc'}, + {'port': '4121f61a-1b2e-4ab0-901e-eade9b1cb09d'}, + {'network': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + 'fixed_ip': '31.32.33.34'}] + + before_props = self.server_props.copy() + before_props['networks'] = old_networks + update_props = self.server_props.copy() + update_props['networks'] = [{'allocate_network': 'none'}] + update_template = server.t.freeze(properties=update_props) + server.t = server.t.freeze(properties=before_props) + + self.patchobject(self.fc.servers, 'get', return_value=return_server) + port_interfaces = [ + self.create_fake_iface('95e25541-d26a-478d-8f36-ae1c8f6b74dc', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '11.12.13.14'), + self.create_fake_iface('4121f61a-1b2e-4ab0-901e-eade9b1cb09d', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '21.22.23.24'), + self.create_fake_iface('0907fa82-a024-43c2-9fc5-efa1bccaa74a', + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + '31.32.33.34') + ] + + self.patchobject(return_server, 'interface_list', + return_value=port_interfaces) + mock_detach = self.patchobject(return_server, 'interface_detach') + self.patchobject(nova.NovaClientPlugin, + 'check_interface_detach', + return_value=True) + mock_attach = self.patchobject(return_server, 'interface_attach') + + scheduler.TaskRunner(server.update, update_template)() + self.assertEqual((server.UPDATE, server.COMPLETE), server.state) + self.assertEqual(3, mock_detach.call_count) + self.assertEqual(0, mock_attach.call_count) + def test_server_update_networks_with_complex_parameters(self): return_server = self.fc.servers.list()[1] return_server.id = '5678'