Source code for ironic.drivers.modules.network.ironic_networking

#    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.

"""
Ironic Networking Network Interface

This is a network interface designed for standalone Ironic deployments that
require minimal network configuration. It implements all required
NetworkInterface methods but performs no actual operations, making it suitable
for environments where external network configuration is handled separately or
not required.
"""

from oslo_config import cfg

import jsonschema
from jsonschema import exceptions as json_schema_exc
from oslo_log import log

from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import network
from ironic.common import states
from ironic.conf import ironic_networking
from ironic.drivers import base
from ironic.drivers.modules.network import ironic_networking_schemas
from ironic.drivers.modules.network.switchport_config import SwitchPortConfig
from ironic.networking import api as networking_api

LOG = log.getLogger(__name__)

CONF = cfg.CONF

# Register networking configuration options
ironic_networking.register_opts(CONF)


[docs] class IronicNetworking(base.NetworkInterface): """Ironic Networking network interface. This network interface is designed for standalone Ironic deployments where configuration of switch ports should be handled by Ironic. Port Configuration: This interface validates and processes ports that have an 'extra' property containing a 'switchport' sub-property. The switchport configuration must conform to the SWITCHPORT_SCHEMA defined in ironic_networking_schemas.py. Expected switchport configuration format: - mode: 'access', 'trunk', or 'hybrid' - native_vlan: VLAN ID (required) - allowed_vlans: List of VLAN IDs (required for trunk/hybrid modes only) - lag_name: Optional LAG name Portgroup Configuration: This interface validates and processes portgroups that have an 'extra' property containing a 'lag' sub-property. The lag configuration must conform to the LAG_SCHEMA defined in ironic_networking_schemas.py. """ def __init__(self): """Initialize the IronicNetworking interface.""" super(IronicNetworking, self).__init__() self.switchport_schema = ironic_networking_schemas.SWITCHPORT_SCHEMA self.lag_schema = ironic_networking_schemas.LAG_SCHEMA
[docs] def validate(self, task): """Validate the network interface configuration. For the ironic networking interface, this validates any ports that have switchport configuration in their 'extra' field and any portgroups that have LAG configuration in their 'extra' field. :param task: A TaskManager instance. :raises: InvalidParameterValue if switchport or lag configuration is invalid. """ # Validate ports with switchport configuration for port in task.ports: self._validate_port_switchport_config(task, port) # Validate portgroups with LAG configuration for portgroup in task.portgroups: self._validate_portgroup_lag_config(portgroup) self._validate_portgroup_member_ports(task, portgroup)
@staticmethod def _get_switch_port_config(task, network_type): """Get the SwitchPortConfig for a given network type. The value is determined by first checking the node's driver_info for an override, and then falling back to the global conf option. :param task: A TaskManager instance. :param network_type: One of 'cleaning', 'provisioning', 'servicing', 'rescuing', or 'inspection'. :returns: A SwitchPortConfig instance, or None if not set. :raises: InvalidParameterValue if the network value is set but has an invalid format. """ node = task.node driver_info_key = f'{network_type}_network' conf_key = f'{network_type}_network' # Check driver_info override network_value = node.driver_info.get(driver_info_key) if not network_value: # Fallback to global conf try: network_value = getattr(CONF.ironic_networking, conf_key, '') LOG.debug("Retrieved config value for %s: %s", conf_key, network_value) except AttributeError as e: LOG.warning( "Networking configuration not available or option '%s' " "not found: %s", conf_key, e) network_value = '' if not network_value: LOG.warning( "No network configured for network type '%s'. " "Falling back to port's switchport attributes.", network_type ) return None config = SwitchPortConfig.from_string(network_value, network_type) LOG.debug( "Called _get_switch_port_config with node=%(node)s, " "network_type=%(network_type)s; returning %(config)s " "from value='%(value)s'", {'node': task.node.uuid, 'network_type': network_type, 'config': config, 'value': network_value}) return config def _validate_network_requirements(self, task, network_type): """Validate that at least one port has required network configuration. This method checks that at least one port on the node has both local_link_connection information and valid mode/native_vlan configuration either from the network configuration or the port's switchport configuration. :param task: A TaskManager instance. :param network_type: The type of network to validate (e.g., 'cleaning', 'rescuing', 'inspection', 'servicing'). :raises: InvalidParameterValue if unable to parse network configuration """ for port in task.ports: # Check if port has local_link_connection if not port.local_link_connection: continue # Try to get switchport info from port.extra switchport = port.extra.get('switchport') if port.extra else None if not switchport: continue # Get the SwitchPortConfig for the network type config = self._get_switch_port_config(task, network_type) # If network configuration is not set, use port's switchport info if config is None or not config.is_valid: config = SwitchPortConfig.from_switchport(switchport) # If we have valid configuration, we found at least one valid port if config and config.is_valid: return # TODO(alegacy): Need to consider how far the validation should go. # For example, the initial inspection may not have any ports so no # validation is required, but then later we could cause issues with # cleaning or rescuing if we require ports to be present and # configured. Needs more thought. Maybe just enforce that ports # that have switchport configuration must have a # local_link_connection? LOG.warning( _("Node %(node)s requires at least one port with " "local_link_connection and valid mode/native_vlan " "configuration for %(network_type)s network"), {'node': task.node.uuid, 'network_type': network_type}) def _validate_port_switchport_config(self, task, port): """Validate switchport configuration in port's extra field. :param task: A TaskManager instance. :param port: A Port object. :raises: InvalidParameterValue if switchport configuration is invalid. """ if not port.extra: return switchport_config = port.extra.get('switchport') if not switchport_config: return try: jsonschema.validate(switchport_config, self.switchport_schema) except json_schema_exc.ValidationError as e: raise exception.InvalidParameterValue( _("Invalid switchport configuration for port %(port)s: " "%(error)s") % {'port': port.uuid, 'error': e}) # Validate that PXE-enabled ports use access mode if port.pxe_enabled: mode = switchport_config.get('mode') if mode != 'access': raise exception.InvalidParameterValue( _("Port %(port)s is PXE-enabled but has switchport mode " "'%(mode)s'. PXE-enabled ports must use 'access' mode " "when switchport configuration is present.") % {'port': port.uuid, 'mode': mode}) # Validate that local_link_connection has required fields if not port.local_link_connection: # Inspection is the only state where not having a # local_link_connection is permitted since we expect it to be # populated by LLDP if it is available. If the intent is not to # use inspection then the user should have manually populated # this field. if task.node.provision_state not in states.INSPECTION_STATES: raise exception.InvalidParameterValue( _("Port %(port)s has switchport configuration but is " "missing local_link_connection in state: %(state)s.") % {'port': port.uuid, 'state': task.node.provision_state}) # During inspection, we allow this and just log it hoping that # inspection will populate it. LOG.debug( "Port %s is missing local_link_connection in state: %s", port.uuid, task.node.provision_state) return switch_id = port.local_link_connection.get('switch_id') port_id = port.local_link_connection.get('port_id') errors = [] if not switch_id: errors.append("'switch_id'") if not port_id: errors.append("'port_id'") if errors: raise exception.InvalidParameterValue( _("Port %(port)s local_link_connection missing required " "field(s): %(fields)s") % {'port': port.uuid, 'fields': ', '.join(errors)}) def _validate_portgroup_lag_config(self, portgroup): """Validate LAG configuration in portgroup's extra field. :param portgroup: A Portgroup object. :raises: InvalidParameterValue if LAG configuration is invalid. """ if not portgroup.extra: return lag_config = portgroup.extra.get('lag') if not lag_config: return try: jsonschema.validate(lag_config, self.lag_schema) LOG.debug("Portgroup %s LAG configuration is valid", portgroup.uuid) except json_schema_exc.ValidationError as e: raise exception.InvalidParameterValue( _("Invalid LAG configuration for portgroup " "%(portgroup)s: %(error)s") % {'portgroup': portgroup.uuid, 'error': e}) @staticmethod def _validate_portgroup_member_ports(task, portgroup): """Validate that portgroup member ports have switchport configuration. :param task: A TaskManager instance. :param portgroup: A Portgroup object. :raises: InvalidParameterValue if member ports lack switchport configuration. """ if not portgroup.extra or not portgroup.extra.get('lag'): return # Find member ports of this portgroup member_ports = [port for port in task.ports if port.portgroup_id == portgroup.id] if not member_ports: raise exception.InvalidParameterValue( _("Portgroup %(portgroup)s has LAG configuration " "but no member ports") % {'portgroup': portgroup.uuid}) # Check that all member ports have switchport configuration ports_without_switchport = [] ports_without_local_link = [] for port in member_ports: if not port.extra or not port.extra.get('switchport'): ports_without_switchport.append(port.uuid) elif (not port.local_link_connection or not port.local_link_connection.get('switch_id') or not port.local_link_connection.get('port_id')): ports_without_local_link.append(port.uuid) errors = [] if ports_without_switchport: errors.append( _("member ports %(ports)s lack switchport configuration") % {'ports': ', '.join(ports_without_switchport)}) if ports_without_local_link: errors.append( _("member ports %(ports)s lack proper local_link_connection " "with switch_id and port_id fields") % {'ports': ', '.join(ports_without_local_link)}) if errors: raise exception.InvalidParameterValue( _("Portgroup %(portgroup)s has LAG configuration but: " "%(errors)s") % {'portgroup': portgroup.uuid, 'errors': '; '.join(errors)}) @staticmethod def _get_portgroup_switch_ids(task, portgroup): """Get switch IDs from member ports of a portgroup. :param task: A TaskManager instance. :param portgroup: A Portgroup object. :returns: List of unique switch IDs from member ports, or None if no valid ports. """ if not portgroup.extra or not portgroup.extra.get('lag'): return None # Find member ports of this portgroup member_ports = [port for port in task.ports if port.portgroup_id == portgroup.id] if not member_ports: return None # Extract switch IDs from member ports' local_link_connection switch_ids = set() for port in member_ports: if (port.extra and port.extra.get('switchport') and port.local_link_connection): switch_id = port.local_link_connection.get('switch_id') if switch_id: switch_ids.add(switch_id) return list(switch_ids) if switch_ids else None @staticmethod def _get_portgroup_lag_name(portgroup): """Get LAG name from portgroup configuration. For now, we'll use the portgroup name as the LAG name. This could be enhanced to use a specific field in the future. :param portgroup: A Portgroup object. :returns: LAG name string. """ if portgroup.name: return portgroup.name else: # TODO(alegacy): naming of LAG instances needs to be re-visited # when that part of the feature is completed. Some switches will # want to name the LAG itself and others may have specific naming # requirements which may make this tricky at best. return f"lag-{portgroup.uuid[:8]}" @staticmethod def _get_port_switch_info(port): """Get switch ID and port name from port's local_link_connection. :param port: A Port object. :returns: Tuple of (switch_id, port_name) or (None, None) if not available. """ if not port.local_link_connection: return None, None switch_id = port.local_link_connection.get('switch_id') port_name = port.local_link_connection.get('port_id') return switch_id, port_name @staticmethod def _get_port_description(port): """Generate description for a port. :param port: A Port object. :returns: Description string. """ return f"Ironic Port {port.uuid}" @staticmethod def _get_portgroup_description(portgroup): """Generate description for a portgroup. :param portgroup: A Portgroup object. :returns: Description string. """ return f"Ironic PortGroup {portgroup.uuid}" def _resolve_network_configuration(self, task, port_obj, network_type): """Resolve network configuration for a port based on network type. :param task: A TaskManager instance. :param port_obj: A Port object. :param network_type: The type of network to resolve configuration for. :returns: A SwitchPortConfig instance, or None if not configured. :raises: InvalidParameterValue if unable to parse network configuration """ # Try network-specific configuration first config = self._get_switch_port_config(task, network_type) if config is None or not config.is_valid: # Fallback to port's switchport configuration unless the network # is the idle network in which case we simply allow the port to # remain unconfigured (or configured to the switch-wide default) if network_type == network.IDLE_NETWORK: return None switchport = (port_obj.extra.get('switchport', {}) if port_obj.extra else {}) config = SwitchPortConfig.from_switchport(switchport) LOG.debug( "Resolving network configuration for port %(port)s, " "network_type=%(network_type)s: %(config)s", { 'port': getattr(port_obj, 'uuid', None), 'network_type': network_type, 'config': config, }) return config def _get_original_port_config(self, task, port_obj): """Get original port configuration before changes. Called from port_changed() to retrieve the port's configuration from task.ports before the current changes were applied. This allows comparison between original and new values to determine what network operations are needed. :param task: A TaskManager instance. :param port_obj: The changed Port object with new values. :returns: Tuple of (original_port, original_switchport, original_local_link) where original_port is the Port object from task.ports, original_switchport is the switchport dict from port.extra, and original_local_link is the local_link_connection dict. Any value may be None if not found. """ original_port = None for port in task.ports: if port.uuid == port_obj.uuid: original_port = port break original_switchport = None original_local_link = None if original_port: original_switchport = (original_port.extra.get('switchport') if original_port.extra else None) original_local_link = original_port.local_link_connection return original_port, original_switchport, original_local_link def _determine_port_changes(self, original_switchport, current_switchport, original_local_link, current_local_link): """Determine what changes occurred to the port. Called from port_changed() after retrieving original configuration to classify the type of change that occurred. The result is used to decide which network operations (reset, update, etc.) are needed. :param original_switchport: The switchport dict from the original port.extra, or None. :param current_switchport: The switchport dict from the changed port.extra, or None. :param original_local_link: The original local_link_connection dict, or None. :param current_local_link: The current local_link_connection dict, or None. :returns: Tuple of (switchport_removed, local_link_removed, local_link_changed, switchport_changed, switchport_added) where each value is a boolean indicating that type of change. """ switchport_removed = original_switchport and not current_switchport local_link_removed = original_local_link and not current_local_link local_link_changed = (original_local_link != current_local_link and original_local_link and current_local_link) switchport_changed = (original_switchport != current_switchport and original_switchport and current_switchport) switchport_added = not original_switchport and current_switchport return (switchport_removed, local_link_removed, local_link_changed, switchport_changed, switchport_added) def _handle_port_removal(self, task, port_obj, original_port, original_local_link, original_switchport, active_network_type): """Handle port removal cases. Called when switchport or local_link_connection is removed from a port. Resets the switch port configuration using the original values. :param task: A TaskManager instance. :param port_obj: The changed Port object. :param original_port: The original Port object from task.ports. :param original_local_link: The original local_link_connection dict. :param original_switchport: The original switchport configuration. :param active_network_type: The network type that should be active. :raises: InvalidParameterValue if unable to parse network configuration :raises: NetworkError if the networking service call fails """ LOG.debug( "Port %(port)s switchport or local_link_connection removed", {'port': port_obj.uuid}) # Resolve the idle network configuration (if any) idle_config = self._get_switch_port_config(task, network.IDLE_NETWORK) if original_local_link and original_switchport: # Get configuration for reset operation using original values original_config = self._resolve_network_configuration( task, original_port, active_network_type) if original_config and original_config.is_valid: # Get switch and port info from original state switch_id = original_local_link.get('switch_id') port_name = original_local_link.get('port_id') if switch_id and port_name: idle_vlan = (idle_config.native_vlan if idle_config else None) result = networking_api.reset_port( task.context, switch_id, port_name, original_config.native_vlan, allowed_vlans=original_config.allowed_vlans, default_vlan=idle_vlan) LOG.debug( "Successfully reset port %(port_name)s on " "switch %(switch_id)s via networking service: " "%(result)s", {'port_name': port_name, 'switch_id': switch_id, 'result': result}) else: LOG.warning( "Cannot reset port %(port)s: missing " "switch_id or port_id", {'port': port_obj.uuid}) elif active_network_type != 'idle': LOG.warning( "Cannot reset port %(port)s: missing mode or " "native_vlan for %(active_network_type)s network", {'port': port_obj.uuid}) def _handle_local_link_change(self, task, port_obj, original_port, original_local_link, original_switchport, active_network_type): """Handle local_link_connection change (requires reset + update). Called when the local_link_connection changes on a port. Resets the old switch port configuration before the new configuration is applied. :param task: A TaskManager instance. :param port_obj: The changed Port object. :param original_port: The original Port object from task.ports. :param original_local_link: The original local_link_connection dict. :param original_switchport: The original switchport configuration. :param active_network_type: The network type that should be active. :raises: InvalidParameterValue if unable to parse network configuration :raises: NetworkError if the networking service call fails """ LOG.debug("Port %(port)s local_link_connection changed", {'port': port_obj.uuid}) # Resolve the idle network configuration (if any) idle_config = self._get_switch_port_config(task, network.IDLE_NETWORK) # First, reset the old port if original_local_link and original_switchport: original_config = self._resolve_network_configuration( task, original_port, active_network_type) if original_config and original_config.is_valid: switch_id = original_local_link.get('switch_id') port_name = original_local_link.get('port_id') if switch_id and port_name: idle_vlan = (idle_config.native_vlan if idle_config else None) result = networking_api.reset_port( task.context, switch_id, port_name, original_config.native_vlan, allowed_vlans=original_config.allowed_vlans, default_vlan=idle_vlan) LOG.debug( "Successfully reset old port %(port_name)s " "on switch %(switch_id)s via networking " "service: %(result)s", {'port_name': port_name, 'switch_id': switch_id, 'result': result}) def _handle_switchport_update(self, task, port_obj, current_local_link, current_switchport, active_network_type, switchport_added): """Handle switchport addition or modification. Called when switchport configuration is added or changed on a port. Validates the configuration and updates the switch port. :param task: A TaskManager instance. :param port_obj: The changed Port object. :param current_local_link: The current local_link_connection dict. :param current_switchport: The current switchport configuration. :param active_network_type: The network type that should be active. :param switchport_added: True if switchport was added, False if updated. :raises: InvalidParameterValue if unable to parse network configuration :raises: NetworkError if the networking service call fails """ if switchport_added: LOG.info("Port %(port)s switchport configuration added", {'port': port_obj.uuid}) else: LOG.info("Port %(port)s switchport configuration updated", {'port': port_obj.uuid}) if not current_local_link: return # Validate the current switchport configuration self._validate_port_switchport_config(task, port_obj) # Resolve the idle network configuration (if any) idle_config = self._get_switch_port_config(task, network.IDLE_NETWORK) # Resolve configuration based on active network type config = self._resolve_network_configuration( task, port_obj, active_network_type) if config and config.is_valid: # Get switch and port info from current state switch_id = current_local_link.get('switch_id') port_name = current_local_link.get('port_id') description = self._get_port_description(port_obj) lag_name = current_switchport.get('lag_name') if switch_id and port_name: idle_vlan = (idle_config.native_vlan if idle_config else None) result = networking_api.update_port( task.context, switch_id, port_name, description, config.mode, config.native_vlan, allowed_vlans=config.allowed_vlans, lag_name=lag_name, default_vlan=idle_vlan) LOG.debug( "Successfully updated port %(port_name)s " "on switch %(switch_id)s for %(network_type)s " "network via networking service: %(result)s", {'port_name': port_name, 'switch_id': switch_id, 'network_type': active_network_type, 'result': result}) else: LOG.warning( "Cannot update port %(port)s: missing " "switch_id or port_id", {'port': port_obj.uuid}) else: LOG.warning( "Cannot update port %(port)s: missing mode or " "native_vlan for %(network_type)s network", {'port': port_obj.uuid, 'network_type': active_network_type})
[docs] def port_changed(self, task, port_obj): """Handle any actions required when a port changes. In the ironic networking interface, this method processes ports that have switchport configuration in their 'extra' field and calls the networking service API accordingly based on the node's current provision state and what network should be active. :param task: A TaskManager instance. :param port_obj: A changed Port object. """ # Check if relevant fields have changed changed_fields = port_obj.obj_what_changed() if not ({'extra', 'local_link_connection'} & changed_fields): # No relevant changes, skip this port return # Determine what network should be active based on node state active_network_type = network.get_network_type_for_state( task.node.provision_state) LOG.debug( "Port %(port)s changed on node %(node)s in state %(state)s, " "active network type: %(network_type)s, changed fields: " "%(fields)s", {'port': port_obj.uuid, 'node': task.node.uuid, 'state': task.node.provision_state, 'network_type': active_network_type, 'fields': list(changed_fields)}) # Get current configuration current_switchport = (port_obj.extra.get('switchport') if port_obj.extra else None) current_local_link = port_obj.local_link_connection # Get original port configuration (before changes) (original_port, original_switchport, original_local_link) = self._get_original_port_config(task, port_obj) # Determine what actions need to be taken (switchport_removed, local_link_removed, local_link_changed, switchport_changed, switchport_added) = self._determine_port_changes( original_switchport, current_switchport, original_local_link, current_local_link) # Handle removal cases if switchport_removed or local_link_removed: self._handle_port_removal(task, port_obj, original_port, original_local_link, original_switchport, active_network_type) return # Handle local_link_connection change (requires reset + update) if local_link_changed: self._handle_local_link_change(task, port_obj, original_port, original_local_link, original_switchport, active_network_type) # Then configure the new port (fall through to update logic) switchport_changed = True # Force update with new configuration # Handle switchport addition or modification if switchport_added or switchport_changed: self._handle_switchport_update(task, port_obj, current_local_link, current_switchport, active_network_type, switchport_added)
[docs] def portgroup_changed(self, task, portgroup_obj): """Handle any actions required when a portgroup changes. In the ironic networking interface, portgroup changes are not currently supported and will be logged but no action will be taken. :param task: A TaskManager instance. :param portgroup_obj: A changed Portgroup object. """ LOG.debug( "Portgroup %(portgroup)s (%(name)s) configuration changed - " "portgroup changes not currently supported by " "ironic-networking interface", {'portgroup': portgroup_obj.uuid, 'name': portgroup_obj.name or 'unnamed'})
[docs] def vif_attach(self, task, vif_info): """Attach a virtual network interface to a node. In the ironic networking interface, VIF attachment is a no-op. This allows the operation to complete successfully without performing any actual network configuration. :param task: A TaskManager instance. :param vif_info: A dictionary of information about a VIF. It must have an 'id' key, whose value is a unique identifier for that VIF. """ pass
[docs] def vif_detach(self, task, vif_id): """Detach a virtual network interface from a node. In the ironic networking interface, VIF detachment is a no-op. This allows the operation to complete successfully without performing any actual network configuration. :param task: A TaskManager instance. :param vif_id: A VIF ID to detach. """ pass
[docs] def vif_list(self, task): """List attached VIF IDs for a node. In the ironic networking interface, no VIFs are ever attached, so this always returns an empty list. :param task: A TaskManager instance. :returns: Empty list as no VIFs are managed by this interface. """ return []
[docs] def get_current_vif(self, task, p_obj): """Return the currently used VIF associated with port or portgroup. In the ironic networking interface, no VIFs are managed, so this always returns None. :param task: A TaskManager instance. :param p_obj: Ironic port or portgroup object. :returns: None as no VIFs are managed by this interface. """ return None
def _add_tenant_networks(self, task): """Add tenant networks to a node. This method attempts to configure the tenant network for each port on the node using only the port's switchport configuration. If the switchport configuration is missing or incomplete, the port is skipped. :param task: A TaskManager instance. """ LOG.debug("Adding tenant networks to node %(node)s", {'node': task.node.uuid}) # Resolve the idle network configuration (if any) idle_config = self._get_switch_port_config(task, network.IDLE_NETWORK) for port in task.ports: switchport = port.extra.get('switchport') if port.extra else None link_info = port.local_link_connection if not switchport or not link_info: LOG.debug( "Skipping port %(port)s: missing switchport or " "local_link_connection info for tenant network setup.", {'port': port.uuid} ) continue config = SwitchPortConfig.from_switchport(switchport) if not config or not config.is_valid: LOG.debug( "Skipping port %(port)s: missing mode or native_vlan or " "allowed_vlans for tenant network setup.", {'port': port.uuid} ) continue try: idle_vlan = (idle_config.native_vlan if idle_config else None) networking_api.update_port( task.context, link_info.get('switch_id'), link_info.get('port_id'), self._get_port_description(port), config.mode, config.native_vlan, allowed_vlans=config.allowed_vlans, lag_name=None, default_vlan=idle_vlan, ) except (exception.InvalidParameterValue, exception.NetworkError) as exc: LOG.error( "Failed to update port %(port)s for tenant network: " "%(err)s", {'port': port.uuid, 'err': exc} ) raise exception.NetworkError( _("Failed to configure tenant network for port " "%(port)s: %(err)s") % {'port': port.uuid, 'err': exc} ) def _remove_tenant_networks(self, task): """Remove tenant networks from a node. This method attempts to reset the tenant network configuration for each port on the node. It uses the tenant network configuration if set, otherwise falls back to the port's switchport configuration. If neither is available, the port is skipped with a log message. :param task: A TaskManager instance. """ LOG.debug("Removing tenant networks from node %(node)s", {'node': task.node.uuid}) # Resolve the idle network configuration (if any) idle_config = self._get_switch_port_config(task, network.IDLE_NETWORK) errors = [] for port in task.ports: switchport = port.extra.get('switchport') if port.extra else None link_info = port.local_link_connection if not switchport or not link_info: LOG.debug( "Skipping port %(port)s: missing switchport or " "local_link_connection info for tenant network removal.", {'port': port.uuid} ) continue config = SwitchPortConfig.from_switchport(switchport) if not config or not config.is_valid: LOG.debug( "Skipping port %(port)s: missing mode or native_vlan or " "allowed_vlans for tenant network removal.", {'port': port.uuid} ) continue try: idle_vlan = (idle_config.native_vlan if idle_config else None) networking_api.reset_port( task.context, link_info.get('switch_id'), link_info.get('port_id'), config.native_vlan, allowed_vlans=config.allowed_vlans, default_vlan=idle_vlan ) except (exception.InvalidParameterValue, exception.NetworkError) as exc: # Accumulate errors to ensure that we attempt to reset each # port regardless of if any single port had an error. message = (f"Failed to reset tenant network for " f"port {port.uuid}: {exc}") LOG.error(message) errors.append(message) if len(errors) > 0: raise exception.NetworkError( _("Failed to reset ports for tenant network, " "errors: %(errors)s") % {"errors": errors}) def _add_network(self, task, network_type): """Add a network to a node. This method attempts to configure the specified network for each port on the node. It uses the network configuration if set, otherwise falls back to the port's switchport configuration. If neither is available, the port is skipped with a log message. :param task: A TaskManager instance. :param network_type: The type of network to add (e.g., 'provisioning', 'cleaning', 'rescuing', 'inspection', 'servicing'). """ LOG.info("Adding %(network_type)s network to node %(node)s", {'network_type': network_type, 'node': task.node.uuid}) # Resolve the idle network configuration (if any) idle_config = self._get_switch_port_config(task, network.IDLE_NETWORK) # Get the config for the network type. It may be # overridden by the port's switchport configuration. global_config = self._get_switch_port_config(task, network_type) for port in task.ports: # If the local_link_connection info is missing, skip the port link_info = port.local_link_connection if not link_info: LOG.debug( "Skipping port %(port)s: missing local_link_connection " "info for %(network_type)s network setup.", {'port': port.uuid, 'network_type': network_type} ) continue # If the global config is not complete, try to get # switchport info from port.extra config = global_config if config is None or not config.is_valid: switchport = (port.extra.get('switchport') if port.extra else None) if switchport: config = SwitchPortConfig.from_switchport(switchport) if not config or not config.is_valid: LOG.debug( "Skipping port %(port)s: missing mode or native_vlan or " "allowed_vlans for %(network_type)s network setup.", {'port': port.uuid, 'network_type': network_type} ) continue try: idle_vlan = (idle_config.native_vlan if idle_config else None) networking_api.update_port( task.context, link_info.get('switch_id'), link_info.get('port_id'), self._get_port_description(port), config.mode, config.native_vlan, allowed_vlans=config.allowed_vlans, lag_name=None, default_vlan=idle_vlan ) LOG.debug( "Configured %(network_type)s network for port %(port)s: " "switch_id=%(switch_id)s, port_name=%(port_name)s, " "config=%(config)s", { 'network_type': network_type, 'port': port.uuid, 'switch_id': link_info.get('switch_id'), 'port_name': link_info.get('port_id'), 'config': config, } ) except (exception.InvalidParameterValue, exception.NetworkError) as exc: LOG.error( "Failed to configure %(network_type)s network for port " "%(port)s: %(err)s", {'network_type': network_type, 'port': port.uuid, 'err': exc} ) raise exception.NetworkError( _("Failed to configure %(network_type)s network for port " "%(port)s: %(err)s") % {'network_type': network_type, 'port': port.uuid, 'err': exc} ) def _remove_network(self, task, network_type): """Remove a network from a node. This method attempts to reset the specified network configuration for each port on the node. It uses the network configuration if set, otherwise falls back to the port's switchport configuration. If neither is available, the port is skipped with a log message. :param task: A TaskManager instance. :param network_type: The type of network to remove (e.g., 'provisioning', 'cleaning', 'rescuing', 'inspection', 'servicing'). """ LOG.info("Removing %(network_type)s network from node %(node)s", {'network_type': network_type, 'node': task.node.uuid}) # Resolve the idle network configuration (if any) idle_config = self._get_switch_port_config(task, network.IDLE_NETWORK) # Get the config for the network type. It may be # overridden by the port's switchport configuration. global_config = self._get_switch_port_config(task, network_type) errors = [] for port in task.ports: # Get the switch and port info from the port's # local_link_connection link_info = port.local_link_connection if not link_info: LOG.debug( "Skipping port %(port)s: missing local_link_connection " "info for %(network_type)s network removal.", {'port': port.uuid, 'network_type': network_type} ) continue # If the global config is not complete, try to get # switchport info from port.extra config = global_config if config is None or not config.is_valid: switchport = (port.extra.get('switchport') if port.extra else None) if switchport: config = SwitchPortConfig.from_switchport(switchport) if not config or not config.is_valid: LOG.debug( "Skipping port %(port)s: missing mode or native_vlan or " "allowed_vlans for %(network_type)s network removal.", {'port': port.uuid, 'network_type': network_type} ) continue try: idle_vlan = (idle_config.native_vlan if idle_config else None) networking_api.reset_port( task.context, link_info.get('switch_id'), link_info.get('port_id'), config.native_vlan, allowed_vlans=config.allowed_vlans, default_vlan=idle_vlan ) LOG.debug( "Reset %(network_type)s network for port %(port)s: " "switch_id=%(switch_id)s, port_name=%(port_name)s, " "config=%(config)s", { 'network_type': network_type, 'port': port.uuid, 'switch_id': link_info.get('switch_id'), 'port_name': link_info.get('port_id'), 'config': config, } ) except (exception.InvalidParameterValue, exception.NetworkError) as exc: # Accumulate errors to ensure that we attempt to reset each # port regardless of if any single port had an error. message = (f"Failed to reset {network_type} network for " f"port {port.uuid}: {exc}") LOG.error(message) errors.append(message) if len(errors) > 0: raise exception.NetworkError( _("Failed to reset %(network_type)s network, " "errors: %(errors)s") % {"network_type": network_type, "errors": errors})
[docs] def add_provisioning_network(self, task): """Add the provisioning network to a node. This method configures the provisioning network for each port on the node by applying the port's switchport configuration with the provisioning network VLAN/segment ID override if set. :param task: A TaskManager instance. """ self._add_network(task, network.PROVISIONING_NETWORK)
[docs] def remove_provisioning_network(self, task): """Remove the provisioning network from a node. This method resets the provisioning network configuration for each port on the node by restoring the port's default switchport configuration. :param task: A TaskManager instance. """ self._remove_network(task, network.PROVISIONING_NETWORK)
[docs] def configure_tenant_networks(self, task): """Configure tenant networks for a node. This method configures tenant network connectivity for each port on the node by applying the port's switchport configuration with any tenant network overrides. :param task: A TaskManager instance. """ self._add_tenant_networks(task)
[docs] def unconfigure_tenant_networks(self, task): """Unconfigure tenant networks for a node. This method removes tenant network configuration for each port on the node by restoring the port's default switchport configuration. :param task: A TaskManager instance. """ self._remove_tenant_networks(task)
[docs] def add_cleaning_network(self, task): """Add the cleaning network to a node. This method configures the cleaning network for each port on the node by applying the port's switchport configuration with the cleaning network VLAN/segment ID override if set. :param task: A TaskManager instance. :returns: Empty dictionary as no ports are configured. """ self._add_network(task, network.CLEANING_NETWORK)
[docs] def remove_cleaning_network(self, task): """Remove the cleaning network from a node. This method resets the cleaning network configuration for each port on the node by restoring the port's default switchport configuration. :param task: A TaskManager instance. """ self._remove_network(task, network.CLEANING_NETWORK)
[docs] def validate_rescue(self, task): """Validate the network interface for rescue operation. This method validates that at least one port has the required network configuration for rescue operations. :param task: A TaskManager instance. :raises: InvalidParameterValue if unable to parse network configuration """ self._validate_network_requirements(task, network.RESCUING_NETWORK)
[docs] def add_rescuing_network(self, task): """Add the rescuing network to the node. This method configures the rescuing network for each port on the node by applying the port's switchport configuration with the rescuing network VLAN/segment ID override if set. :param task: A TaskManager instance. :returns: Empty dictionary as no ports are configured. """ self._add_network(task, network.RESCUING_NETWORK)
[docs] def remove_rescuing_network(self, task): """Remove the rescuing network from a node. This method resets the rescuing network configuration for each port on the node by restoring the port's default switchport configuration. :param task: A TaskManager instance. """ self._remove_network(task, network.RESCUING_NETWORK)
[docs] def validate_inspection(self, task): """Validate that the node has required properties for inspection. This method validates that at least one port has the required network configuration for inspection operations. :param task: A TaskManager instance with the node being checked. :raises: InvalidParameterValue if unable to parse network configuration """ self._validate_network_requirements(task, network.INSPECTION_NETWORK)
[docs] def add_inspection_network(self, task): """Add the inspection network to the node. This method configures the inspection network for each port on the node by applying the port's switchport configuration with the inspection network VLAN/segment ID override if set. :param task: A TaskManager instance. :returns: Empty dictionary as no ports are configured. """ self._add_network(task, network.INSPECTION_NETWORK)
[docs] def remove_inspection_network(self, task): """Remove the inspection network from a node. This method resets the inspection network configuration for each port on the node by restoring the port's default switchport configuration. :param task: A TaskManager instance. """ self._remove_network(task, network.INSPECTION_NETWORK)
[docs] def need_power_on(self, task): """Check if node must be powered on before applying network changes. Switch operations can be performed without powering on the node. :param task: A TaskManager instance. :returns: False as no power state changes are needed. """ return False
[docs] def get_node_network_data(self, task): """Return network configuration for node NICs. This method returns network configuration data for the node. It first checks if static network data is configured on the node itself. If present, that takes precedence. Otherwise, it builds network data from the ports and portgroups attached to the node. The network data is returned in Nova network metadata layout (`network_data.json`) format. For the ironic-networking interface, this generates: - Physical links (type: "phy") for each port with a MAC address - VLAN interfaces (type: "vlan") for ports with allowed_vlans configured in their switchport settings :param task: A TaskManager instance. :returns: a dict holding network configuration information adhering to Nova network metadata layout (`network_data.json`). """ # Static network data takes precedence if task.node.network_data: return task.node.network_data LOG.debug('Building network data from ports for node %(node)s', {'node': task.node.uuid}) links = [] # Process each port to build physical and VLAN links for port in task.ports: # Skip ports without MAC addresses if not port.address: LOG.debug('Skipping port %(port)s: no MAC address', {'port': port.uuid}) continue # Create physical link for this port phy_link = { 'id': port.uuid, 'type': 'phy', 'ethernet_mac_address': port.address, 'mtu': 1500 } links.append(phy_link) # Check for VLAN configuration in switchport settings switchport = port.extra.get('switchport', {}) if port.extra else {} allowed_vlans = switchport.get('allowed_vlans', []) # Generate VLAN interfaces for each allowed VLAN for vlan_id in allowed_vlans: vlan_link = { 'id': f'{port.uuid}_vlan_{vlan_id}', 'type': 'vlan', 'vlan_mac_address': port.address, 'vlan_id': vlan_id, 'vlan_link': port.uuid, 'mtu': 1500 } links.append(vlan_link) LOG.debug('Generated network data for node %(node)s: %(links)d links', {'node': task.node.uuid, 'links': len(links)}) # TODO(alegacy): enhance this for LAG support return {'links': links}
[docs] def add_servicing_network(self, task): """Add the servicing network to the node. This method configures the servicing network for each port on the node by applying the port's switchport configuration with the servicing network VLAN/segment ID override if set. :param task: A TaskManager instance. :returns: Empty dictionary as no ports are configured. """ self._add_network(task, network.SERVICING_NETWORK)
[docs] def remove_servicing_network(self, task): """Remove the servicing network from a node. This method resets the servicing network configuration for each port on the node by restoring the port's default switchport configuration. :param task: A TaskManager instance. """ self._remove_network(task, network.SERVICING_NETWORK)