Merge "Implement networking service RPC API methods"
This commit is contained in:
@@ -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."""
|
||||
|
||||
1203
ironic/tests/unit/networking/test_manager.py
Normal file
1203
ironic/tests/unit/networking/test_manager.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user