From db16984b47afe014d2eadd32b56ccf1c347fb53d Mon Sep 17 00:00:00 2001 From: Sam Betts Date: Thu, 1 Feb 2018 14:56:30 +0000 Subject: [PATCH] Ensure we unbind flat network ports and clear BM mac addresses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch fixes a bug in the ironic nova neutron interaction triangle, where a port in a flat network can be left with bound and with the mac address of a baremetal server if it is not deleted after a deployment or deployment failure. The fix is to ensure that the network interfaces reset the mac address to a random mac address when it unbinds the port in the unconfigure step for provisioning and tenant networks, and then rebind and set the mac address for the configure steps. Co-Authored-By: Harald Jensås Story: #2004428 Task: #28087 Change-Id: I11fff92e0a58ac68e795c003c14a336bceba6d89 --- ironic/common/neutron.py | 35 +++++++- ironic/drivers/modules/network/common.py | 1 + ironic/drivers/modules/network/flat.py | 67 ++++++++++++--- ironic/tests/unit/common/test_neutron.py | 61 +++++++++++--- .../unit/drivers/modules/network/test_flat.py | 81 +++++++++++++++---- .../drivers/modules/network/test_neutron.py | 10 ++- .../tests/unit/drivers/modules/test_agent.py | 16 ++++ .../drivers/modules/test_agent_base_vendor.py | 17 ++-- ...-vifs-and-clear-macs-34eec149618e5964.yaml | 13 +++ 9 files changed, 251 insertions(+), 50 deletions(-) create mode 100644 releasenotes/notes/ensure-unbind-flat-vifs-and-clear-macs-34eec149618e5964.yaml diff --git a/ironic/common/neutron.py b/ironic/common/neutron.py index 2cd31f5f95..1486806496 100644 --- a/ironic/common/neutron.py +++ b/ironic/common/neutron.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import random + from neutronclient.common import exceptions as neutron_exceptions from neutronclient.v2_0 import client as clientv20 from oslo_log import log @@ -38,6 +40,26 @@ PHYSNET_PARAM_NAME = 'provider:physical_network' SEGMENTS_PARAM_NAME = 'segments' """Name of the neutron network API segments parameter.""" +BASE_MAC = ['fa', '16', '3e', '00', '00', '00'] + + +def _get_random_mac(base_mac): + """Get a random MAC address string of the specified base format. + + The first 3 octets will remain unchanged. If the 4th octet is not + 00, it will also be used. The others will be randomly generated. + + :param base_mac: Base MAC address represented by an array of 6 strings/int + :returns: The MAC address string. + """ + + mac = [int(base_mac[0], 16), int(base_mac[1], 16), + int(base_mac[2], 16), random.getrandbits(8), + random.getrandbits(8), random.getrandbits(8)] + if base_mac[3] != '00': + mac[3] = int(base_mac[3], 16) + return ':'.join(["%02x" % x for x in mac]) + def _get_neutron_session(): global _NEUTRON_SESSION @@ -102,11 +124,18 @@ def unbind_neutron_port(port_id, client=None, context=None): if not client: client = get_client(context=context) - body = {'port': {'binding:host_id': '', - 'binding:profile': {}}} + body_unbind = {'port': {'binding:host_id': '', + 'binding:profile': {}}} + body_reset_mac = {'port': { + 'mac_address': _get_random_mac(BASE_MAC)}} try: - client.update_port(port_id, body) + client.update_port(port_id, body_unbind) + # NOTE(hjensas): We need to reset the mac address in a separate step. + # Exception PortBound will be raised by neutron as it refuses to + # update the mac address of a bound port if we attempt to unbind and + # reset the mac in the same call. + client.update_port(port_id, body_reset_mac) # NOTE(vsaienko): Ignore if port was deleted before calling vif detach. except neutron_exceptions.PortNotFoundClient: LOG.info('Port %s was not found while unbinding.', port_id) diff --git a/ironic/drivers/modules/network/common.py b/ironic/drivers/modules/network/common.py index b60ce7b238..485434c3dd 100644 --- a/ironic/drivers/modules/network/common.py +++ b/ironic/drivers/modules/network/common.py @@ -255,6 +255,7 @@ def plug_port_to_tenant_network(task, port_like_obj, client=None): 'port': { 'binding:vnic_type': neutron.VNIC_BAREMETAL, 'binding:host_id': node.uuid, + 'mac_address': port_like_obj.address } } binding_profile = {'local_link_information': local_link_info} diff --git a/ironic/drivers/modules/network/flat.py b/ironic/drivers/modules/network/flat.py index e62fefda4d..1b31aa5dfb 100644 --- a/ironic/drivers/modules/network/flat.py +++ b/ironic/drivers/modules/network/flat.py @@ -53,12 +53,7 @@ class FlatNetwork(common.NeutronVIFPortIDMixin, """ self.get_cleaning_network_uuid(task) - def add_provisioning_network(self, task): - """Add the provisioning network to a node. - - :param task: A TaskManager instance. - :raises: NetworkError when failed to set binding:host_id - """ + def _bind_flat_ports(self, task): LOG.debug("Binding flat network ports") client = neutron.get_client(context=task.context) for port_like_obj in task.ports + task.portgroups: @@ -71,7 +66,8 @@ class FlatNetwork(common.NeutronVIFPortIDMixin, body = { 'port': { 'binding:host_id': task.node.uuid, - 'binding:vnic_type': neutron.VNIC_BAREMETAL + 'binding:vnic_type': neutron.VNIC_BAREMETAL, + 'mac_address': port_like_obj.address } } try: @@ -83,26 +79,52 @@ class FlatNetwork(common.NeutronVIFPortIDMixin, LOG.exception(msg) raise exception.NetworkError(msg) + def _unbind_flat_ports(self, task): + node = task.node + LOG.info('Unbinding instance ports from node %s', node.uuid) + + ports = [p for p in task.ports if not p.portgroup_id] + portgroups = task.portgroups + for port_like_obj in ports + portgroups: + vif_port_id = ( + port_like_obj.internal_info.get(common.TENANT_VIF_KEY) or + port_like_obj.extra.get('vif_port_id')) + if not vif_port_id: + continue + neutron.unbind_neutron_port(vif_port_id, context=task.context) + + def add_provisioning_network(self, task): + """Add the provisioning network to a node. + + :param task: A TaskManager instance. + :raises: NetworkError when failed to set binding:host_id + """ + self._bind_flat_ports(task) + def remove_provisioning_network(self, task): """Remove the provisioning network from a node. :param task: A TaskManager instance. """ - pass + self._unbind_flat_ports(task) def configure_tenant_networks(self, task): """Configure tenant networks for a node. :param task: A TaskManager instance. """ - pass + self._bind_flat_ports(task) def unconfigure_tenant_networks(self, task): """Unconfigure tenant networks for a node. + Unbind the port here/now to avoid the possibility of the ironic port + being bound to the tenant and cleaning networks at the same time. + :param task: A TaskManager instance. + :raises: NetworkError """ - pass + self._unbind_flat_ports(task) def add_cleaning_network(self, task): """Add the cleaning network to a node. @@ -141,3 +163,28 @@ class FlatNetwork(common.NeutronVIFPortIDMixin, del internal_info['cleaning_vif_port_id'] port.internal_info = internal_info port.save() + + def add_rescuing_network(self, task): + """Add the rescuing network to a node. + + Flat network does not use the rescuing network. + Bind the port again since unconfigure_tenant_network() unbound it. + + :param task: A TaskManager instance. + :returns: a dictionary in the form {port.uuid: neutron_port['id']} + :raises: NetworkError, InvalidParameterValue + """ + LOG.info('Bind ports for rescuing node %s', task.node.uuid) + self._bind_flat_ports(task) + + def remove_rescuing_network(self, task): + """Remove the rescuing network from a node. + + Flat network does not use the rescuing network. + Unbind the port again since add_rescuing_network() bound it. + + :param task: A TaskManager instance. + :raises: NetworkError + """ + LOG.info('Unbind ports for rescuing node %s', task.node.uuid) + self._unbind_flat_ports(task) diff --git a/ironic/tests/unit/common/test_neutron.py b/ironic/tests/unit/common/test_neutron.py index e58cac646c..d871d5f219 100644 --- a/ironic/tests/unit/common/test_neutron.py +++ b/ironic/tests/unit/common/test_neutron.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import random + from keystoneauth1 import loading as kaloading import mock from neutronclient.common import exceptions as neutron_client_exc @@ -748,29 +750,42 @@ class TestUpdatePortAddress(base.TestCase): @mock.patch.object(neutron, 'get_client', autospec=True) +@mock.patch.object(neutron, '_get_random_mac', autospec=True) class TestUnbindPort(base.TestCase): def setUp(self): super(TestUnbindPort, self).setUp() self.context = context.RequestContext() - def test_unbind_neutron_port_client_passed(self, mock_client): + def test_unbind_neutron_port_client_passed(self, mock_random_mac, + mock_client): port_id = 'fake-port-id' - body = { + address = 'fe:54:00:77:07:d9' + mock_random_mac.return_value = address + body_unbind = { 'port': { 'binding:host_id': '', 'binding:profile': {} } } + body_reset_mac = { + 'port': { + 'mac_address': address + } + } + update_calls = [ + mock.call(port_id, body_unbind), + mock.call(port_id, body_reset_mac) + ] neutron.unbind_neutron_port(port_id, mock_client(context=self.context), context=self.context) self.assertEqual(1, mock_client.call_count) - mock_client.return_value.update_port.assert_called_once_with(port_id, - body) + mock_client.return_value.update_port.assert_has_calls(update_calls) @mock.patch.object(neutron, 'LOG', autospec=True) - def test_unbind_neutron_port_failure(self, mock_log, mock_client): + def test_unbind_neutron_port_failure(self, mock_log, mock_random_mac, + mock_client): mock_client.return_value.update_port.side_effect = ( neutron_client_exc.NeutronClientException()) body = { @@ -787,21 +802,32 @@ class TestUnbindPort(base.TestCase): body) mock_log.exception.assert_called_once() - def test_unbind_neutron_port(self, mock_client): + def test_unbind_neutron_port(self, mock_random_mac, mock_client): port_id = 'fake-port-id' - body = { + address = 'fe:54:00:77:07:d9' + mock_random_mac.return_value = address + body_unbind = { 'port': { 'binding:host_id': '', 'binding:profile': {} } } + body_reset_mac = { + 'port': { + 'mac_address': address + } + } + update_calls = [ + mock.call(port_id, body_unbind), + mock.call(port_id, body_reset_mac) + ] neutron.unbind_neutron_port(port_id, context=self.context) mock_client.assert_called_once_with(context=self.context) - mock_client.return_value.update_port.assert_called_once_with(port_id, - body) + mock_client.return_value.update_port.assert_has_calls(update_calls) @mock.patch.object(neutron, 'LOG', autospec=True) - def test_unbind_neutron_port_not_found(self, mock_log, mock_client): + def test_unbind_neutron_port_not_found(self, mock_log, mock_random_mac, + mock_client): port_id = 'fake-port-id' mock_client.return_value.update_port.side_effect = ( neutron_client_exc.PortNotFoundClient()) @@ -1042,3 +1068,18 @@ class TestGetPhysnetsByPortUUID(base.TestCase): fields=self.PORT_FIELDS) mock_gn.assert_called_once_with(self.client, network_uuid, fields=self.NETWORK_FIELDS) + + +class TestGetRandomMac(base.TestCase): + + @mock.patch.object(random, 'getrandbits', return_value=0xa2) + def test_first_4_octets_unchanged(self, mock_rnd): + mac = neutron._get_random_mac(['aa', 'bb', '00', 'dd', 'ee', 'ff']) + self.assertEqual('aa:bb:00:dd:a2:a2', mac) + mock_rnd.assert_called_with(8) + + @mock.patch.object(random, 'getrandbits', return_value=0xa2) + def test_first_4th_octet_generated(self, mock_rnd): + mac = neutron._get_random_mac(['aa', 'bb', 'cc', '00', 'ee', 'ff']) + self.assertEqual('aa:bb:cc:a2:a2:a2', mac) + mock_rnd.assert_called_with(8) diff --git a/ironic/tests/unit/drivers/modules/network/test_flat.py b/ironic/tests/unit/drivers/modules/network/test_flat.py index 31462c3fb9..7179f6590d 100644 --- a/ironic/tests/unit/drivers/modules/network/test_flat.py +++ b/ironic/tests/unit/drivers/modules/network/test_flat.py @@ -175,8 +175,7 @@ class TestFlatInterface(db_base.DbTestCase): self.assertNotIn('cleaning_vif_port_id', self.port.internal_info) @mock.patch.object(neutron, 'get_client') - def test_add_provisioning_network_set_binding_host_id( - self, client_mock): + def test__bind_flat_ports_set_binding_host_id(self, client_mock): upd_mock = mock.Mock() client_mock.return_value.update_port = upd_mock extra = {'vif_port_id': 'foo'} @@ -184,14 +183,14 @@ class TestFlatInterface(db_base.DbTestCase): address='52:54:00:cf:2d:33', extra=extra, uuid=uuidutils.generate_uuid()) exp_body = {'port': {'binding:host_id': self.node.uuid, - 'binding:vnic_type': neutron.VNIC_BAREMETAL}} + 'binding:vnic_type': neutron.VNIC_BAREMETAL, + 'mac_address': '52:54:00:cf:2d:33'}} with task_manager.acquire(self.context, self.node.id) as task: - self.interface.add_provisioning_network(task) + self.interface._bind_flat_ports(task) upd_mock.assert_called_once_with('foo', exp_body) @mock.patch.object(neutron, 'get_client') - def test_add_provisioning_network_set_binding_host_id_portgroup( - self, client_mock): + def test__bind_flat_ports_set_binding_host_id_portgroup(self, client_mock): upd_mock = mock.Mock() client_mock.return_value.update_port = upd_mock internal_info = {'tenant_vif_port_id': 'foo'} @@ -201,17 +200,46 @@ class TestFlatInterface(db_base.DbTestCase): utils.create_test_port( self.context, node_id=self.node.id, address='52:54:00:cf:2d:33', extra={'vif_port_id': 'bar'}, uuid=uuidutils.generate_uuid()) - exp_body = {'port': {'binding:host_id': self.node.uuid, - 'binding:vnic_type': neutron.VNIC_BAREMETAL}} + exp_body1 = {'port': {'binding:host_id': self.node.uuid, + 'binding:vnic_type': neutron.VNIC_BAREMETAL, + 'mac_address': '52:54:00:cf:2d:33'}} + exp_body2 = {'port': {'binding:host_id': self.node.uuid, + 'binding:vnic_type': neutron.VNIC_BAREMETAL, + 'mac_address': '52:54:00:cf:2d:31'}} with task_manager.acquire(self.context, self.node.id) as task: - self.interface.add_provisioning_network(task) + self.interface._bind_flat_ports(task) upd_mock.assert_has_calls([ - mock.call('bar', exp_body), mock.call('foo', exp_body) - ]) + mock.call('bar', exp_body1), mock.call('foo', exp_body2)]) + + @mock.patch.object(neutron, 'unbind_neutron_port') + def test__unbind_flat_ports(self, unbind_neutron_port_mock): + extra = {'vif_port_id': 'foo'} + utils.create_test_port(self.context, node_id=self.node.id, + address='52:54:00:cf:2d:33', extra=extra, + uuid=uuidutils.generate_uuid()) + with task_manager.acquire(self.context, self.node.id) as task: + self.interface._unbind_flat_ports(task) + unbind_neutron_port_mock.assert_called_once_with('foo', + context=self.context) + + @mock.patch.object(neutron, 'unbind_neutron_port') + def test__unbind_flat_ports_portgroup(self, unbind_neutron_port_mock): + internal_info = {'tenant_vif_port_id': 'foo'} + utils.create_test_portgroup(self.context, node_id=self.node.id, + internal_info=internal_info, + uuid=uuidutils.generate_uuid()) + extra = {'vif_port_id': 'bar'} + utils.create_test_port(self.context, node_id=self.node.id, + address='52:54:00:cf:2d:33', extra=extra, + uuid=uuidutils.generate_uuid()) + with task_manager.acquire(self.context, self.node.id) as task: + self.interface._unbind_flat_ports(task) + unbind_neutron_port_mock.has_calls( + [mock.call('foo', context=self.context), + mock.call('bar', context=self.context)]) @mock.patch.object(neutron, 'get_client') - def test_add_provisioning_network_binding_host_id_raise( - self, client_mock): + def test__bind_flat_ports_set_binding_host_id_raise(self, client_mock): client_mock.return_value.update_port.side_effect = \ (neutron_exceptions.ConnectionFailed()) extra = {'vif_port_id': 'foo'} @@ -220,5 +248,28 @@ class TestFlatInterface(db_base.DbTestCase): uuid=uuidutils.generate_uuid()) with task_manager.acquire(self.context, self.node.id) as task: self.assertRaises(exception.NetworkError, - self.interface.add_provisioning_network, - task) + self.interface._bind_flat_ports, task) + + @mock.patch.object(flat_interface.FlatNetwork, '_bind_flat_ports') + def test_add_rescuing_network(self, bind_mock): + with task_manager.acquire(self.context, self.node.id) as task: + self.interface.add_rescuing_network(task) + bind_mock.assert_called_once_with(task) + + @mock.patch.object(flat_interface.FlatNetwork, '_unbind_flat_ports') + def test_remove_rescuing_network(self, unbind_mock): + with task_manager.acquire(self.context, self.node.id) as task: + self.interface.remove_rescuing_network(task) + unbind_mock.assert_called_once_with(task) + + @mock.patch.object(flat_interface.FlatNetwork, '_bind_flat_ports') + def test_add_provisioning_network(self, bind_mock): + with task_manager.acquire(self.context, self.node.id) as task: + self.interface.add_provisioning_network(task) + bind_mock.assert_called_once_with(task) + + @mock.patch.object(flat_interface.FlatNetwork, '_unbind_flat_ports') + def test_remove_provisioning_network(self, unbind_mock): + with task_manager.acquire(self.context, self.node.id) as task: + self.interface.remove_provisioning_network(task) + unbind_mock.assert_called_once_with(task) diff --git a/ironic/tests/unit/drivers/modules/network/test_neutron.py b/ironic/tests/unit/drivers/modules/network/test_neutron.py index 49e3d01a20..39c7fd9d80 100644 --- a/ironic/tests/unit/drivers/modules/network/test_neutron.py +++ b/ironic/tests/unit/drivers/modules/network/test_neutron.py @@ -580,12 +580,10 @@ class NeutronInterfaceTestCase(db_base.DbTestCase): 'binding:vnic_type': 'baremetal', 'binding:host_id': self.node.uuid, 'binding:profile': {'local_link_information': - [self.port.local_link_connection]} + [self.port.local_link_connection]}, + 'mac_address': '52:54:00:cf:2d:32' } } - utils.create_test_port(self.context, node_id=self.node.id, - address='52:54:00:cf:2d:33', extra={}, - uuid=uuidutils.generate_uuid()) upd_mock = mock.Mock() client_mock.return_value.update_port = upd_mock with task_manager.acquire(self.context, self.node.id) as task: @@ -645,10 +643,12 @@ class NeutronInterfaceTestCase(db_base.DbTestCase): port1_body['port']['binding:profile'] = { 'local_link_information': [self.port.local_link_connection] } + port1_body['port']['mac_address'] = '52:54:00:cf:2d:32' port2_body = copy.deepcopy(expected_body) port2_body['port']['binding:profile'] = { 'local_link_information': [second_port.local_link_connection] } + port2_body['port']['mac_address'] = '52:54:00:cf:2d:33' if is_client_id: port1_body['port']['extra_dhcp_opts'] = ( [{'opt_name': '61', 'opt_value': client_ids[0]}]) @@ -725,12 +725,14 @@ class NeutronInterfaceTestCase(db_base.DbTestCase): call1_body['port']['binding:profile'] = { 'local_link_information': [self.port.local_link_connection], } + call1_body['port']['mac_address'] = '52:54:00:cf:2d:32' call2_body = copy.deepcopy(expected_body) call2_body['port']['binding:profile'] = { 'local_link_information': [port1.local_link_connection, port2.local_link_connection], 'local_group_information': local_group_info } + call2_body['port']['mac_address'] = 'ff:54:00:cf:2d:32' with task_manager.acquire(self.context, self.node.id) as task: # Override task.portgroups here, to have ability to check # that mocked get_local_group_information was called with diff --git a/ironic/tests/unit/drivers/modules/test_agent.py b/ironic/tests/unit/drivers/modules/test_agent.py index fba4807c4c..16c653c433 100644 --- a/ironic/tests/unit/drivers/modules/test_agent.py +++ b/ironic/tests/unit/drivers/modules/test_agent.py @@ -178,6 +178,7 @@ class TestAgentDeploy(db_base.DbTestCase): 'driver_info': DRIVER_INFO, 'driver_internal_info': DRIVER_INTERNAL_INFO, 'storage_interface': 'noop', + 'network_interface': 'noop' } self.node = object_utils.create_test_node(self.context, **n) self.ports = [ @@ -351,6 +352,9 @@ class TestAgentDeploy(db_base.DbTestCase): storage_detach_volumes_mock): object_utils.create_test_volume_target( self.context, node_id=self.node.id) + node = self.node + node.network_interface = 'flat' + node.save() with task_manager.acquire( self.context, self.node['uuid'], shared=False) as task: driver_return = self.driver.tear_down(task) @@ -386,6 +390,9 @@ class TestAgentDeploy(db_base.DbTestCase): build_instance_info_mock, build_options_mock, pxe_prepare_ramdisk_mock, storage_driver_info_mock, storage_attach_volumes_mock): + node = self.node + node.network_interface = 'flat' + node.save() with task_manager.acquire( self.context, self.node['uuid'], shared=False) as task: task.node.provision_state = states.DEPLOYING @@ -601,6 +608,9 @@ class TestAgentDeploy(db_base.DbTestCase): build_options_mock, pxe_prepare_ramdisk_mock, validate_net_mock, add_provisioning_net_mock): self.config(group='agent', manage_agent_boot=False) + node = self.node + node.network_interface = 'flat' + node.save() with task_manager.acquire( self.context, self.node['uuid'], shared=False) as task: task.node.provision_state = states.DEPLOYING @@ -700,6 +710,9 @@ class TestAgentDeploy(db_base.DbTestCase): add_provisioning_net_mock, storage_driver_info_mock, storage_attach_volumes_mock, should_write_image_mock): should_write_image_mock.return_value = False + node = self.node + node.network_interface = 'flat' + node.save() with task_manager.acquire( self.context, self.node['uuid'], shared=False) as task: task.node.provision_state = states.DEPLOYING @@ -750,6 +763,9 @@ class TestAgentDeploy(db_base.DbTestCase): validate_net_mock, add_provisioning_net_mock): mock_write.return_value = False + node = self.node + node.network_interface = 'flat' + node.save() with task_manager.acquire( self.context, self.node['uuid'], shared=False) as task: task.node.provision_state = states.DEPLOYING diff --git a/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py b/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py index c99c921875..cc071bb4cd 100644 --- a/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py +++ b/ironic/tests/unit/drivers/modules/test_agent_base_vendor.py @@ -68,6 +68,7 @@ class AgentDeployMixinBaseTest(db_base.DbTestCase): 'instance_info': INSTANCE_INFO, 'driver_info': DRIVER_INFO, 'driver_internal_info': DRIVER_INTERNAL_INFO, + 'network_interface': 'noop' } self.node = object_utils.create_test_node(self.context, **n) @@ -497,9 +498,9 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest): spec=types.FunctionType) @mock.patch.object(agent_client.AgentClient, 'power_off', spec=types.FunctionType) - @mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.' + @mock.patch('ironic.drivers.modules.network.noop.NoopNetwork.' 'remove_provisioning_network', spec_set=True, autospec=True) - @mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.' + @mock.patch('ironic.drivers.modules.network.noop.NoopNetwork.' 'configure_tenant_networks', spec_set=True, autospec=True) def test_reboot_and_finish_deploy_soft_poweroff_doesnt_complete( self, configure_tenant_net_mock, remove_provisioning_net_mock, @@ -528,9 +529,9 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest): @mock.patch.object(manager_utils, 'node_power_action', autospec=True) @mock.patch.object(agent_client.AgentClient, 'power_off', spec=types.FunctionType) - @mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.' + @mock.patch('ironic.drivers.modules.network.noop.NoopNetwork.' 'remove_provisioning_network', spec_set=True, autospec=True) - @mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.' + @mock.patch('ironic.drivers.modules.network.noop.NoopNetwork.' 'configure_tenant_networks', spec_set=True, autospec=True) def test_reboot_and_finish_deploy_soft_poweroff_fails( self, configure_tenant_net_mock, remove_provisioning_net_mock, @@ -560,9 +561,9 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest): spec=types.FunctionType) @mock.patch.object(agent_client.AgentClient, 'power_off', spec=types.FunctionType) - @mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.' + @mock.patch('ironic.drivers.modules.network.noop.NoopNetwork.' 'remove_provisioning_network', spec_set=True, autospec=True) - @mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.' + @mock.patch('ironic.drivers.modules.network.noop.NoopNetwork.' 'configure_tenant_networks', spec_set=True, autospec=True) def test_reboot_and_finish_deploy_get_power_state_fails( self, configure_tenant_net_mock, remove_provisioning_net_mock, @@ -655,9 +656,9 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest): spec=types.FunctionType) @mock.patch.object(agent_client.AgentClient, 'power_off', spec=types.FunctionType) - @mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.' + @mock.patch('ironic.drivers.modules.network.noop.NoopNetwork.' 'remove_provisioning_network', spec_set=True, autospec=True) - @mock.patch('ironic.drivers.modules.network.flat.FlatNetwork.' + @mock.patch('ironic.drivers.modules.network.noop.NoopNetwork.' 'configure_tenant_networks', spec_set=True, autospec=True) def test_reboot_and_finish_deploy_power_on_fails( self, configure_tenant_net_mock, remove_provisioning_net_mock, diff --git a/releasenotes/notes/ensure-unbind-flat-vifs-and-clear-macs-34eec149618e5964.yaml b/releasenotes/notes/ensure-unbind-flat-vifs-and-clear-macs-34eec149618e5964.yaml new file mode 100644 index 0000000000..347b7d3c5c --- /dev/null +++ b/releasenotes/notes/ensure-unbind-flat-vifs-and-clear-macs-34eec149618e5964.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Fixes an issue where Neutron ports would be left with a baremetal MAC + address associated after an instance is deleted from a baremetal host. This + caused problems with MAC address conflicts in follow up deployments to the + same baremetal host. `bug 2004428 + `_. + - | + Fixes an issue where a flat Neutron port would be left with a host ID + associated with it after an instance is deleted from a baremetal host. This + caused problems with reusing the same port for a new instance as it is + already bound to the old instance.