Source code for ironic.networking.manager

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

"""Networking service manager for Ironic.

The networking service handles network-related operations for Ironic,
providing RPC interfaces for configuring switch ports and network settings.
"""

import functools
import inspect

from oslo_log import log
import oslo_messaging as messaging

from ironic.common import exception
from ironic.common.exception import ConfigInvalid
from ironic.common.i18n import _
from ironic.common import metrics_utils
from ironic.common import rpc
from ironic.conf import CONF
from ironic.drivers.modules.switch.base import SwitchDriverException
from ironic.drivers.modules.switch.base import SwitchMethodNotImplemented
from ironic.networking import switch_config
from ironic.networking.switch_drivers import driver_adapter
from ironic.networking.switch_drivers import driver_factory
from ironic.networking import utils as networking_utils

LOG = log.getLogger(__name__)

METRICS = metrics_utils.get_metrics_logger(__name__)


[docs] def validate_vlan_configuration( operation, switch_id_arg_name="switch_id", ): """Decorator to validate VLAN configuration against allowed/denied lists. This decorator extracts native_vlan and allowed_vlans from method arguments and validates them against the switch's VLAN configuration. :param operation: Description of the operation for error messages. :param switch_id_arg_name: Name of the argument containing the switch ID. For multi-switch operations, this should be the argument containing the list of switch IDs. :returns: Decorated function that applies VLAN validation. """ def decorator(func): # Precompute the function signature at decoration time sig = inspect.signature(func) @functools.wraps(func) def wrapper(self, context, *args, **kwargs): """Wrapper to validate VLAN config before executing operation.""" # Use the precomputed signature to bind the arguments bound_args = sig.bind(self, context, *args, **kwargs) bound_args.apply_defaults() # Extract VLAN-related arguments native_vlan = bound_args.arguments.get("native_vlan", None) allowed_vlans = bound_args.arguments.get("allowed_vlans", []) switch_id_value = bound_args.arguments.get( switch_id_arg_name, None ) # For multi-switch operations, use the primary (first) switch if isinstance(switch_id_value, (list, tuple)) and switch_id_value: primary_switch_id = switch_id_value[0] else: primary_switch_id = switch_id_value # Only validate if we have a switch_id and native_vlan if primary_switch_id and native_vlan is not None: # Get the switch driver driver = self._get_switch_driver(primary_switch_id) # Build list of VLANs to check vlans_to_check = [] if native_vlan: vlans_to_check.append(native_vlan) if allowed_vlans: vlans_to_check.extend(allowed_vlans) # Validate VLAN configuration switch_config.validate_vlan_configuration( vlans_to_check, driver, primary_switch_id, operation, ) return func(self, context, *args, **kwargs) return wrapper return decorator
def _get_switch_config_filename(): return CONF.ironic_networking.driver_config_dir + "/switches.conf"
[docs] class NetworkingManager(object): """Ironic Networking service manager.""" # NOTE(alegacy): This must be in sync with rpcapi.NetworkingAPI's. RPC_API_VERSION = "1.0" target = messaging.Target(version=RPC_API_VERSION) def __init__(self, host, topic=None): if not host: host = CONF.host self.host = host if topic is None: if networking_utils.rpc_transport() == "json-rpc": # Always use the JSON-RPC config for both topic and host host_ip = CONF.ironic_networking_json_rpc.host_ip port = CONF.ironic_networking_json_rpc.port topic_host = f"{host_ip}:{port}" topic = f"ironic.{topic_host}" self.host = topic_host else: topic = rpc.NETWORKING_TOPIC self.topic = topic # Tell the RPC service which json-rpc config group to use for # networking. This enables separate listener configuration. self.json_rpc_conf_group = "ironic_networking_json_rpc" # Initialize driver adapter for configuration preprocessing self._driver_adapter = None # Initialize switch driver factory (will be set properly in init_host) self._switch_driver_factory = None
[docs] def prepare_host(self): """Prepare host for networking service initialization. This method is called by the RPC service before starting the listener. Since networking host configuration is now handled in __init__, this method is a no-op. """ pass
[docs] def init_host(self, admin_context=None): """Initialize the networking service host. This method implements two-phase driver initialization: 1. Load driver classes (but don't initialize instances yet) 2. Get translators from driver classes and preprocess config 3. Initialize driver instances (config files now exist) :param admin_context: admin context (unused but kept for compatibility) """ LOG.info("Initializing networking service on host %s", self.host) # Phase 1: Load driver classes (invoke_on_load=False) self._switch_driver_factory = ( driver_factory.get_switch_driver_factory() ) available_drivers = self._switch_driver_factory.names if not available_drivers: LOG.error("No switch drivers loaded") raise ConfigInvalid(_("No switch drivers loaded")) LOG.info("Available switch drivers: %s", ", ".join(available_drivers)) # Phase 2: Get driver classes and create adapter try: # Get driver classes (not instances) driver_classes = self._switch_driver_factory.get_driver_classes() # Create adapter with driver classes self._driver_adapter = ( driver_adapter.NetworkingDriverAdapter(driver_classes) ) # Preprocess config - writes driver-specific config files count = self._driver_adapter.preprocess_config( _get_switch_config_filename() ) LOG.info( "Generated %d driver-specific config files during init", count ) except Exception as e: LOG.exception("Failed to preprocess driver configuration: %s", e) raise # Phase 3: Now initialize driver instances (config files are ready) try: self._switch_driver_factory.initialize_drivers() LOG.info("Successfully initialized switch driver instances") except Exception as e: LOG.exception("Failed to initialize switch drivers: %s", e) raise
def _get_switch_driver(self, switch_id): """Get the appropriate switch driver for a switch. This method finds the correct driver for a switch by checking each available driver to see if it is configured to handle the switch. If multiple drivers can handle the same switch, the first one found is used. :param switch_id: Identifier of the switch. :returns: Switch driver instance. :raises: NetworkError if no drivers are available. :raises: SwitchNotFound if no driver supports the switch. """ available_drivers = self._switch_driver_factory.names if not available_drivers: raise exception.NetworkError( _( "No switch drivers are available. Please configure " "enabled_switch_drivers in the networking section." ) ) # Check each driver to see if it can handle this switch for driver_name in available_drivers: try: driver = self._switch_driver_factory.get_driver(driver_name) # Check if this driver is configured for this switch if driver.is_switch_configured(switch_id): LOG.debug( "Using switch driver '%s' for switch '%s'", driver_name, switch_id, ) return driver except exception.DriverNotFound: LOG.warning( "Switch driver '%s' not found, skipping", driver_name ) continue except Exception as e: LOG.warning( "Error checking driver '%s' for switch '%s': %s", driver_name, switch_id, e, ) continue # No driver found that supports this switch raise exception.SwitchNotFound(switch_id=switch_id) def _update_port_impl( self, switch_id, port_name, description, mode, native_vlan, allowed_vlans=None, default_vlan=None, lag_name=None, ): """Implementation of port update operation. :param switch_id: Identifier of the network switch. :param port_name: Name of the port to update. :param description: Description for the port. :param mode: Port mode (e.g., 'access', 'trunk'). :param native_vlan: VLAN ID to be removed from the port. :param allowed_vlans: Allowed VLAN IDs to be removed (optional). :param default_vlan: VLAN ID to restore onto the port (optional). :param lag_name: Name of the LAG (optional). :returns: Dictionary containing the updated port configuration. """ # Get the appropriate switch driver driver = self._get_switch_driver(switch_id) # Call the driver's update_port method driver.update_port( switch_id, port_name, description, mode, native_vlan, allowed_vlans=allowed_vlans, default_vlan=default_vlan, lag_name=lag_name, ) # Return configuration summary port_config = { "switch_id": switch_id, "port_name": port_name, "description": description, "mode": mode, "native_vlan": native_vlan, "allowed_vlans": allowed_vlans or [], "default_vlan": default_vlan, "lag_name": lag_name, "status": "configured", } LOG.info( "Successfully configured port %(port)s on switch %(switch)s", {"port": port_name, "switch": switch_id}, ) return port_config @METRICS.timer("NetworkingManager.update_port") @messaging.expected_exceptions( exception.InvalidParameterValue, exception.NetworkError, exception.SwitchNotFound, ) @validate_vlan_configuration("update_port") def update_port( self, context, switch_id, port_name, description, mode, native_vlan, allowed_vlans=None, default_vlan=None, lag_name=None, ): """Update a network switch port configuration. :param context: request context. :param switch_id: Identifier of the network switch. :param port_name: Name of the port to update. :param description: Description for the port. :param mode: Port mode (e.g., 'access', 'trunk'). :param native_vlan: VLAN ID to be removed from the port. :param allowed_vlans: Allowed VLAN IDs to be removed (optional). :param default_vlan: VLAN ID to restore onto the port (optional). :param lag_name: Name of the LAG if port is part of a link aggregation group (optional). :raises: InvalidParameterValue if validation fails. :raises: NetworkError if the network operation fails. :returns: Dictionary containing the updated port configuration. """ LOG.debug( "RPC update_port called for switch %(switch)s, port %(port)s", {"switch": switch_id, "port": port_name}, ) # Validate mode valid_modes = ["access", "trunk"] if mode not in valid_modes: raise exception.InvalidParameterValue( _("mode must be one of: %s") % ", ".join(valid_modes) ) # Validate VLAN ID if ( not isinstance(native_vlan, int) or native_vlan < 1 or native_vlan > 4094 ): raise exception.InvalidParameterValue( _("native_vlan must be an integer between 1 and 4094") ) # Validate allowed_vlans if provided if allowed_vlans is not None: if not isinstance(allowed_vlans, (list, tuple)): raise exception.InvalidParameterValue( _("allowed_vlans must be a list or tuple") ) for vlan in allowed_vlans: if not isinstance(vlan, int) or vlan < 1 or vlan > 4094: raise exception.InvalidParameterValue( _( "Each VLAN in allowed_vlans must be an integer " "between 1 and 4094" ) ) try: return self._update_port_impl( switch_id, port_name, description, mode, native_vlan, allowed_vlans=allowed_vlans, default_vlan=default_vlan, lag_name=lag_name, ) except exception.InvalidParameterValue: # Re-raise validation errors as-is raise except exception.NetworkError: # Re-raise NetworkError as-is raise except Exception as e: LOG.exception( "Failed to configure port %(port)s on switch " "%(switch)s", {"port": port_name, "switch": switch_id}, ) raise exception.NetworkError( _("Failed to configure network port: %s") % e ) from e def _reset_port_impl( self, switch_id, port_name, native_vlan, allowed_vlans=None, default_vlan=None ): """Implementation of port reset operation. :param switch_id: Identifier of the network switch. :param port_name: Name of the port to reset. :param native_vlan: VLAN ID to be removed from the port. :param allowed_vlans: Allowed VLAN IDs to be removed (optional). :param default_vlan: VLAN ID to restore onto the port (optional). :returns: Dictionary containing the reset port configuration. """ # Get the appropriate switch driver driver = self._get_switch_driver(switch_id) # Call the driver's reset_port method driver.reset_port( switch_id, port_name, native_vlan, allowed_vlans=allowed_vlans, default_vlan=default_vlan ) # Return reset configuration summary port_config = { "switch_id": switch_id, "port_name": port_name, "description": "Default port configuration", "mode": "access", "native_vlan": native_vlan, "allowed_vlans": [], "default_vlan": default_vlan, "status": "reset", } LOG.info( "Successfully reset port %(port)s on switch %(switch)s", {"port": port_name, "switch": switch_id}, ) return port_config @METRICS.timer("NetworkingManager.reset_port") @messaging.expected_exceptions( exception.InvalidParameterValue, exception.NetworkError, exception.SwitchNotFound, ) def reset_port( self, context, switch_id, port_name, native_vlan, allowed_vlans=None, default_vlan=None, ): """Reset a network switch port to default configuration. :param context: request context. :param switch_id: Identifier of the network switch. :param port_name: Name of the port to reset. :param native_vlan: VLAN ID to be removed from the port. :param allowed_vlans: Allowed VLAN IDs to be removed (optional). :param default_vlan: VLAN ID to restore onto the port (optional). :raises: InvalidParameterValue if validation fails. :raises: NetworkError if the network operation fails. :returns: Dictionary containing the reset port configuration. """ LOG.debug( "RPC reset_port called for switch %(switch)s, port %(port)s", {"switch": switch_id, "port": port_name}, ) try: return self._reset_port_impl( switch_id, port_name, native_vlan, allowed_vlans=allowed_vlans, default_vlan=default_vlan ) except exception.InvalidParameterValue: # Re-raise validation errors as-is raise except exception.NetworkError: # Re-raise NetworkError as-is raise except exception.SwitchNotFound: # Re-raise SwitchNotFound as-is raise except Exception as e: LOG.exception( "Failed to reset port %(port)s on switch " "%(switch)s", {"port": port_name, "switch": switch_id}, ) raise exception.NetworkError( _("Failed to reset network port: %s") % e ) from e @METRICS.timer("NetworkingManager.update_lag") @messaging.expected_exceptions( exception.InvalidParameterValue, exception.NetworkError, exception.SwitchNotFound, exception.Invalid, SwitchMethodNotImplemented, ) @validate_vlan_configuration("update_lag", switch_id_arg_name="switch_ids") def update_lag( self, context, switch_ids, lag_name, description, mode, native_vlan, aggregation_mode, allowed_vlans=None, default_vlan=None, ): """Update a link aggregation group (LAG) configuration. :param context: request context. :param switch_ids: List of switch identifiers. :param lag_name: Name of the LAG to update. :param description: Description for the LAG. :param mode: LAG mode (e.g., 'access', 'trunk'). :param native_vlan: VLAN ID to be removed from the port. :param aggregation_mode: Aggregation mode (e.g., 'lacp', 'static'). :param allowed_vlans: Allowed VLAN IDs to be removed (optional). :param default_vlan: VLAN ID to restore onto the port (optional). :raises: InvalidParameterValue if validation fails. :raises: NetworkError if the network operation fails. :raises: SwitchMethodNotImplemented - LAG is not yet supported. :returns: Dictionary containing the updated LAG configuration. """ raise SwitchMethodNotImplemented( _("LAG operations are not yet supported") ) @METRICS.timer("NetworkingManager.delete_lag") @messaging.expected_exceptions( exception.InvalidParameterValue, exception.NetworkError, exception.SwitchNotFound, exception.Invalid, SwitchMethodNotImplemented, ) def delete_lag(self, context, switch_ids, lag_name): """Delete a link aggregation group (LAG) configuration (stub). :param context: request context. :param switch_ids: List of switch identifiers. :param lag_name: Name of the LAG to delete. :raises: InvalidParameterValue if validation fails. :raises: NetworkError if the network operation fails. :raises: SwitchMethodNotImplemented - LAG is not yet supported. :returns: Dictionary containing the deletion status. """ raise SwitchMethodNotImplemented( _("LAG operations are not yet supported") ) @METRICS.timer("NetworkingManager.get_switches") @messaging.expected_exceptions(exception.NetworkError) def get_switches(self, context): """Get information about all switches from all drivers. :param context: Request context :returns: Dictionary of switch_id -> switch_info dictionaries :raises: NetworkError if driver operations fail """ switches = {} if self._switch_driver_factory is None: LOG.debug("Switch driver factory not available") return switches driver_names = self._switch_driver_factory.names if not driver_names: LOG.debug("No switch drivers available") return switches for driver_name in driver_names: try: driver = self._switch_driver_factory.get_driver(driver_name) except (exception.DriverNotFound, exception.DriverLoadError) as e: LOG.error( "Error accessing driver %(driver)s: %(error)s", {"driver": driver_name, "error": e} ) raise exception.NetworkError( _("Error accessing driver %(driver)s: %(error)s") % { "driver": driver_name, "error": e } ) from e try: switch_ids = driver.get_switch_ids() for switch_id in switch_ids: switch_info = driver.get_switch_info(switch_id) if switch_info: switches[switch_id] = switch_info except SwitchDriverException as e: LOG.error( "Failed to get switch information from driver " "%(driver)s: %(error)s", {"driver": driver_name, "error": e} ) raise exception.NetworkError( _("Failed to get switch information from driver " "%(driver)s: %(error)s") % { "driver": driver_name, "error": e } ) from e LOG.info("Successfully retrieved %(count)d switch config sections", {"count": len(switches)}) return switches
[docs] def cleanup(self): """Clean up resources.""" pass