Validate port info before assume we may use it

Ironic 'neutron' network driver requires local_link_connection to be
present on ironic port to be able to work with it (plug to specific
network).
This patch ignores ironic ports without local_link_connection when
attaching VIF to node with 'neutron' network driver.
Also make sure that we do not pick such port for provisioning and
cleaning.

Closes-Bug: #1653249

Change-Id: Icb8298b9be0d8ba62192580263feafa0e1708129
This commit is contained in:
Vasyl Saienko 2016-12-30 15:53:41 +02:00
parent c1c86e81af
commit 56570b1f41
7 changed files with 151 additions and 21 deletions

View File

@ -162,8 +162,7 @@ def _verify_security_groups(security_groups, client):
raise exception.NetworkError(msg) raise exception.NetworkError(msg)
def add_ports_to_network(task, network_uuid, is_flat=False, def add_ports_to_network(task, network_uuid, security_groups=None):
security_groups=None):
"""Create neutron ports to boot the ramdisk. """Create neutron ports to boot the ramdisk.
Create neutron ports for each pxe_enabled port on task.node to boot Create neutron ports for each pxe_enabled port on task.node to boot
@ -172,7 +171,6 @@ def add_ports_to_network(task, network_uuid, is_flat=False,
:param task: a TaskManager instance. :param task: a TaskManager instance.
:param network_uuid: UUID of a neutron network where ports will be :param network_uuid: UUID of a neutron network where ports will be
created. created.
:param is_flat: Indicates whether it is a flat network or not.
:param security_groups: List of Security Groups UUIDs to be used for :param security_groups: List of Security Groups UUIDs to be used for
network. network.
:raises: NetworkError :raises: NetworkError
@ -199,7 +197,7 @@ def add_ports_to_network(task, network_uuid, is_flat=False,
if security_groups: if security_groups:
body['port']['security_groups'] = security_groups body['port']['security_groups'] = security_groups
if not is_flat: if node.network_interface != 'flat':
# NOTE(vdrok): It seems that change # NOTE(vdrok): It seems that change
# I437290affd8eb87177d0626bf7935a165859cbdd to neutron broke the # I437290affd8eb87177d0626bf7935a165859cbdd to neutron broke the
# possibility to always bind port. Set binding:host_id only in # possibility to always bind port. Set binding:host_id only in
@ -216,6 +214,10 @@ def add_ports_to_network(task, network_uuid, is_flat=False,
portmap = get_node_portmap(task) portmap = get_node_portmap(task)
pxe_enabled_ports = [p for p in task.ports if p.pxe_enabled] pxe_enabled_ports = [p for p in task.ports if p.pxe_enabled]
for ironic_port in pxe_enabled_ports: for ironic_port in pxe_enabled_ports:
# Skip ports that are missing required information for deploy.
if not validate_port_info(node, ironic_port):
failures.append(ironic_port.uuid)
continue
body['port']['mac_address'] = ironic_port.address body['port']['mac_address'] = ironic_port.address
binding_profile = {'local_link_information': binding_profile = {'local_link_information':
[portmap[ironic_port.uuid]]} [portmap[ironic_port.uuid]]}
@ -394,6 +396,27 @@ def validate_network(uuid_or_name, net_type=_('network')):
return networks[0] return networks[0]
def validate_port_info(node, port):
"""Check that port contains enough information for deploy.
Neutron network interface requires that local_link_information field is
filled before we can use this port.
:param node: Ironic node object.
:param port: Ironic port object.
:returns: True if port info is valid, False otherwise.
"""
if (node.network_interface == 'neutron' and
not port.local_link_connection):
LOG.warning(_LW("The local_link_connection is required for"
"'neutron' network interface and is not present "
"in the nodes %(node)s port %(port)s"),
{'node': node.uuid, 'port': port.uuid})
return False
return True
class NeutronNetworkInterfaceMixin(object): class NeutronNetworkInterfaceMixin(object):
_cleaning_network_uuid = None _cleaning_network_uuid = None

View File

@ -75,6 +75,9 @@ def _get_free_portgroups_and_ports(task, vif_id):
non_usable_portgroups = set() non_usable_portgroups = set()
for p in task.ports: for p in task.ports:
# Validate that port has needed information
if not neutron.validate_port_info(task.node, p):
continue
if _vif_attached(p, vif_id): if _vif_attached(p, vif_id):
# Consider such portgroup unusable. The fact that we can have None # Consider such portgroup unusable. The fact that we can have None
# added in this set is not a problem # added in this set is not a problem

View File

@ -123,7 +123,7 @@ class FlatNetwork(common.VIFPortIDMixin, neutron.NeutronNetworkInterfaceMixin,
neutron.rollback_ports(task, self.get_cleaning_network_uuid()) neutron.rollback_ports(task, self.get_cleaning_network_uuid())
LOG.info(_LI('Adding cleaning network to node %s'), task.node.uuid) LOG.info(_LI('Adding cleaning network to node %s'), task.node.uuid)
vifs = neutron.add_ports_to_network( vifs = neutron.add_ports_to_network(
task, self.get_cleaning_network_uuid(), is_flat=True) task, self.get_cleaning_network_uuid())
for port in task.ports: for port in task.ports:
if port.uuid in vifs: if port.uuid in vifs:
internal_info = port.internal_info internal_info = port.internal_info

View File

@ -139,6 +139,8 @@ class TestNeutronNetworkActions(db_base.DbTestCase):
def _test_add_ports_to_vlan_network(self, is_client_id, def _test_add_ports_to_vlan_network(self, is_client_id,
security_groups=None): security_groups=None):
# Ports will be created only if pxe_enabled is True # Ports will be created only if pxe_enabled is True
self.node.network_interface = 'neutron'
self.node.save()
object_utils.create_test_port( object_utils.create_test_port(
self.context, node_id=self.node.id, self.context, node_id=self.node.id,
uuid=uuidutils.generate_uuid(), uuid=uuidutils.generate_uuid(),
@ -272,6 +274,8 @@ class TestNeutronNetworkActions(db_base.DbTestCase):
self._test_add_ports_to_vlan_network(is_client_id=True) self._test_add_ports_to_vlan_network(is_client_id=True)
def _test_add_ports_to_flat_network(self, is_client_id): def _test_add_ports_to_flat_network(self, is_client_id):
self.node.network_interface = 'flat'
self.node.save()
port = self.ports[0] port = self.ports[0]
if is_client_id: if is_client_id:
extra = port.extra extra = port.extra
@ -299,20 +303,26 @@ class TestNeutronNetworkActions(db_base.DbTestCase):
'port': self.neutron_port} 'port': self.neutron_port}
expected = {port.uuid: self.neutron_port['id']} expected = {port.uuid: self.neutron_port['id']}
with task_manager.acquire(self.context, self.node.uuid) as task: with task_manager.acquire(self.context, self.node.uuid) as task:
ports = neutron.add_ports_to_network(task, self.network_uuid, ports = neutron.add_ports_to_network(task, self.network_uuid)
is_flat=True)
self.assertEqual(expected, ports) self.assertEqual(expected, ports)
self.client_mock.create_port.assert_called_once_with( self.client_mock.create_port.assert_called_once_with(
expected_body) expected_body)
def test_add_ports_to_flat_network(self): @mock.patch.object(neutron, 'validate_port_info', autospec=True,
return_value=True)
def test_add_ports_to_flat_network(self, vpi_mock):
self._test_add_ports_to_flat_network(is_client_id=False) self._test_add_ports_to_flat_network(is_client_id=False)
self.assertTrue(vpi_mock.called)
def test_add_ports_with_client_id_to_flat_network(self): @mock.patch.object(neutron, 'validate_port_info', autospec=True,
return_value=True)
def test_add_ports_with_client_id_to_flat_network(self, vpi_mock):
self._test_add_ports_to_flat_network(is_client_id=True) self._test_add_ports_to_flat_network(is_client_id=True)
def test_add_ports_to_vlan_network_instance_uuid(self): @mock.patch.object(neutron, 'validate_port_info', autospec=True)
def test_add_ports_to_vlan_network_instance_uuid(self, vpi_mock):
self.node.instance_uuid = uuidutils.generate_uuid() self.node.instance_uuid = uuidutils.generate_uuid()
self.node.network_interface = 'neutron'
self.node.save() self.node.save()
port = self.ports[0] port = self.ports[0]
expected_body = { expected_body = {
@ -329,6 +339,7 @@ class TestNeutronNetworkActions(db_base.DbTestCase):
} }
} }
} }
vpi_mock.return_value = True
# Ensure we can create ports # Ensure we can create ports
self.client_mock.create_port.return_value = {'port': self.neutron_port} self.client_mock.create_port.return_value = {'port': self.neutron_port}
expected = {port.uuid: self.neutron_port['id']} expected = {port.uuid: self.neutron_port['id']}
@ -336,6 +347,7 @@ class TestNeutronNetworkActions(db_base.DbTestCase):
ports = neutron.add_ports_to_network(task, self.network_uuid) ports = neutron.add_ports_to_network(task, self.network_uuid)
self.assertEqual(expected, ports) self.assertEqual(expected, ports)
self.client_mock.create_port.assert_called_once_with(expected_body) self.client_mock.create_port.assert_called_once_with(expected_body)
self.assertTrue(vpi_mock.called)
@mock.patch.object(neutron, 'rollback_ports') @mock.patch.object(neutron, 'rollback_ports')
def test_add_network_all_ports_fail(self, rollback_mock): def test_add_network_all_ports_fail(self, rollback_mock):
@ -448,6 +460,41 @@ class TestNeutronNetworkActions(db_base.DbTestCase):
neutron.rollback_ports(task, self.network_uuid) neutron.rollback_ports(task, self.network_uuid)
self.assertTrue(log_mock.exception.called) self.assertTrue(log_mock.exception.called)
@mock.patch.object(neutron, 'LOG')
def test_validate_port_info_neutron_interface(self, log_mock):
self.node.network_interface = 'neutron'
self.node.save()
port = object_utils.create_test_port(
self.context, node_id=self.node.id, uuid=uuidutils.generate_uuid(),
address='52:54:00:cf:2d:33')
res = neutron.validate_port_info(self.node, port)
self.assertTrue(res)
self.assertFalse(log_mock.warning.called)
@mock.patch.object(neutron, 'LOG')
def test_validate_port_info_neutron_interface_missed_info(self, log_mock):
self.node.network_interface = 'neutron'
self.node.save()
llc = {}
port = object_utils.create_test_port(
self.context, node_id=self.node.id, uuid=uuidutils.generate_uuid(),
address='52:54:00:cf:2d:33', local_link_connection=llc)
res = neutron.validate_port_info(self.node, port)
self.assertFalse(res)
self.assertTrue(log_mock.warning.called)
@mock.patch.object(neutron, 'LOG')
def test_validate_port_info_flat_interface(self, log_mock):
self.node.network_interface = 'flat'
self.node.save()
llc = {}
port = object_utils.create_test_port(
self.context, node_id=self.node.id, uuid=uuidutils.generate_uuid(),
address='52:54:00:cf:2d:33', local_link_connection=llc)
res = neutron.validate_port_info(self.node, port)
self.assertTrue(res)
self.assertFalse(log_mock.warning.called)
@mock.patch.object(neutron, 'get_client', autospec=True) @mock.patch.object(neutron, 'get_client', autospec=True)
class TestValidateNetwork(base.TestCase): class TestValidateNetwork(base.TestCase):

