diff --git a/setup.cfg b/setup.cfg index 4b19c3a..68402c4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,3 +28,4 @@ packages = ironic.inspection.hooks = ib_physnet = stackhpc_inspector_plugins.plugins.ib_physnet:IBPhysnetHook system_name_physnet = stackhpc_inspector_plugins.plugins.ib_physnet:SystemNamePhysnetHook + port_groups = stackhpc_inspector_plugins.plugins.port_group:PortGroupsHook diff --git a/stackhpc_inspector_plugins/conf.py b/stackhpc_inspector_plugins/conf.py index 9b3d4d9..44583e5 100644 --- a/stackhpc_inspector_plugins/conf.py +++ b/stackhpc_inspector_plugins/conf.py @@ -31,11 +31,25 @@ 'system name.')), ] +PORT_GROUP_OPTS = [ + cfg.ListOpt( + 'port_group_switches', + default=[], + help=('A list of switch names, where two ports are connected to ' + 'switches in this list, those ports will be added to a port ' + 'group.')), + cfg.StrOpt( + 'port_group_mode', + default='active-backup', + help=('The port group mode to use when creating port groups.')), +] cfg.CONF.register_opts(PORT_PHYSNET_OPTS, group='port_physnet') +cfg.CONF.register_opts(PORT_GROUP_OPTS, group='port_group') def list_opts(): return [ ('port_physnet', PORT_PHYSNET_OPTS), + ('port_group', PORT_GROUP_OPTS), ] diff --git a/stackhpc_inspector_plugins/plugins/port_group.py b/stackhpc_inspector_plugins/plugins/port_group.py new file mode 100644 index 0000000..6ba06a9 --- /dev/null +++ b/stackhpc_inspector_plugins/plugins/port_group.py @@ -0,0 +1,89 @@ +# 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 oslo_config import cfg +from oslo_log import log as logging +from oslo_utils import uuidutils + +from ironic.drivers.modules.inspector.hooks import base +from ironic import objects + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class PortGroupsHook(base.InspectionHook): + """Hook to set add port groups based on switch name""" + + dependencies = ['validate-interfaces'] + + def __call__(self, task, inventory, plugin_data): + """Process inspection data and patch the port's physical network.""" + + node_port_groups = objects.Portgroup.list_by_node_id( + task.context, task.node.id) + if len(node_port_groups) > 0: + LOG.debug("Node %s already has port groups defined, skipping " + "port group creation.", task.node.uuid) + return + + candidate_ports = [] + node_ports = objects.Port.list_by_node_id(task.context, task.node.id) + for port in node_ports: + if not port.local_link_connection: + LOG.debug("Port %s has no local link connection data, " + "skipping.", port.uuid) + continue + + switch_name = port.local_link_connection.get('switch_info', {}) + if not switch_name: + LOG.debug("Port %s has no switch info in local link " + "connection data, skipping.", port.uuid) + continue + + if switch_name not in CONF.port_group.port_group_switches: + LOG.debug("Port %s connected to switch %s which is not in " + "the configured port group switches, skipping.", + port.uuid, switch_name) + continue + + candidate_ports.append(port) + + if len(candidate_ports) != 2: + LOG.debug("Found %d candidate ports for port group creation on " + "node %s, need exactly 2, skipping port group " + "creation.", len(candidate_ports), task.node.uuid) + return + + # sort list by mac address + candidate_ports.sort(key=lambda p: p.address) + + # create port group + new_port_group = objects.Portgroup( + task.context, + uuid=uuidutils.generate_uuid(), + mode=CONF.port_group.port_group_mode, + node_id=task.node.id, + address=candidate_ports[0].address, + standalone_ports_supported=True, + physical_network=candidate_ports[0].physical_network) + new_port_group.create() + new_port_group.refresh() + LOG.info("Created port group %s on node %s", + new_port_group.uuid, task.node.uuid) + + for port in candidate_ports: + port.portgroup_id = new_port_group.id + port.save() + LOG.info("Added port %s to port group %s", + port.uuid, new_port_group.uuid) diff --git a/stackhpc_inspector_plugins/tests/unit/test_plugins_port_group.py b/stackhpc_inspector_plugins/tests/unit/test_plugins_port_group.py new file mode 100644 index 0000000..3189ac2 --- /dev/null +++ b/stackhpc_inspector_plugins/tests/unit/test_plugins_port_group.py @@ -0,0 +1,142 @@ +# 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 unittest import mock + +from ironic.conductor import task_manager +from ironic import objects +from ironic.tests.unit.db import base as db_base +from ironic.tests.unit.objects import utils as obj_utils +from ironic.conf import CONF + +from stackhpc_inspector_plugins.plugins import port_group + + +class TestPortGroupHook(db_base.DbTestCase): + def setUp(self): + super().setUp() + CONF.set_override('enabled_inspect_interfaces', + ['agent', 'no-inspect']) + self.node = obj_utils.create_test_node(self.context, + inspect_interface='agent') + + @mock.patch.object(objects.Port, 'list_by_node_id', autospec=True) + def test_port_group_works(self, mock_list_by_nodeid): + CONF.set_override('port_group_switches', + ['my_switch_1', 'my_switch_2'], + group='port_group') + with task_manager.acquire(self.context, self.node.id) as task: + port1 = obj_utils.create_test_port(self.context, + address='11:11:11:11:11:11', + node_id=self.node.id) + port2 = obj_utils.create_test_port( + self.context, id=988, + uuid='2be26c0b-03f2-4d2e-ae87-c02d7f33c781', + address='22:22:22:22:22:22', node_id=self.node.id, + local_link_connection={ + 'switch_info': 'my_switch_1', + }) + port3 = obj_utils.create_test_port( + self.context, id=989, + uuid='2be26c0b-03f2-4d2e-ae87-c02d7f33c784', + address='22:22:22:22:22:23', node_id=self.node.id, + local_link_connection={ + 'switch_info': 'my_switch_2', + }) + port4 = obj_utils.create_test_port( + self.context, id=990, + uuid='2be26c0b-03f2-4d2e-ae87-c02d7f33c786', + address='22:22:22:22:22:24', node_id=self.node.id, + local_link_connection={ + 'switch_info': 'my_switch_3', + }) + port5 = obj_utils.create_test_port( + self.context, id=991, + uuid='2be26c0b-03f2-4d2e-ae87-c02d7f33c788', + address='22:22:22:22:22:25', node_id=self.node.id, + local_link_connection={ + 'foo': 'bar', + }) + ports = [port1, port2, port3, port4, port5] + mock_list_by_nodeid.return_value = ports + + port_group.PortGroupsHook().__call__( + task, {}, {}) + + port1.refresh() + port2.refresh() + port3.refresh() + self.assertEqual(port1.portgroup_id, None) + + new_port_groups = objects.Portgroup.list_by_node_id( + self.context, self.node.id) + self.assertEqual(len(new_port_groups), 1) + new_port_group = new_port_groups[0] + + self.assertEqual(port2.portgroup_id, new_port_group.id) + self.assertEqual(port3.portgroup_id, new_port_group.id) + self.assertEqual(port2.address, new_port_group.address) + + @mock.patch.object(objects.Port, 'list_by_node_id', autospec=True) + def test_port_group_skip_three_ports(self, mock_list_by_nodeid): + CONF.set_override('port_group_switches', + ['my_switch_1', 'my_switch_2'], + group='port_group') + with task_manager.acquire(self.context, self.node.id) as task: + port1 = obj_utils.create_test_port(self.context, + address='11:11:11:11:11:11', + node_id=self.node.id) + port2 = obj_utils.create_test_port( + self.context, id=988, + uuid='2be26c0b-03f2-4d2e-ae87-c02d7f33c781', + address='22:22:22:22:22:22', node_id=self.node.id, + local_link_connection={ + 'switch_info': 'my_switch_1', + }) + port3 = obj_utils.create_test_port( + self.context, id=989, + uuid='2be26c0b-03f2-4d2e-ae87-c02d7f33c784', + address='22:22:22:22:22:23', node_id=self.node.id, + local_link_connection={ + 'switch_info': 'my_switch_1', + }) + port4 = obj_utils.create_test_port( + self.context, id=990, + uuid='2be26c0b-03f2-4d2e-ae87-c02d7f33c786', + address='22:22:22:22:22:24', node_id=self.node.id, + local_link_connection={ + 'switch_info': 'my_switch_1', + }) + port5 = obj_utils.create_test_port( + self.context, id=991, + uuid='2be26c0b-03f2-4d2e-ae87-c02d7f33c788', + address='22:22:22:22:22:25', node_id=self.node.id, + local_link_connection={ + 'foo': 'bar', + }) + ports = [port1, port2, port3, port4, port5] + mock_list_by_nodeid.return_value = ports + + port_group.PortGroupsHook().__call__( + task, {}, {}) + + port1.refresh() + port2.refresh() + port3.refresh() + self.assertEqual(port1.portgroup_id, None) + # check no port group created + new_port_groups = objects.Portgroup.list_by_node_id( + self.context, self.node.id) + self.assertEqual(len(new_port_groups), 0) + self.assertIsNone(port2.portgroup_id) + self.assertIsNone(port3.portgroup_id)