Merge "Implement networking service RPC API methods"

This commit is contained in:
Zuul
2025-12-09 17:33:02 +00:00
committed by Gerrit Code Review
2 changed files with 1642 additions and 30 deletions

View File

@@ -17,6 +17,9 @@ 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
@@ -25,12 +28,85 @@ from ironic.common.i18n import _
from ironic.common import metrics_utils
from ironic.common import rpc
from ironic.conf import CONF
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__)
def validate_vlan_configuration(
switch_id_arg_name="switch_id", operation_description="operation"
):
"""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 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.
:param operation_description: Description of the operation for error
messages.
: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 = [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_description,
)
return func(self, context, *args, **kwargs)
return wrapper
return decorator
def _get_switch_config_filename():
return CONF.ironic_networking.driver_config_dir + "/switches.conf"
class NetworkingManager(object):
"""Ironic Networking service manager."""
@@ -44,17 +120,33 @@ class NetworkingManager(object):
host = CONF.host
self.host = host
if topic is None:
topic = rpc.NETWORKING_TOPIC
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
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
@@ -64,10 +156,150 @@ class NetworkingManager(object):
:param admin_context: admin context (unused but kept for compatibility)
"""
LOG.info("Initializing networking service on host %s", self.host)
LOG.warning(
"Networking service initialized with stub implementations. "
"Driver framework not yet loaded."
# Initialize driver adapter for configuration preprocessing
try:
self._driver_adapter = driver_adapter.NetworkingDriverAdapter()
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.error("Failed to initialize driver adapter: %s", e)
raise e
# Initialize switch driver factory
self._switch_driver_factory = (
driver_factory.get_switch_driver_factory()
)
available_drivers = self._switch_driver_factory.names
if available_drivers:
LOG.info(
"Networking service initialized with switch drivers: %s",
", ".join(available_drivers),
)
else:
# Allow service to start for monitoring and dynamic reload,
# but warn about limited functionality
LOG.warning(
"No switch drivers loaded - networking service will "
"operate without switch management capabilities. "
"Configure enabled_switch_drivers to enable "
"functionality."
)
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,
str(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(
@@ -75,6 +307,7 @@ class NetworkingManager(object):
exception.NetworkError,
exception.SwitchNotFound,
)
@validate_vlan_configuration(operation_description="port configuration")
def update_port(
self,
context,
@@ -87,7 +320,7 @@ class NetworkingManager(object):
default_vlan=None,
lag_name=None,
):
"""Update a network switch port configuration (stub).
"""Update a network switch port configuration.
:param context: request context.
:param switch_id: Identifier of the network switch.
@@ -99,18 +332,115 @@ class NetworkingManager(object):
: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: NotImplementedError - Implementation not loaded
:raises: InvalidParameterValue if validation fails.
:raises: NetworkError if the network operation fails.
:returns: Dictionary containing the updated port configuration.
"""
LOG.warning(
"update_port called but driver framework not loaded: "
"switch=%s, port=%s",
switch_id,
port_name,
LOG.info(
"RPC update_port called for switch %(switch)s, port %(port)s",
{"switch": switch_id, "port": port_name},
)
raise exception.NetworkError(
_("Network driver framework not yet loaded")
# 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.error(
"Failed to configure port %(port)s on switch "
"%(switch)s: %(error)s",
{"port": port_name, "switch": switch_id, "error": str(e)},
)
raise exception.NetworkError(
_("Failed to configure network port: %s") % str(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,
@@ -126,7 +456,7 @@ class NetworkingManager(object):
allowed_vlans=None,
default_vlan=None,
):
"""Reset a network switch port to default configuration (stub).
"""Reset a network switch port to default configuration.
:param context: request context.
:param switch_id: Identifier of the network switch.
@@ -134,18 +464,39 @@ class NetworkingManager(object):
: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: NotImplementedError - Implementation not loaded
:raises: InvalidParameterValue if validation fails.
:raises: NetworkError if the network operation fails.
:returns: Dictionary containing the reset port configuration.
"""
LOG.warning(
"reset_port called but driver framework not loaded: "
"switch=%s, port=%s",
switch_id,
port_name,
)
raise exception.NetworkError(
_("Network driver framework not yet loaded")
LOG.info(
"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.error(
"Failed to reset port %(port)s on switch "
"%(switch)s: %(error)s",
{"port": port_name, "switch": switch_id, "error": str(e)},
)
raise exception.NetworkError(
_("Failed to reset network port: %s") % str(e)
)
@METRICS.timer("NetworkingManager.update_lag")
@messaging.expected_exceptions(
exception.InvalidParameterValue,
@@ -153,6 +504,10 @@ class NetworkingManager(object):
exception.SwitchNotFound,
exception.Invalid,
)
@validate_vlan_configuration(
switch_id_arg_name="switch_ids",
operation_description="lag configuration",
)
def update_lag(
self,
context,
@@ -165,7 +520,7 @@ class NetworkingManager(object):
allowed_vlans=None,
default_vlan=None,
):
"""Update a link aggregation group (LAG) configuration (stub).
"""Update a link aggregation group (LAG) configuration.
:param context: request context.
:param switch_ids: List of switch identifiers.
@@ -176,7 +531,10 @@ class NetworkingManager(object):
: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: Invalid - LAG operations are not yet supported.
:raises: InvalidParameterValue if validation fails.
:raises: NetworkError if the network operation fails.
:raises: NotImplemented - LAG is not yet supported.
:returns: Dictionary containing the updated LAG configuration.
"""
raise exception.Invalid(
_("LAG operations are not yet supported")
@@ -195,7 +553,10 @@ class NetworkingManager(object):
:param context: request context.
:param switch_ids: List of switch identifiers.
:param lag_name: Name of the LAG to delete.
:raises: Invalid - LAG operations are not yet supported.
:raises: InvalidParameterValue if validation fails.
:raises: NetworkError if the network operation fails.
:raises: NotImplemented - LAG is not yet supported.
:returns: Dictionary containing the deletion status.
"""
raise exception.Invalid(
_("LAG operations are not yet supported")
@@ -204,13 +565,61 @@ class NetworkingManager(object):
@METRICS.timer("NetworkingManager.get_switches")
@messaging.expected_exceptions(exception.NetworkError)
def get_switches(self, context):
"""Get information about all switches (stub).
"""Get information about all switches from all drivers.
:param context: Request context
:returns: Empty dictionary (no drivers loaded)
:returns: Dictionary of switch_id -> switch_info dictionaries
:raises: NetworkError if driver operations fail
"""
LOG.warning("get_switches called but driver framework not loaded")
return {}
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:
LOG.error("Driver %(driver)s not found",
{"driver": driver_name})
continue
except Exception as e:
LOG.error("Error accessing driver %(driver)s: %(error)s",
{"driver": driver_name, "error": str(e)})
continue
try:
switch_ids = driver.get_switch_ids()
for switch_id in switch_ids:
try:
switch_info = driver.get_switch_info(switch_id)
if switch_info:
switches[switch_id] = switch_info
except Exception as e:
LOG.error(
"Failed to get info for switch "
"%(switch)s from driver %(driver)s: "
"%(error)s",
{
"switch": switch_id,
"driver": driver_name,
"error": str(e),
},
)
except Exception as e:
LOG.warning("Failed to get switch IDs from driver "
"%(driver)s: %(error)s",
{"driver": driver_name, "error": str(e)})
LOG.info("Successfully retrieved %(count)d switch config sections",
{"count": len(switches)})
return switches
def cleanup(self):
"""Clean up resources."""

File diff suppressed because it is too large Load Diff