View File

@ -77,6 +77,8 @@ class TestCommonFunctions(db_base.DbTestCase):
return pg1, pg1_ports, pg2, pg2_ports, pg3, pg3_ports return pg1, pg1_ports, pg2, pg2_ports, pg3, pg3_ports
def test__get_free_portgroups_and_ports(self): def test__get_free_portgroups_and_ports(self):
self.node.network_interface = 'flat'
self.node.save()
pg1, pg1_ports, pg2, pg2_ports, pg3, pg3_ports = self._objects_setup() pg1, pg1_ports, pg2, pg2_ports, pg3, pg3_ports = self._objects_setup()
with task_manager.acquire(self.context, self.node.id) as task: with task_manager.acquire(self.context, self.node.id) as task:
free_portgroups, free_ports = ( free_portgroups, free_ports = (
@ -86,12 +88,42 @@ class TestCommonFunctions(db_base.DbTestCase):
[p.uuid for p in free_ports]) [p.uuid for p in free_ports])
self.assertItemsEqual([pg1.uuid], [p.uuid for p in free_portgroups]) self.assertItemsEqual([pg1.uuid], [p.uuid for p in free_portgroups])
def test_get_free_port_like_object_ports(self): @mock.patch.object(neutron_common, 'validate_port_info', autospec=True)
def test__get_free_portgroups_and_ports_neutron_missed(self, vpi_mock):
vpi_mock.return_value = False
with task_manager.acquire(self.context, self.node.id) as task:
free_portgroups, free_ports = (
common._get_free_portgroups_and_ports(task, self.vif_id))
self.assertItemsEqual([], free_ports)
@mock.patch.object(neutron_common, 'validate_port_info', autospec=True)
def test__get_free_portgroups_and_ports_neutron(self, vpi_mock):
vpi_mock.return_value = True
with task_manager.acquire(self.context, self.node.id) as task:
free_portgroups, free_ports = (
common._get_free_portgroups_and_ports(task, self.vif_id))
self.assertItemsEqual(
[self.port.uuid], [p.uuid for p in free_ports])
@mock.patch.object(neutron_common, 'validate_port_info', autospec=True)
def test__get_free_portgroups_and_ports_flat(self, vpi_mock):
self.node.network_interface = 'flat'
self.node.save()
vpi_mock.return_value = True
with task_manager.acquire(self.context, self.node.id) as task:
free_portgroups, free_ports = (
common._get_free_portgroups_and_ports(task, self.vif_id))
@mock.patch.object(neutron_common, 'validate_port_info', autospec=True,
return_value=True)
def test_get_free_port_like_object_ports(self, vpi_mock):
with task_manager.acquire(self.context, self.node.id) as task: with task_manager.acquire(self.context, self.node.id) as task:
res = common.get_free_port_like_object(task, self.vif_id) res = common.get_free_port_like_object(task, self.vif_id)
self.assertEqual(self.port.uuid, res.uuid) self.assertEqual(self.port.uuid, res.uuid)
def test_get_free_port_like_object_ports_pxe_enabled_first(self): @mock.patch.object(neutron_common, 'validate_port_info', autospec=True,
return_value=True)
def test_get_free_port_like_object_ports_pxe_enabled_first(self, vpi_mock):
self.port.pxe_enabled = False self.port.pxe_enabled = False
self.port.save() self.port.save()
other_port = obj_utils.create_test_port( other_port = obj_utils.create_test_port(
@ -101,7 +133,9 @@ class TestCommonFunctions(db_base.DbTestCase):
res = common.get_free_port_like_object(task, self.vif_id) res = common.get_free_port_like_object(task, self.vif_id)
self.assertEqual(other_port.uuid, res.uuid) self.assertEqual(other_port.uuid, res.uuid)
def test_get_free_port_like_object_portgroup_first(self): @mock.patch.object(neutron_common, 'validate_port_info', autospec=True,
return_value=True)
def test_get_free_port_like_object_portgroup_first(self, vpi_mock):
pg = obj_utils.create_test_portgroup( pg = obj_utils.create_test_portgroup(
self.context, node_id=self.node.id) self.context, node_id=self.node.id)
obj_utils.create_test_port( obj_utils.create_test_port(
@ -111,13 +145,18 @@ class TestCommonFunctions(db_base.DbTestCase):
res = common.get_free_port_like_object(task, self.vif_id) res = common.get_free_port_like_object(task, self.vif_id)
self.assertEqual(pg.uuid, res.uuid) self.assertEqual(pg.uuid, res.uuid)
def test_get_free_port_like_object_ignores_empty_portgroup(self): @mock.patch.object(neutron_common, 'validate_port_info', autospec=True,
return_value=True)
def test_get_free_port_like_object_ignores_empty_portgroup(self, vpi_mock):
obj_utils.create_test_portgroup(self.context, node_id=self.node.id) obj_utils.create_test_portgroup(self.context, node_id=self.node.id)
with task_manager.acquire(self.context, self.node.id) as task: with task_manager.acquire(self.context, self.node.id) as task:
res = common.get_free_port_like_object(task, self.vif_id) res = common.get_free_port_like_object(task, self.vif_id)
self.assertEqual(self.port.uuid, res.uuid) self.assertEqual(self.port.uuid, res.uuid)
def test_get_free_port_like_object_ignores_standalone_portgroup(self): @mock.patch.object(neutron_common, 'validate_port_info', autospec=True,
return_value=True)
def test_get_free_port_like_object_ignores_standalone_portgroup(
self, vpi_mock):
self.port.destroy() self.port.destroy()
pg = obj_utils.create_test_portgroup( pg = obj_utils.create_test_portgroup(
self.context, node_id=self.node.id) self.context, node_id=self.node.id)
@ -132,7 +171,10 @@ class TestCommonFunctions(db_base.DbTestCase):
res = common.get_free_port_like_object(task, self.vif_id) res = common.get_free_port_like_object(task, self.vif_id)
self.assertEqual(free_port.uuid, res.uuid) self.assertEqual(free_port.uuid, res.uuid)
def test_get_free_port_like_object_vif_attached_to_portgroup(self): @mock.patch.object(neutron_common, 'validate_port_info', autospec=True,
return_value=True)
def test_get_free_port_like_object_vif_attached_to_portgroup(
self, vpi_mock):
pg = obj_utils.create_test_portgroup( pg = obj_utils.create_test_portgroup(
self.context, node_id=self.node.id, self.context, node_id=self.node.id,
internal_info={common.TENANT_VIF_KEY: self.vif_id}) internal_info={common.TENANT_VIF_KEY: self.vif_id})
@ -145,7 +187,10 @@ class TestCommonFunctions(db_base.DbTestCase):
r"already attached to Ironic Portgroup", r"already attached to Ironic Portgroup",
common.get_free_port_like_object, task, self.vif_id) common.get_free_port_like_object, task, self.vif_id)
def test_get_free_port_like_object_vif_attached_to_portgroup_extra(self): @mock.patch.object(neutron_common, 'validate_port_info', autospec=True,
return_value=True)
def test_get_free_port_like_object_vif_attached_to_portgroup_extra(
self, vpi_mock):
pg = obj_utils.create_test_portgroup( pg = obj_utils.create_test_portgroup(
self.context, node_id=self.node.id, self.context, node_id=self.node.id,
extra={'vif_port_id': self.vif_id}) extra={'vif_port_id': self.vif_id})
@ -158,7 +203,9 @@ class TestCommonFunctions(db_base.DbTestCase):
r"already attached to Ironic Portgroup", r"already attached to Ironic Portgroup",
common.get_free_port_like_object, task, self.vif_id) common.get_free_port_like_object, task, self.vif_id)
def test_get_free_port_like_object_vif_attached_to_port(self): @mock.patch.object(neutron_common, 'validate_port_info', autospec=True,
return_value=True)
def test_get_free_port_like_object_vif_attached_to_port(self, vpi_mock):
self.port.internal_info = {common.TENANT_VIF_KEY: self.vif_id} self.port.internal_info = {common.TENANT_VIF_KEY: self.vif_id}
self.port.save() self.port.save()
with task_manager.acquire(self.context, self.node.id) as task: with task_manager.acquire(self.context, self.node.id) as task:
@ -167,7 +214,10 @@ class TestCommonFunctions(db_base.DbTestCase):
r"already attached to Ironic Port\b", r"already attached to Ironic Port\b",
common.get_free_port_like_object, task, self.vif_id) common.get_free_port_like_object, task, self.vif_id)
def test_get_free_port_like_object_vif_attached_to_port_extra(self): @mock.patch.object(neutron_common, 'validate_port_info', autospec=True,
return_value=True)
def test_get_free_port_like_object_vif_attached_to_port_extra(
self, vpi_mock):
self.port.extra = {'vif_port_id': self.vif_id} self.port.extra = {'vif_port_id': self.vif_id}
self.port.save() self.port.save()
with task_manager.acquire(self.context, self.node.id) as task: with task_manager.acquire(self.context, self.node.id) as task:
@ -176,7 +226,9 @@ class TestCommonFunctions(db_base.DbTestCase):
r"already attached to Ironic Port\b", r"already attached to Ironic Port\b",
common.get_free_port_like_object, task, self.vif_id) common.get_free_port_like_object, task, self.vif_id)
def test_get_free_port_like_object_nothing_free(self): @mock.patch.object(neutron_common, 'validate_port_info', autospec=True,
return_value=True)
def test_get_free_port_like_object_nothing_free(self, vpi_mock):
self.port.extra = {'vif_port_id': 'another-vif'} self.port.extra = {'vif_port_id': 'another-vif'}
self.port.save() self.port.save()
with task_manager.acquire(self.context, self.node.id) as task: with task_manager.acquire(self.context, self.node.id) as task:

View File

@ -92,7 +92,7 @@ class TestFlatInterface(db_base.DbTestCase):
rollback_mock.assert_called_once_with( rollback_mock.assert_called_once_with(
task, CONF.neutron.cleaning_network) task, CONF.neutron.cleaning_network)
add_mock.assert_called_once_with( add_mock.assert_called_once_with(
task, CONF.neutron.cleaning_network, is_flat=True) task, CONF.neutron.cleaning_network)
validate_mock.assert_called_once_with( validate_mock.assert_called_once_with(
CONF.neutron.cleaning_network, CONF.neutron.cleaning_network,
'cleaning network') 'cleaning network')

View File

@ -0,0 +1,5 @@
---
fixes:
- Fixes an issue when attaching VIF to a port with missed
``local_link_connection`` field was allowed when node network
interface was ``neutron``.