mirror of
https://opendev.org/openstack/ironic.git
synced 2026-01-11 19:57:20 +00:00
Add standalone networking service for ironic
Implements the foundational infrastructure for a new standalone networking service that can operate independently of the main ironic conductor. This commit establishes the service skeleton with: - RPC API layer with oslo.messaging integration for remote calls - Public API interface for conductor/API to interact with the service - RPC service implementation for handling network requests - Stub networking manager with method signatures (implementation added in subsequent commit) - Service entry point (ironic-networking command) for deployment - Configuration options for service behavior and networking backend - Infrastructure and packaging changes for the new service The manager includes stub implementations that raise NetworkError, with the full implementation of network operations, driver framework and switch drivers are added in subsequence commits. Related-Bug: 2113769 Assisted-by: Claude/sonnet-4.5 Change-Id: I351c7afe96cbcebd6b2e2bb5f0b4f17b5d804ceb Signed-off-by: Allain Legacy <alegacy@redhat.com>
This commit is contained in:
parent
f2a3ec5d5e
commit
b8f3318ca6
26 changed files with 2117 additions and 4 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -15,6 +15,7 @@ releasenotes/build
|
|||
|
||||
# sample config files
|
||||
etc/ironic/ironic.conf.sample
|
||||
etc/ironic/ironic.networking.conf.sample
|
||||
etc/ironic/policy.yaml.sample
|
||||
|
||||
# Packages/installer info
|
||||
|
|
@ -33,6 +34,7 @@ develop-eggs
|
|||
# Other
|
||||
*.DS_Store
|
||||
.idea
|
||||
.vscode
|
||||
.testrepository
|
||||
.stestr
|
||||
.tox
|
||||
|
|
|
|||
64
ironic/command/networking.py
Normal file
64
ironic/command/networking.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
The Ironic Networking Service
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_service import service
|
||||
|
||||
from ironic.command import utils as command_utils
|
||||
from ironic.common import service as ironic_service
|
||||
from ironic.networking import rpc_service
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
def issue_startup_warnings(conf):
|
||||
"""Issue any startup warnings for the networking service."""
|
||||
# Add any networking-specific startup warnings here
|
||||
LOG.info("Starting Ironic Networking Service")
|
||||
|
||||
|
||||
def main():
|
||||
# NOTE(alegacy): Safeguard to prevent 'ironic.networking.manager'
|
||||
# from being imported prior to the configuration options being loaded.
|
||||
assert 'ironic.networking.manager' not in sys.modules
|
||||
|
||||
# Parse config file and command line options, then start logging
|
||||
ironic_service.prepare_service('ironic_networking', sys.argv)
|
||||
ironic_service.ensure_rpc_transport(CONF)
|
||||
|
||||
mgr = rpc_service.NetworkingRPCService(CONF.host,
|
||||
'ironic.networking.manager',
|
||||
'NetworkingManager')
|
||||
|
||||
issue_startup_warnings(CONF)
|
||||
|
||||
launcher = service.launch(CONF, mgr, restart_method='mutate')
|
||||
|
||||
# Set override signals.
|
||||
command_utils.handle_signal()
|
||||
|
||||
# Start the processes!
|
||||
sys.exit(launcher.wait())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
|
@ -34,6 +34,7 @@ def main():
|
|||
# more information see: https://bugs.launchpad.net/ironic/+bug/1562258
|
||||
# and https://bugs.launchpad.net/ironic/+bug/1279774.
|
||||
assert 'ironic.conductor.manager' not in sys.modules
|
||||
assert 'ironic.networking.manager' not in sys.modules
|
||||
|
||||
# Parse config file and command line options, then start logging
|
||||
ironic_service.prepare_service('ironic', sys.argv)
|
||||
|
|
|
|||
|
|
@ -377,6 +377,11 @@ class NodeNotFound(NotFound):
|
|||
_msg_fmt = _("Node %(node)s could not be found.")
|
||||
|
||||
|
||||
class SwitchNotFound(NotFound):
|
||||
_msg_fmt = _("Switch %(switch_id)s could not be found or is not "
|
||||
"supported by any configured switch driver.")
|
||||
|
||||
|
||||
class DuplicateNodeOnLookup(NodeNotFound):
|
||||
pass # Same error message, the difference only matters internally
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ from ironic.common import exception
|
|||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
# Network Types
|
||||
IDLE_NETWORK = 'idle'
|
||||
RESCUING_NETWORK = 'rescuing'
|
||||
CLEANING_NETWORK = 'cleaning'
|
||||
SERVICING_NETWORK = 'servicing'
|
||||
INSPECTION_NETWORK = 'inspection'
|
||||
PROVISIONING_NETWORK = 'provisioning'
|
||||
TENANT_NETWORK = 'tenant'
|
||||
|
||||
|
||||
def get_node_vif_ids(task):
|
||||
"""Get all VIF ids for a node.
|
||||
|
|
|
|||
|
|
@ -946,6 +946,7 @@ RELEASE_MAPPING = {
|
|||
'master': {
|
||||
'api': '1.104',
|
||||
'rpc': '1.62',
|
||||
'networking_rpc': '1.0',
|
||||
'objects': {
|
||||
'Allocation': ['1.3', '1.2', '1.1'],
|
||||
'BIOSSetting': ['1.2', '1.1'],
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ EXTRA_EXMODS = []
|
|||
GLOBAL_MANAGER = None
|
||||
|
||||
MANAGER_TOPIC = 'ironic.conductor_manager'
|
||||
NETWORKING_TOPIC = 'ironic.networking_manager'
|
||||
|
||||
|
||||
def init(conf):
|
||||
|
|
|
|||
|
|
@ -55,19 +55,22 @@ class BaseRPCService(service.Service):
|
|||
else:
|
||||
self._started = True
|
||||
|
||||
def _rpc_transport(self):
|
||||
return CONF.rpc_transport
|
||||
|
||||
def _real_start(self):
|
||||
admin_context = context.get_admin_context()
|
||||
|
||||
serializer = objects_base.IronicObjectSerializer(is_server=True)
|
||||
# Perform preparatory actions before starting the RPC listener
|
||||
self.manager.prepare_host()
|
||||
if CONF.rpc_transport == 'json-rpc':
|
||||
if self._rpc_transport() == 'json-rpc':
|
||||
conf_group = getattr(self.manager, 'json_rpc_conf_group',
|
||||
'json_rpc')
|
||||
self.rpcserver = json_rpc.WSGIService(
|
||||
self.manager, serializer, context.RequestContext.from_dict,
|
||||
conf_group=conf_group)
|
||||
elif CONF.rpc_transport != 'none':
|
||||
elif self._rpc_transport() != 'none':
|
||||
target = messaging.Target(topic=self.topic, server=self.host)
|
||||
endpoints = [self.manager]
|
||||
self.rpcserver = rpc.get_server(target, endpoints, serializer)
|
||||
|
|
@ -80,4 +83,4 @@ class BaseRPCService(service.Service):
|
|||
LOG.info('Created RPC server with %(transport)s transport for service '
|
||||
'%(service)s on host %(host)s.',
|
||||
{'service': self.topic, 'host': self.host,
|
||||
'transport': CONF.rpc_transport})
|
||||
'transport': self._rpc_transport()})
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ from ironic.conf import inspector
|
|||
from ironic.conf import inventory
|
||||
from ironic.conf import ipmi
|
||||
from ironic.conf import irmc
|
||||
from ironic.conf import ironic_networking
|
||||
from ironic.conf import json_rpc
|
||||
from ironic.conf import mdns
|
||||
from ironic.conf import metrics
|
||||
|
|
@ -82,7 +83,11 @@ inspector.register_opts(CONF)
|
|||
inventory.register_opts(CONF)
|
||||
ipmi.register_opts(CONF)
|
||||
irmc.register_opts(CONF)
|
||||
ironic_networking.register_opts(CONF)
|
||||
# Register default json_rpc group used for conductor
|
||||
json_rpc.register_opts(CONF)
|
||||
# Register a separate json_rpc group for ironic networking specific settings
|
||||
json_rpc.register_opts(CONF, group='ironic_networking_json_rpc')
|
||||
mdns.register_opts(CONF)
|
||||
metrics.register_opts(CONF)
|
||||
molds.register_opts(CONF)
|
||||
|
|
|
|||
118
ironic/conf/ironic_networking.py
Normal file
118
ironic/conf/ironic_networking.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic.common.i18n import _
|
||||
|
||||
opts = [
|
||||
# Overrides the global rpc_transport setting so that the conductor
|
||||
# and networking service can use different transports if necessary.
|
||||
cfg.StrOpt('rpc_transport',
|
||||
default=None,
|
||||
choices=['json-rpc', 'oslo_messaging'],
|
||||
help=_('The transport mechanism used for RPC communication. '
|
||||
'This can be set to "json-rpc" for JSON-RPC, '
|
||||
'"oslo_messaging" for Oslo Messaging, or "none" '
|
||||
'for no transport.')),
|
||||
cfg.StrOpt('switch_config_file',
|
||||
default='',
|
||||
help=_('Path to the switch configuration file that defines '
|
||||
'switches to be acted upon. The config file should be '
|
||||
'in INI format. For syntax refer to the user guide.')),
|
||||
cfg.StrOpt('driver_config_dir',
|
||||
default='/var/lib/ironic/networking',
|
||||
help=_('The path to the driver configuration directory. This '
|
||||
'is used to dynamically write driver config files that '
|
||||
'are derived from entries in the file specified by the '
|
||||
'switch_config_file option. This directory should not '
|
||||
'be populated with files manually.')),
|
||||
cfg.ListOpt('enabled_switch_drivers',
|
||||
default=[],
|
||||
help=_('A list of switch drivers to load and make available '
|
||||
'for managing network switches. Switch drivers are '
|
||||
'loaded from external projects via entry points in '
|
||||
'the "ironic.networking.switch_drivers" namespace. '
|
||||
'Only drivers listed here will be loaded and made '
|
||||
'available for use. An empty list means no switch '
|
||||
'drivers will be loaded.')),
|
||||
cfg.ListOpt('allowed_vlans',
|
||||
default=None,
|
||||
help=_('A list of VLAN IDs that are allowed to be used for '
|
||||
'port configuration. If not specified (None), all '
|
||||
'VLAN IDs are allowed. If set to an empty list ([]), '
|
||||
'no VLANs are allowed. If set to a list of values, '
|
||||
'only the specified VLAN IDs are allowed. The list '
|
||||
'is a comma separated list of VLAN ID values or range '
|
||||
'of values. For example, 100,101,102-104,106 would '
|
||||
'allow VLANs 100, 101, 102, 103, 104, and 106, but '
|
||||
'not 105. This setting can be overridden on a '
|
||||
'per-switch basis in the switch configuration file.')),
|
||||
cfg.StrOpt('cleaning_network',
|
||||
default='',
|
||||
help=_('The network to use for cleaning nodes. This should be '
|
||||
'expressed as {access|trunk}/native_vlan=VLAN_ID. Can '
|
||||
'be overridden on a per-node basis using the '
|
||||
'driver_info attribute and specifying this as '
|
||||
'`cleaning_network`')),
|
||||
cfg.StrOpt('rescuing_network',
|
||||
default='',
|
||||
help=_('The network to use for rescuing nodes. This should be '
|
||||
'expressed as {access|trunk}/native_vlan=VLAN_ID. Can '
|
||||
'be overridden on a per-node basis using the '
|
||||
'driver_info attribute and specifying this as '
|
||||
'`rescuing_network`')),
|
||||
cfg.StrOpt('provisioning_network',
|
||||
default='',
|
||||
help=_('The network to use for provisioning nodes. This '
|
||||
'should be expressed as '
|
||||
'{access|trunk}/native_vlan=VLAN_ID. Can be overridden '
|
||||
'on a per-node basis using the driver_info attribute '
|
||||
'and specifying this as '
|
||||
'`provisioning_network`')),
|
||||
cfg.StrOpt('servicing_network',
|
||||
default='',
|
||||
help=_('The network to use for servicing nodes. This '
|
||||
'should be expressed as '
|
||||
'{access|trunk}/native_vlan=VLAN_ID. Can be overridden '
|
||||
'on a per-node basis using the driver_info attribute '
|
||||
'and specifying this as '
|
||||
'`servicing_network`')),
|
||||
cfg.StrOpt('inspection_network',
|
||||
default='',
|
||||
help=_('The network to use for inspecting nodes. This '
|
||||
'should be expressed as '
|
||||
'{access|trunk}/native_vlan=VLAN_ID. Can be overridden '
|
||||
'on a per-node basis using the driver_info attribute '
|
||||
'and specifying this as '
|
||||
'`inspection_network`')),
|
||||
cfg.StrOpt('idle_network',
|
||||
default='',
|
||||
help=_('The network to use for initial inspecting of nodes. '
|
||||
'If provided switch ports will be configured back to '
|
||||
'this network whenever any of the other networks are '
|
||||
'removed/unconfigured. '
|
||||
'This should be expressed as '
|
||||
'{access|trunk}/native_vlan=VLAN_ID. Can be overridden '
|
||||
'on a per-node basis using the driver_info attribute '
|
||||
'and specifying this as '
|
||||
'`idle_network`'))
|
||||
]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_opts(opts, group='ironic_networking')
|
||||
|
||||
|
||||
def list_opts():
|
||||
return opts
|
||||
|
|
@ -41,6 +41,9 @@ _opts = [
|
|||
('inventory', ironic.conf.inventory.opts),
|
||||
('ipmi', ironic.conf.ipmi.opts),
|
||||
('irmc', ironic.conf.irmc.opts),
|
||||
('ironic_networking', ironic.conf.ironic_networking.list_opts()),
|
||||
# Expose the ironic networking-specific JSON-RPC group for sample configs
|
||||
('ironic_networking_json_rpc', ironic.conf.json_rpc.list_opts()),
|
||||
('json_rpc', ironic.conf.json_rpc.list_opts()),
|
||||
('mdns', ironic.conf.mdns.opts),
|
||||
('metrics', ironic.conf.metrics.opts),
|
||||
|
|
|
|||
0
ironic/networking/__init__.py
Normal file
0
ironic/networking/__init__.py
Normal file
169
ironic/networking/api.py
Normal file
169
ironic/networking/api.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
#
|
||||
# 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 API for other parts of Ironic to use.
|
||||
"""
|
||||
|
||||
from ironic.networking import rpcapi
|
||||
|
||||
# Global networking API instance
|
||||
_NETWORKING_API = None
|
||||
|
||||
|
||||
def get_networking_api():
|
||||
"""Get the networking API instance.
|
||||
|
||||
:returns: NetworkingAPI instance
|
||||
"""
|
||||
global _NETWORKING_API
|
||||
if _NETWORKING_API is None:
|
||||
_NETWORKING_API = rpcapi.NetworkingAPI()
|
||||
return _NETWORKING_API
|
||||
|
||||
|
||||
def update_port(
|
||||
context,
|
||||
switch_id,
|
||||
port_name,
|
||||
description,
|
||||
mode,
|
||||
native_vlan,
|
||||
allowed_vlans=None,
|
||||
lag_name=None,
|
||||
default_vlan=None
|
||||
):
|
||||
"""Update a network switch port configuration.
|
||||
|
||||
This is a convenience function that other parts of Ironic can use
|
||||
to update network switch port configurations.
|
||||
|
||||
:param context: request context.
|
||||
:param switch_id: Identifier for the switch.
|
||||
:param port_name: Name of the port on the switch.
|
||||
:param description: Description to set for the port.
|
||||
:param mode: Port mode ('access', 'trunk', or 'hybrid').
|
||||
:param native_vlan: VLAN ID to be set on the port.
|
||||
:param allowed_vlans: List of allowed VLAN IDs to be added(optional).
|
||||
:param default_vlan: VLAN ID to removed from the port(optional).
|
||||
:param lag_name: LAG name if port is part of a link aggregation group.
|
||||
:raises: InvalidParameterValue if validation fails.
|
||||
:raises: NetworkError if the network operation fails.
|
||||
:returns: Dictionary containing the updated port configuration.
|
||||
"""
|
||||
api = get_networking_api()
|
||||
return api.update_port(
|
||||
context,
|
||||
switch_id,
|
||||
port_name,
|
||||
description,
|
||||
mode,
|
||||
native_vlan,
|
||||
allowed_vlans=allowed_vlans,
|
||||
lag_name=lag_name,
|
||||
default_vlan=default_vlan
|
||||
)
|
||||
|
||||
|
||||
def reset_port(
|
||||
context,
|
||||
switch_id,
|
||||
port_name,
|
||||
native_vlan=None,
|
||||
allowed_vlans=None,
|
||||
default_vlan=None,
|
||||
):
|
||||
"""Reset a network switch port to default configuration.
|
||||
|
||||
This is a convenience function that other parts of Ironic can use
|
||||
to reset network switch ports to their default configurations.
|
||||
|
||||
:param context: request context.
|
||||
:param switch_id: Identifier for the switch.
|
||||
:param port_name: Name of the port on the switch.
|
||||
:param native_vlan: VLAN ID to be removed from the port.
|
||||
:param allowed_vlans: List of 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.
|
||||
"""
|
||||
api = get_networking_api()
|
||||
return api.reset_port(
|
||||
context,
|
||||
switch_id,
|
||||
port_name,
|
||||
native_vlan,
|
||||
allowed_vlans=allowed_vlans,
|
||||
default_vlan=default_vlan
|
||||
)
|
||||
|
||||
|
||||
def update_lag(
|
||||
context,
|
||||
switch_ids,
|
||||
lag_name,
|
||||
description,
|
||||
mode,
|
||||
native_vlan,
|
||||
aggregation_mode,
|
||||
allowed_vlans=None,
|
||||
default_vlan=None
|
||||
):
|
||||
"""Update a link aggregation group (LAG) configuration.
|
||||
|
||||
This is a convenience function that other parts of Ironic can use
|
||||
to update LAG configurations.
|
||||
|
||||
:param context: request context.
|
||||
:param switch_ids: List of switch identifiers.
|
||||
:param lag_name: Name of the LAG.
|
||||
:param description: Description for the LAG.
|
||||
:param mode: LAG mode ('access' or 'trunk').
|
||||
:param native_vlan: VLAN ID to be set for the LAG.
|
||||
:param aggregation_mode: Aggregation mode (e.g., 'lacp', 'static').
|
||||
:param allowed_vlans: List of allowed VLAN IDs to be added (optional).
|
||||
:param default_vlan: VLAN ID to removed from the port(optional).
|
||||
:raises: InvalidParameterValue if validation fails.
|
||||
:raises: NetworkError if the network operation fails.
|
||||
:returns: Dictionary containing the updated LAG configuration.
|
||||
"""
|
||||
api = get_networking_api()
|
||||
return api.update_lag(
|
||||
context,
|
||||
switch_ids,
|
||||
lag_name,
|
||||
description,
|
||||
mode,
|
||||
native_vlan,
|
||||
aggregation_mode,
|
||||
allowed_vlans=allowed_vlans,
|
||||
default_vlan=default_vlan
|
||||
)
|
||||
|
||||
|
||||
def delete_lag(context, switch_ids, lag_name):
|
||||
"""Delete a link aggregation group (LAG) configuration.
|
||||
|
||||
This is a convenience function that other parts of Ironic can use
|
||||
to delete LAG configurations.
|
||||
|
||||
: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.
|
||||
:returns: Dictionary containing the deletion status.
|
||||
"""
|
||||
api = get_networking_api()
|
||||
return api.delete_lag(context, switch_ids, lag_name)
|
||||
217
ironic/networking/manager.py
Normal file
217
ironic/networking/manager.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
#
|
||||
# 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.
|
||||
"""
|
||||
|
||||
from oslo_log import log
|
||||
import oslo_messaging as messaging
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import metrics_utils
|
||||
from ironic.common import rpc
|
||||
from ironic.conf import CONF
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
|
||||
|
||||
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:
|
||||
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"
|
||||
|
||||
def prepare_host(self):
|
||||
"""Prepare host for networking service initialization.
|
||||
|
||||
This method is called by the RPC service before starting the listener.
|
||||
"""
|
||||
pass
|
||||
|
||||
def init_host(self, admin_context=None):
|
||||
"""Initialize the networking service host.
|
||||
|
||||
: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."
|
||||
)
|
||||
|
||||
@METRICS.timer("NetworkingManager.update_port")
|
||||
@messaging.expected_exceptions(
|
||||
exception.InvalidParameterValue,
|
||||
exception.NetworkError,
|
||||
exception.SwitchNotFound,
|
||||
)
|
||||
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 (stub).
|
||||
|
||||
: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: NotImplementedError - Implementation not loaded
|
||||
"""
|
||||
LOG.warning(
|
||||
"update_port called but driver framework not loaded: "
|
||||
"switch=%s, port=%s",
|
||||
switch_id,
|
||||
port_name,
|
||||
)
|
||||
raise exception.NetworkError(
|
||||
_("Network driver framework not yet loaded")
|
||||
)
|
||||
|
||||
@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 (stub).
|
||||
|
||||
: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: NotImplementedError - Implementation not loaded
|
||||
"""
|
||||
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")
|
||||
)
|
||||
|
||||
@METRICS.timer("NetworkingManager.update_lag")
|
||||
@messaging.expected_exceptions(
|
||||
exception.InvalidParameterValue,
|
||||
exception.NetworkError,
|
||||
exception.SwitchNotFound,
|
||||
exception.Invalid,
|
||||
)
|
||||
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 (stub).
|
||||
|
||||
: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: Invalid - LAG operations are not yet supported.
|
||||
"""
|
||||
raise exception.Invalid(
|
||||
_("LAG operations are not yet supported")
|
||||
)
|
||||
|
||||
@METRICS.timer("NetworkingManager.delete_lag")
|
||||
@messaging.expected_exceptions(
|
||||
exception.InvalidParameterValue,
|
||||
exception.NetworkError,
|
||||
exception.SwitchNotFound,
|
||||
exception.Invalid,
|
||||
)
|
||||
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: Invalid - LAG operations are not yet supported.
|
||||
"""
|
||||
raise exception.Invalid(
|
||||
_("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 (stub).
|
||||
|
||||
:param context: Request context
|
||||
:returns: Empty dictionary (no drivers loaded)
|
||||
"""
|
||||
LOG.warning("get_switches called but driver framework not loaded")
|
||||
return {}
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up resources."""
|
||||
pass
|
||||
78
ironic/networking/rpc_service.py
Normal file
78
ironic/networking/rpc_service.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from ironic.common import rpc_service
|
||||
from ironic.networking import utils as networking_utils
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class NetworkingRPCService(rpc_service.BaseRPCService):
|
||||
"""RPC service for the Ironic Networking Manager."""
|
||||
|
||||
def __init__(self, host, manager_module, manager_class):
|
||||
super().__init__(host, manager_module, manager_class)
|
||||
self.graceful_shutdown = False
|
||||
|
||||
def _rpc_transport(self):
|
||||
return networking_utils.rpc_transport()
|
||||
|
||||
def _real_start(self):
|
||||
"""Start the networking service."""
|
||||
super()._real_start()
|
||||
LOG.info(
|
||||
"Started networking RPC server for service %(service)s on "
|
||||
"host %(host)s.",
|
||||
{"service": self.topic, "host": self.host},
|
||||
)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the networking service."""
|
||||
LOG.info(
|
||||
"Stopping networking RPC server for service %(service)s on "
|
||||
"host %(host)s.",
|
||||
{"service": self.topic, "host": self.host},
|
||||
)
|
||||
|
||||
try:
|
||||
if hasattr(self.manager, "del_host"):
|
||||
self.manager.del_host()
|
||||
except Exception as e:
|
||||
LOG.exception(
|
||||
"Service error occurred when cleaning up "
|
||||
"the networking RPC manager. Error: %s",
|
||||
e,
|
||||
)
|
||||
|
||||
try:
|
||||
if self.rpcserver is not None:
|
||||
self.rpcserver.stop()
|
||||
self.rpcserver.wait()
|
||||
except Exception as e:
|
||||
LOG.exception(
|
||||
"Service error occurred when stopping the "
|
||||
"networking RPC server. Error: %s",
|
||||
e,
|
||||
)
|
||||
|
||||
super().stop(graceful=True)
|
||||
LOG.info(
|
||||
"Stopped networking RPC server for service %(service)s on "
|
||||
"host %(host)s.",
|
||||
{"service": self.topic, "host": self.host},
|
||||
)
|
||||
251
ironic/networking/rpcapi.py
Normal file
251
ironic/networking/rpcapi.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Client side of the networking RPC API.
|
||||
"""
|
||||
|
||||
from oslo_log import log
|
||||
import oslo_messaging as messaging
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common.json_rpc import client as json_rpc
|
||||
from ironic.common import release_mappings as versions
|
||||
from ironic.common import rpc
|
||||
from ironic.conf import CONF
|
||||
from ironic.networking import utils as networking_utils
|
||||
from ironic.objects import base as objects_base
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class NetworkingAPI(object):
|
||||
"""Client side of the networking RPC API.
|
||||
|
||||
API version history:
|
||||
|
||||
| 1.0 - Initial version.
|
||||
"""
|
||||
|
||||
# NOTE(alegacy): This must be in sync with manager.NetworkingManager's.
|
||||
RPC_API_VERSION = "1.0"
|
||||
|
||||
def __init__(self, topic=None):
|
||||
super(NetworkingAPI, self).__init__()
|
||||
self.topic = topic
|
||||
if self.topic is None:
|
||||
if networking_utils.rpc_transport() == "json-rpc":
|
||||
# Use host_ip and port from the JSON-RPC config for topic
|
||||
host_ip = CONF.ironic_networking_json_rpc.host_ip
|
||||
port = CONF.ironic_networking_json_rpc.port
|
||||
topic_host = f"{host_ip}:{port}"
|
||||
self.topic = f"ironic.{topic_host}"
|
||||
else:
|
||||
self.topic = rpc.NETWORKING_TOPIC
|
||||
|
||||
serializer = objects_base.IronicObjectSerializer()
|
||||
release_ver = versions.RELEASE_MAPPING.get(CONF.pin_release_version)
|
||||
version_cap = (
|
||||
release_ver.get("networking_rpc")
|
||||
if release_ver else self.RPC_API_VERSION
|
||||
)
|
||||
|
||||
if networking_utils.rpc_transport() == "json-rpc":
|
||||
# Use a dedicated configuration group for networking JSON-RPC
|
||||
self.client = json_rpc.Client(
|
||||
serializer=serializer,
|
||||
version_cap=version_cap,
|
||||
conf_group="ironic_networking_json_rpc",
|
||||
)
|
||||
# Keep the original topic for JSON-RPC (needed for host extraction)
|
||||
elif networking_utils.rpc_transport() != "none":
|
||||
target = messaging.Target(topic=self.topic, version="1.0")
|
||||
self.client = rpc.get_client(
|
||||
target, version_cap=version_cap, serializer=serializer
|
||||
)
|
||||
else:
|
||||
self.client = None
|
||||
|
||||
def _prepare_call(self, topic, version=None):
|
||||
"""Prepare an RPC call.
|
||||
|
||||
:param topic: RPC topic to send to.
|
||||
:param version: RPC API version to require.
|
||||
"""
|
||||
topic = topic or self.topic
|
||||
|
||||
# A safeguard for the case someone uses rpc_transport=None
|
||||
if self.client is None:
|
||||
raise exception.ServiceUnavailable(
|
||||
_("Cannot use 'none' RPC to connect to networking service")
|
||||
)
|
||||
|
||||
# Normal RPC path
|
||||
return self.client.prepare(topic=topic, version=version)
|
||||
|
||||
def get_topic(self):
|
||||
"""Get RPC topic name for the networking service."""
|
||||
return self.topic
|
||||
|
||||
def update_port(
|
||||
self,
|
||||
context,
|
||||
switch_id,
|
||||
port_name,
|
||||
description,
|
||||
mode,
|
||||
native_vlan,
|
||||
allowed_vlans=None,
|
||||
default_vlan=None,
|
||||
lag_name=None,
|
||||
topic=None,
|
||||
):
|
||||
"""Update a port configuration on a switch.
|
||||
|
||||
:param context: request context.
|
||||
:param switch_id: Identifier for the switch.
|
||||
:param port_name: Name of the port on the switch.
|
||||
:param description: Description to set for the port.
|
||||
:param mode: Port mode ('access', 'trunk', or 'hybrid').
|
||||
:param native_vlan: VLAN ID to be set on the port.
|
||||
:param allowed_vlans: List of allowed VLAN IDs to be added(optional).
|
||||
:param default_vlan: VLAN ID to removed from the port(optional).
|
||||
:param lag_name: LAG name if port is part of a link aggregation group.
|
||||
:param topic: RPC topic. Defaults to self.topic.
|
||||
:raises: InvalidParameterValue if validation fails.
|
||||
:raises: NetworkError if the network operation fails.
|
||||
:returns: Dictionary containing the updated port configuration.
|
||||
"""
|
||||
cctxt = self._prepare_call(topic=topic, version="1.0")
|
||||
return cctxt.call(
|
||||
context,
|
||||
"update_port",
|
||||
switch_id=switch_id,
|
||||
port_name=port_name,
|
||||
description=description,
|
||||
mode=mode,
|
||||
native_vlan=native_vlan,
|
||||
allowed_vlans=allowed_vlans,
|
||||
lag_name=lag_name,
|
||||
default_vlan=default_vlan,
|
||||
)
|
||||
|
||||
def reset_port(
|
||||
self,
|
||||
context,
|
||||
switch_id,
|
||||
port_name,
|
||||
native_vlan,
|
||||
allowed_vlans=None,
|
||||
default_vlan=None,
|
||||
topic=None,
|
||||
):
|
||||
"""Reset a network switch port to default configuration.
|
||||
|
||||
:param context: request context.
|
||||
:param switch_id: Identifier for the switch.
|
||||
:param port_name: Name of the port on the switch.
|
||||
:param native_vlan: VLAN ID to be removed from the port.
|
||||
:param allowed_vlans: List of allowed VLAN IDs to be removed(optional).
|
||||
:param default_vlan: VLAN ID to restore onto the port(optional).
|
||||
:param topic: RPC topic. Defaults to self.topic.
|
||||
:raises: InvalidParameterValue if validation fails.
|
||||
:raises: NetworkError if the network operation fails.
|
||||
:returns: Dictionary containing the reset port configuration.
|
||||
"""
|
||||
cctxt = self._prepare_call(topic=topic, version="1.0")
|
||||
return cctxt.call(
|
||||
context,
|
||||
"reset_port",
|
||||
switch_id=switch_id,
|
||||
port_name=port_name,
|
||||
native_vlan=native_vlan,
|
||||
allowed_vlans=allowed_vlans,
|
||||
default_vlan=default_vlan,
|
||||
)
|
||||
|
||||
def get_switches(self, context, topic=None):
|
||||
"""Get information about all configured switches.
|
||||
|
||||
:param context: request context.
|
||||
:param topic: RPC topic. Defaults to self.topic.
|
||||
:raises: NetworkError if the network operation fails.
|
||||
:returns: Dictionary with switch_id as key and switch_info as value.
|
||||
"""
|
||||
cctxt = self._prepare_call(topic=topic, version="1.0")
|
||||
return cctxt.call(context, "get_switches")
|
||||
|
||||
def update_lag(
|
||||
self,
|
||||
context,
|
||||
switch_ids,
|
||||
lag_name,
|
||||
description,
|
||||
mode,
|
||||
native_vlan,
|
||||
aggregation_mode,
|
||||
allowed_vlans=None,
|
||||
default_vlan=None,
|
||||
topic=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.
|
||||
:param description: Description for the LAG.
|
||||
:param mode: LAG mode ('access' or 'trunk').
|
||||
:param native_vlan: VLAN ID to be set for the LAG.
|
||||
:param aggregation_mode: Aggregation mode (e.g., 'lacp', 'static').
|
||||
:param allowed_vlans: List of allowed VLAN IDs to be added (optional).
|
||||
:param default_vlan: VLAN ID to removed from the port(optional).
|
||||
:param topic: RPC topic. Defaults to self.topic.
|
||||
:raises: InvalidParameterValue if validation fails.
|
||||
:raises: NetworkError if the network operation fails.
|
||||
:returns: Dictionary containing the updated LAG configuration.
|
||||
"""
|
||||
cctxt = self._prepare_call(topic=topic, version="1.0")
|
||||
return cctxt.call(
|
||||
context,
|
||||
"update_lag",
|
||||
switch_ids=switch_ids,
|
||||
lag_name=lag_name,
|
||||
description=description,
|
||||
mode=mode,
|
||||
native_vlan=native_vlan,
|
||||
aggregation_mode=aggregation_mode,
|
||||
allowed_vlans=allowed_vlans,
|
||||
default_vlan=default_vlan,
|
||||
)
|
||||
|
||||
def delete_lag(
|
||||
self, context, switch_ids, lag_name, topic=None
|
||||
):
|
||||
"""Delete 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 delete.
|
||||
:param topic: RPC topic. Defaults to self.topic.
|
||||
:raises: InvalidParameterValue if validation fails.
|
||||
:raises: NetworkError if the network operation fails.
|
||||
:returns: Dictionary containing the deletion status.
|
||||
"""
|
||||
cctxt = self._prepare_call(topic=topic, version="1.0")
|
||||
return cctxt.call(
|
||||
context,
|
||||
"delete_lag",
|
||||
switch_ids=switch_ids,
|
||||
lag_name=lag_name,
|
||||
)
|
||||
129
ironic/networking/utils.py
Normal file
129
ironic/networking/utils.py
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Utilities for networking service operations.
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def rpc_transport():
|
||||
"""Get the RPC transport type."""
|
||||
if CONF.ironic_networking.rpc_transport is None:
|
||||
return CONF.rpc_transport
|
||||
else:
|
||||
return CONF.ironic_networking.rpc_transport
|
||||
|
||||
|
||||
def parse_vlan_ranges(vlan_spec):
|
||||
"""Parse VLAN specification into a set of VLAN IDs.
|
||||
|
||||
:param vlan_spec: List of VLAN IDs or ranges (e.g., ['100', '102-104'])
|
||||
:returns: Set of integer VLAN IDs
|
||||
:raises: InvalidParameterValue if the specification is invalid
|
||||
"""
|
||||
if vlan_spec is None:
|
||||
return None
|
||||
|
||||
vlan_set = set()
|
||||
for item in vlan_spec:
|
||||
item = item.strip()
|
||||
if '-' in item:
|
||||
# Handle range (e.g., "102-104")
|
||||
try:
|
||||
start, end = item.split('-', 1)
|
||||
start_vlan = int(start.strip())
|
||||
end_vlan = int(end.strip())
|
||||
if start_vlan < 1 or end_vlan > 4094:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('VLAN IDs must be between 1 and 4094, got range '
|
||||
'%(start)s-%(end)s') % {'start': start_vlan,
|
||||
'end': end_vlan})
|
||||
if start_vlan > end_vlan:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Invalid VLAN range %(start)s-%(end)s: start must '
|
||||
'be less than or equal to end') %
|
||||
{'start': start_vlan, 'end': end_vlan})
|
||||
vlan_set.update(range(start_vlan, end_vlan + 1))
|
||||
except (ValueError, AttributeError) as e:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Invalid VLAN range format "%(item)s": %(error)s') %
|
||||
{'item': item, 'error': str(e)})
|
||||
else:
|
||||
# Handle single VLAN ID
|
||||
try:
|
||||
vlan_id = int(item)
|
||||
if vlan_id < 1 or vlan_id > 4094:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('VLAN ID must be between 1 and 4094, got %s') %
|
||||
vlan_id)
|
||||
vlan_set.add(vlan_id)
|
||||
except ValueError as e:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('Invalid VLAN ID "%(item)s": %(error)s') %
|
||||
{'item': item, 'error': str(e)})
|
||||
|
||||
return vlan_set
|
||||
|
||||
|
||||
def validate_vlan_allowed(vlan_id, allowed_vlans_config=None,
|
||||
switch_config=None):
|
||||
"""Validate that a VLAN ID is allowed.
|
||||
|
||||
:param vlan_id: The VLAN ID to validate
|
||||
:param allowed_vlans_config: Global list of allowed vlans from config
|
||||
:param switch_config: Optional switch-specific configuration dict that
|
||||
may contain an 'allowed_vlans' key
|
||||
:returns: True if the VLAN is allowed
|
||||
:raises: InvalidParameterValue if the VLAN is not allowed
|
||||
"""
|
||||
# Check switch-specific configuration first (if provided)
|
||||
if switch_config and 'allowed_vlans' in switch_config:
|
||||
allowed_spec = switch_config['allowed_vlans']
|
||||
else:
|
||||
# Fall back to global configuration
|
||||
if allowed_vlans_config is not None:
|
||||
allowed_spec = allowed_vlans_config
|
||||
else:
|
||||
allowed_spec = CONF.ironic_networking.allowed_vlans
|
||||
|
||||
# None means all VLANs are allowed
|
||||
if allowed_spec is None:
|
||||
return True
|
||||
|
||||
# Empty list means no VLANs are allowed
|
||||
if isinstance(allowed_spec, list) and len(allowed_spec) == 0:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('VLAN %(vlan)s is not allowed: no VLANs are permitted by '
|
||||
'configuration') % {'vlan': vlan_id})
|
||||
|
||||
# Parse and check against allowed VLANs
|
||||
try:
|
||||
allowed_vlans = parse_vlan_ranges(allowed_spec)
|
||||
if vlan_id not in allowed_vlans:
|
||||
raise exception.InvalidParameterValue(
|
||||
_('VLAN %(vlan)s is not in the list of allowed VLANs') %
|
||||
{'vlan': vlan_id})
|
||||
except exception.InvalidParameterValue:
|
||||
# Re-raise validation errors from parse_vlan_ranges
|
||||
raise
|
||||
|
||||
return True
|
||||
|
|
@ -58,12 +58,18 @@ class ReleaseMappingsTestCase(base.TestCase):
|
|||
def test_structure(self):
|
||||
for value in release_mappings.RELEASE_MAPPING.values():
|
||||
self.assertIsInstance(value, dict)
|
||||
self.assertEqual({'api', 'rpc', 'objects'}, set(value))
|
||||
# networking_rpc is optional
|
||||
expected_keys = {'api', 'rpc', 'objects'}
|
||||
optional_keys = {'networking_rpc'}
|
||||
self.assertTrue(set(value).issubset(expected_keys | optional_keys))
|
||||
self.assertTrue(expected_keys.issubset(set(value)))
|
||||
self.assertIsInstance(value['api'], str)
|
||||
(major, minor) = value['api'].split('.')
|
||||
self.assertEqual(1, int(major))
|
||||
self.assertLessEqual(int(minor), api_versions.MINOR_MAX_VERSION)
|
||||
self.assertIsInstance(value['rpc'], str)
|
||||
if 'networking_rpc' in value:
|
||||
self.assertIsInstance(value['networking_rpc'], str)
|
||||
self.assertIsInstance(value['objects'], dict)
|
||||
for obj_value in value['objects'].values():
|
||||
self.assertIsInstance(obj_value, list)
|
||||
|
|
@ -82,6 +88,12 @@ class ReleaseMappingsTestCase(base.TestCase):
|
|||
self.assertEqual(rpcapi.ConductorAPI.RPC_API_VERSION,
|
||||
release_mappings.RELEASE_MAPPING['master']['rpc'])
|
||||
|
||||
def test_current_networking_rpc_version(self):
|
||||
from ironic.networking import rpcapi as networking_rpcapi
|
||||
self.assertEqual(
|
||||
networking_rpcapi.NetworkingAPI.RPC_API_VERSION,
|
||||
release_mappings.RELEASE_MAPPING['master']['networking_rpc'])
|
||||
|
||||
def test_current_object_versions(self):
|
||||
registered_objects = obj_base.IronicObjectRegistry.obj_classes()
|
||||
obj_versions = release_mappings.get_object_versions(
|
||||
|
|
|
|||
0
ironic/tests/unit/networking/__init__.py
Normal file
0
ironic/tests/unit/networking/__init__.py
Normal file
138
ironic/tests/unit/networking/test_api.py
Normal file
138
ironic/tests/unit/networking/test_api.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
#
|
||||
# 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.
|
||||
"""Unit tests for ``ironic.networking.api``."""
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from ironic.networking import api
|
||||
|
||||
|
||||
class NetworkingApiTestCase(unittest.TestCase):
|
||||
"""Test cases for helper functions in ``ironic.networking.api``."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.addCleanup(setattr, api, "_NETWORKING_API", None)
|
||||
|
||||
def test_get_networking_api_singleton(self):
|
||||
with mock.patch(
|
||||
"ironic.networking.api.rpcapi.NetworkingAPI",
|
||||
autospec=True,
|
||||
) as mock_cls:
|
||||
instance = mock_cls.return_value
|
||||
|
||||
result1 = api.get_networking_api()
|
||||
result2 = api.get_networking_api()
|
||||
|
||||
self.assertIs(result1, instance)
|
||||
self.assertIs(result2, instance)
|
||||
mock_cls.assert_called_once_with()
|
||||
|
||||
def test_update_port_delegates_to_rpc(self):
|
||||
api_mock = mock.Mock()
|
||||
with mock.patch.object(
|
||||
api, "get_networking_api", return_value=api_mock,
|
||||
autospec=True
|
||||
):
|
||||
context = object()
|
||||
result = api.update_port(
|
||||
context,
|
||||
"switch0",
|
||||
"eth0",
|
||||
"description",
|
||||
"access",
|
||||
24,
|
||||
allowed_vlans=[10],
|
||||
lag_name="pc1",
|
||||
)
|
||||
|
||||
api_mock.update_port.assert_called_once_with(
|
||||
context,
|
||||
"switch0",
|
||||
"eth0",
|
||||
"description",
|
||||
"access",
|
||||
24,
|
||||
allowed_vlans=[10],
|
||||
lag_name="pc1",
|
||||
default_vlan=None,
|
||||
)
|
||||
self.assertIs(result, api_mock.update_port.return_value)
|
||||
|
||||
def test_reset_port_delegates_to_rpc(self):
|
||||
api_mock = mock.Mock()
|
||||
with mock.patch.object(
|
||||
api, "get_networking_api", return_value=api_mock,
|
||||
autospec=True
|
||||
):
|
||||
context = object()
|
||||
result = api.reset_port(
|
||||
context, "switch1", "eth1", default_vlan=11
|
||||
)
|
||||
|
||||
api_mock.reset_port.assert_called_once_with(
|
||||
context, "switch1", "eth1", None, allowed_vlans=None,
|
||||
default_vlan=11
|
||||
)
|
||||
self.assertIs(result, api_mock.reset_port.return_value)
|
||||
|
||||
def test_update_lag_delegates_to_rpc(self):
|
||||
api_mock = mock.Mock()
|
||||
with mock.patch.object(
|
||||
api, "get_networking_api", return_value=api_mock,
|
||||
autospec=True
|
||||
):
|
||||
context = object()
|
||||
result = api.update_lag(
|
||||
context,
|
||||
["switch1", "switch2"],
|
||||
"pc",
|
||||
"desc",
|
||||
"trunk",
|
||||
100,
|
||||
"lacp",
|
||||
allowed_vlans=[200],
|
||||
)
|
||||
|
||||
api_mock.update_lag.assert_called_once_with(
|
||||
context,
|
||||
["switch1", "switch2"],
|
||||
"pc",
|
||||
"desc",
|
||||
"trunk",
|
||||
100,
|
||||
"lacp",
|
||||
allowed_vlans=[200],
|
||||
default_vlan=None,
|
||||
)
|
||||
self.assertIs(
|
||||
result, api_mock.update_lag.return_value
|
||||
)
|
||||
|
||||
def test_delete_lag_delegates_to_rpc(self):
|
||||
api_mock = mock.Mock()
|
||||
with mock.patch.object(
|
||||
api, "get_networking_api", return_value=api_mock,
|
||||
autospec=True
|
||||
):
|
||||
context = object()
|
||||
result = api.delete_lag(
|
||||
context, ["switch1"], "pc"
|
||||
)
|
||||
|
||||
api_mock.delete_lag.assert_called_once_with(
|
||||
context, ["switch1"], "pc"
|
||||
)
|
||||
self.assertIs(
|
||||
result, api_mock.delete_lag.return_value
|
||||
)
|
||||
54
ironic/tests/unit/networking/test_rpc_service.py
Normal file
54
ironic/tests/unit/networking/test_rpc_service.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic.common.json_rpc import server as json_rpc_server
|
||||
from ironic.common import rpc_service
|
||||
from ironic.networking import manager as networking_manager
|
||||
from ironic.tests import base as tests_base
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class TestNetworkingRPCService(tests_base.TestCase):
|
||||
|
||||
@mock.patch.object(json_rpc_server, "WSGIService", autospec=True)
|
||||
@mock.patch.object(
|
||||
rpc_service.objects_base, "IronicObjectSerializer", autospec=True
|
||||
)
|
||||
@mock.patch.object(rpc_service.context, "get_admin_context", autospec=True)
|
||||
def test_json_rpc_uses_networking_group(self, mock_ctx, mock_ser, mock_ws):
|
||||
CONF.set_override("rpc_transport", "json-rpc")
|
||||
# Ensure ironic networking group is registered and distinguishable
|
||||
CONF.set_override("port", 9999, group="ironic_networking_json_rpc")
|
||||
CONF.set_override("port", 8089, group="json_rpc")
|
||||
|
||||
networking_manager.NetworkingManager(host="hostA")
|
||||
svc = rpc_service.BaseRPCService(
|
||||
"hostA", "ironic.networking.manager", "NetworkingManager"
|
||||
)
|
||||
|
||||
# Trigger start path to build server
|
||||
with mock.patch.object(svc.manager, "prepare_host", autospec=True):
|
||||
with mock.patch.object(svc.manager, "init_host", autospec=True):
|
||||
svc._real_start()
|
||||
|
||||
self.assertTrue(mock_ws.called)
|
||||
# Ensure conf_group was propagated to WSGIService
|
||||
_, kwargs = mock_ws.call_args
|
||||
self.assertEqual("ironic_networking_json_rpc",
|
||||
kwargs.get("conf_group"))
|
||||
535
ironic/tests/unit/networking/test_rpcapi.py
Normal file
535
ironic/tests/unit/networking/test_rpcapi.py
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
#
|
||||
# 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.
|
||||
|
||||
"""Unit tests for networking RPC API."""
|
||||
|
||||
from oslo_config import cfg
|
||||
import unittest.mock as mock
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common import rpc
|
||||
from ironic.networking import rpcapi
|
||||
from ironic.tests import base as test_base
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class TestNetworkingAPI(test_base.TestCase):
|
||||
"""Test cases for NetworkingAPI RPC client."""
|
||||
|
||||
def setUp(self):
|
||||
super(TestNetworkingAPI, self).setUp()
|
||||
self.context = mock.Mock()
|
||||
self.api = rpcapi.NetworkingAPI()
|
||||
|
||||
def test_init_default_topic(self):
|
||||
"""Test NetworkingAPI initialization with default topic."""
|
||||
with mock.patch.object(rpc, "NETWORKING_TOPIC", "test-topic"):
|
||||
api = rpcapi.NetworkingAPI()
|
||||
self.assertEqual("test-topic", api.topic)
|
||||
|
||||
def test_init_custom_topic(self):
|
||||
"""Test NetworkingAPI initialization with custom topic."""
|
||||
api = rpcapi.NetworkingAPI(topic="custom-topic")
|
||||
self.assertEqual("custom-topic", api.topic)
|
||||
|
||||
def test_get_topic(self):
|
||||
"""Test get_topic method."""
|
||||
with mock.patch.object(rpc, "NETWORKING_TOPIC", "test-topic"):
|
||||
api = rpcapi.NetworkingAPI()
|
||||
self.assertEqual("test-topic", api.get_topic())
|
||||
|
||||
@mock.patch.object(rpc, "NETWORKING_TOPIC", "test-topic")
|
||||
def test_init_with_none_transport(self):
|
||||
"""Test initialization with rpc_transport=none."""
|
||||
CONF.set_override("rpc_transport", "none")
|
||||
api = rpcapi.NetworkingAPI()
|
||||
self.assertIsNone(api.client)
|
||||
|
||||
@mock.patch.object(rpc, "NETWORKING_TOPIC", "test-topic")
|
||||
def test_init_with_json_rpc_transport(self):
|
||||
"""Test initialization with json-rpc transport."""
|
||||
CONF.set_override("rpc_transport", "json-rpc")
|
||||
api = rpcapi.NetworkingAPI()
|
||||
self.assertIsNotNone(api.client)
|
||||
# Ensure the client is configured to use networking group
|
||||
self.assertEqual("ironic_networking_json_rpc", api.client.conf_group)
|
||||
|
||||
@mock.patch.object(rpc, "NETWORKING_TOPIC", "test-topic")
|
||||
def test_init_with_oslo_messaging_transport(self):
|
||||
"""Test initialization with oslo.messaging transport."""
|
||||
CONF.set_override("rpc_transport", "oslo")
|
||||
api = rpcapi.NetworkingAPI()
|
||||
self.assertIsNotNone(api.client)
|
||||
|
||||
@mock.patch.object(rpc, "NETWORKING_TOPIC", "test-topic")
|
||||
def test_init_with_json_rpc_uses_networking_host(self):
|
||||
"""Test initialization with json-rpc uses networking host for topic."""
|
||||
CONF.set_override("rpc_transport", "json-rpc")
|
||||
CONF.set_override(
|
||||
"host_ip", "test-networking-host",
|
||||
group="ironic_networking_json_rpc"
|
||||
)
|
||||
CONF.set_override("port", 8089, group="ironic_networking_json_rpc")
|
||||
api = rpcapi.NetworkingAPI()
|
||||
self.assertEqual("ironic.test-networking-host:8089", api.topic)
|
||||
self.assertIsNotNone(api.client)
|
||||
|
||||
@mock.patch.object(rpc, "NETWORKING_TOPIC", "test-topic")
|
||||
def test_init_with_oslo_messaging_uses_default_topic(self):
|
||||
"""Test initialization with oslo.messaging uses default topic."""
|
||||
CONF.set_override("rpc_transport", "oslo")
|
||||
CONF.set_override("host", "test-networking-host")
|
||||
api = rpcapi.NetworkingAPI()
|
||||
self.assertEqual("test-topic", api.topic)
|
||||
self.assertIsNotNone(api.client)
|
||||
|
||||
@mock.patch.object(rpc, "NETWORKING_TOPIC", "test-topic")
|
||||
def test_init_with_custom_topic_overrides_networking_host(self):
|
||||
"""Test custom topic overrides networking host even for JSON-RPC."""
|
||||
CONF.set_override("rpc_transport", "json-rpc")
|
||||
CONF.set_override("host", "test-networking-host")
|
||||
api = rpcapi.NetworkingAPI(topic="custom-topic")
|
||||
self.assertEqual("custom-topic", api.topic)
|
||||
self.assertIsNotNone(api.client)
|
||||
|
||||
def test_prepare_call_none_client(self):
|
||||
"""Test _prepare_call with None client raises exception."""
|
||||
self.api.client = None
|
||||
exc = self.assertRaises(
|
||||
exception.ServiceUnavailable, self.api._prepare_call, topic="test"
|
||||
)
|
||||
self.assertIn("Cannot use 'none' RPC", str(exc))
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_update_port_success(self, mock_prepare):
|
||||
"""Test successful update_port call."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
mock_cctxt.call.return_value = {"status": "configured"}
|
||||
|
||||
result = self.api.update_port(
|
||||
self.context,
|
||||
"switch-01",
|
||||
"port-01",
|
||||
"Test port",
|
||||
"access",
|
||||
100,
|
||||
allowed_vlans=None,
|
||||
lag_name=None,
|
||||
default_vlan=1
|
||||
)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic=None, version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(
|
||||
self.context,
|
||||
"update_port",
|
||||
switch_id="switch-01",
|
||||
port_name="port-01",
|
||||
description="Test port",
|
||||
mode="access",
|
||||
native_vlan=100,
|
||||
allowed_vlans=None,
|
||||
lag_name=None,
|
||||
default_vlan=1
|
||||
)
|
||||
self.assertEqual({"status": "configured"}, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_update_port_with_allowed_vlans(self, mock_prepare):
|
||||
"""Test update_port call with allowed VLANs."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
mock_cctxt.call.return_value = {"status": "configured"}
|
||||
|
||||
result = self.api.update_port(
|
||||
self.context,
|
||||
"switch-01",
|
||||
"port-01",
|
||||
"Test port",
|
||||
"trunk",
|
||||
100,
|
||||
allowed_vlans=[101, 102],
|
||||
lag_name="po1",
|
||||
default_vlan=1
|
||||
)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic=None, version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(
|
||||
self.context,
|
||||
"update_port",
|
||||
switch_id="switch-01",
|
||||
port_name="port-01",
|
||||
description="Test port",
|
||||
mode="trunk",
|
||||
native_vlan=100,
|
||||
allowed_vlans=[101, 102],
|
||||
lag_name="po1",
|
||||
default_vlan=1
|
||||
)
|
||||
self.assertEqual({"status": "configured"}, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_update_port_with_custom_topic(self, mock_prepare):
|
||||
"""Test update_port call with custom topic."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
mock_cctxt.call.return_value = {"status": "configured"}
|
||||
|
||||
result = self.api.update_port(
|
||||
self.context,
|
||||
"switch-01",
|
||||
"port-01",
|
||||
"Test port",
|
||||
"access",
|
||||
100,
|
||||
default_vlan=1,
|
||||
topic="custom-topic",
|
||||
)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic="custom-topic", version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(
|
||||
self.context,
|
||||
"update_port",
|
||||
switch_id="switch-01",
|
||||
port_name="port-01",
|
||||
description="Test port",
|
||||
mode="access",
|
||||
native_vlan=100,
|
||||
allowed_vlans=None,
|
||||
lag_name=None,
|
||||
default_vlan=1,
|
||||
)
|
||||
self.assertEqual({"status": "configured"}, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_reset_port_success(self, mock_prepare):
|
||||
"""Test successful reset_port call."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
mock_cctxt.call.return_value = {"status": "reset"}
|
||||
|
||||
result = self.api.reset_port(
|
||||
self.context,
|
||||
"switch-01",
|
||||
"port-01",
|
||||
100,
|
||||
default_vlan=1)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic=None, version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(
|
||||
self.context,
|
||||
"reset_port",
|
||||
switch_id="switch-01",
|
||||
port_name="port-01",
|
||||
native_vlan=100,
|
||||
allowed_vlans=None,
|
||||
default_vlan=1,
|
||||
)
|
||||
self.assertEqual({"status": "reset"}, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_reset_port_with_allowed_vlans(self, mock_prepare):
|
||||
"""Test reset_port call with allowed VLANs."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
mock_cctxt.call.return_value = {"status": "reset"}
|
||||
|
||||
result = self.api.reset_port(
|
||||
self.context,
|
||||
"switch-01",
|
||||
"port-01",
|
||||
100,
|
||||
allowed_vlans=[101, 102],
|
||||
default_vlan=1
|
||||
)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic=None, version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(
|
||||
self.context,
|
||||
"reset_port",
|
||||
switch_id="switch-01",
|
||||
port_name="port-01",
|
||||
native_vlan=100,
|
||||
allowed_vlans=[101, 102],
|
||||
default_vlan=1,
|
||||
)
|
||||
self.assertEqual({"status": "reset"}, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_reset_port_with_custom_topic(self, mock_prepare):
|
||||
"""Test reset_port call with custom topic."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
mock_cctxt.call.return_value = {"status": "reset"}
|
||||
|
||||
result = self.api.reset_port(
|
||||
self.context,
|
||||
"switch-01",
|
||||
"port-01",
|
||||
100,
|
||||
default_vlan=1,
|
||||
topic="custom-topic"
|
||||
)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic="custom-topic", version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(
|
||||
self.context,
|
||||
"reset_port",
|
||||
switch_id="switch-01",
|
||||
port_name="port-01",
|
||||
native_vlan=100,
|
||||
allowed_vlans=None,
|
||||
default_vlan=1,
|
||||
)
|
||||
self.assertEqual({"status": "reset"}, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_get_switches_success(self, mock_prepare):
|
||||
"""Test successful get_switches call."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
expected_switches = {
|
||||
"switch-01": {"name": "switch-01", "status": "connected"},
|
||||
"switch-02": {"name": "switch-02", "status": "connected"},
|
||||
}
|
||||
mock_cctxt.call.return_value = expected_switches
|
||||
|
||||
result = self.api.get_switches(self.context)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic=None, version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(self.context, "get_switches")
|
||||
self.assertEqual(expected_switches, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_get_switches_with_custom_topic(self, mock_prepare):
|
||||
"""Test get_switches call with custom topic."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
expected_switches = {}
|
||||
mock_cctxt.call.return_value = expected_switches
|
||||
|
||||
result = self.api.get_switches(self.context, topic="custom-topic")
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic="custom-topic", version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(self.context, "get_switches")
|
||||
self.assertEqual(expected_switches, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_update_lag_success(self, mock_prepare):
|
||||
"""Test successful update_lag call."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
mock_cctxt.call.return_value = {"status": "configured"}
|
||||
|
||||
result = self.api.update_lag(
|
||||
self.context,
|
||||
["switch-01", "switch-02"],
|
||||
"lag-01",
|
||||
"Test LAG",
|
||||
"trunk",
|
||||
100,
|
||||
"lacp",
|
||||
allowed_vlans=[101, 102],
|
||||
default_vlan=1,
|
||||
)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic=None, version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(
|
||||
self.context,
|
||||
"update_lag",
|
||||
switch_ids=["switch-01", "switch-02"],
|
||||
lag_name="lag-01",
|
||||
description="Test LAG",
|
||||
mode="trunk",
|
||||
native_vlan=100,
|
||||
aggregation_mode="lacp",
|
||||
allowed_vlans=[101, 102],
|
||||
default_vlan=1,
|
||||
)
|
||||
self.assertEqual({"status": "configured"}, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_update_lag_without_allowed_vlans(self, mock_prepare):
|
||||
"""Test update_lag call without allowed VLANs."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
mock_cctxt.call.return_value = {"status": "configured"}
|
||||
|
||||
result = self.api.update_lag(
|
||||
self.context,
|
||||
["switch-01"],
|
||||
"lag-01",
|
||||
"Test LAG",
|
||||
"access",
|
||||
100,
|
||||
"static",
|
||||
default_vlan=1,
|
||||
)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic=None, version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(
|
||||
self.context,
|
||||
"update_lag",
|
||||
switch_ids=["switch-01"],
|
||||
lag_name="lag-01",
|
||||
description="Test LAG",
|
||||
mode="access",
|
||||
native_vlan=100,
|
||||
aggregation_mode="static",
|
||||
allowed_vlans=None,
|
||||
default_vlan=1,
|
||||
)
|
||||
self.assertEqual({"status": "configured"}, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_update_lag_with_custom_topic(self, mock_prepare):
|
||||
"""Test update_lag call with custom topic."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
mock_cctxt.call.return_value = {"status": "configured"}
|
||||
|
||||
result = self.api.update_lag(
|
||||
self.context,
|
||||
["switch-01"],
|
||||
"lag-01",
|
||||
"Test LAG",
|
||||
"access",
|
||||
100,
|
||||
"static",
|
||||
default_vlan=1,
|
||||
topic="custom-topic",
|
||||
)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic="custom-topic", version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(
|
||||
self.context,
|
||||
"update_lag",
|
||||
switch_ids=["switch-01"],
|
||||
lag_name="lag-01",
|
||||
description="Test LAG",
|
||||
mode="access",
|
||||
native_vlan=100,
|
||||
aggregation_mode="static",
|
||||
allowed_vlans=None,
|
||||
default_vlan=1,
|
||||
)
|
||||
self.assertEqual({"status": "configured"}, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_delete_lag_success(self, mock_prepare):
|
||||
"""Test successful delete_lag call."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
mock_cctxt.call.return_value = {"status": "deleted"}
|
||||
|
||||
result = self.api.delete_lag(
|
||||
self.context, ["switch-01", "switch-02"], "lag-01"
|
||||
)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic=None, version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(
|
||||
self.context,
|
||||
"delete_lag",
|
||||
switch_ids=["switch-01", "switch-02"],
|
||||
lag_name="lag-01",
|
||||
)
|
||||
self.assertEqual({"status": "deleted"}, result)
|
||||
|
||||
@mock.patch.object(rpcapi.NetworkingAPI, "_prepare_call", autospec=True)
|
||||
def test_delete_lag_with_custom_topic(self, mock_prepare):
|
||||
"""Test delete_lag call with custom topic."""
|
||||
mock_cctxt = mock.Mock()
|
||||
mock_prepare.return_value = mock_cctxt
|
||||
mock_cctxt.call.return_value = {"status": "deleted"}
|
||||
|
||||
result = self.api.delete_lag(
|
||||
self.context, ["switch-01"], "lag-01", topic="custom-topic"
|
||||
)
|
||||
|
||||
mock_prepare.assert_called_once_with(
|
||||
self.api, topic="custom-topic", version="1.0"
|
||||
)
|
||||
mock_cctxt.call.assert_called_once_with(
|
||||
self.context,
|
||||
"delete_lag",
|
||||
switch_ids=["switch-01"],
|
||||
lag_name="lag-01",
|
||||
)
|
||||
self.assertEqual({"status": "deleted"}, result)
|
||||
|
||||
|
||||
class TestNetworkingAPIVersionCap(test_base.TestCase):
|
||||
"""Test cases for NetworkingAPI version cap handling."""
|
||||
|
||||
def setUp(self):
|
||||
super(TestNetworkingAPIVersionCap, self).setUp()
|
||||
self.context = mock.Mock()
|
||||
|
||||
@mock.patch.object(rpc, "NETWORKING_TOPIC", "test-topic")
|
||||
def test_version_cap_from_release_mapping(self):
|
||||
"""Test version cap is set from release mapping."""
|
||||
with mock.patch.object(
|
||||
rpcapi.versions,
|
||||
"RELEASE_MAPPING",
|
||||
{"zed": {"rpc": "1.0"}}):
|
||||
CONF.set_override("pin_release_version", "zed")
|
||||
api = rpcapi.NetworkingAPI()
|
||||
# Version cap should be applied from release mapping
|
||||
self.assertIsNotNone(api.client)
|
||||
|
||||
@mock.patch.object(rpc, "NETWORKING_TOPIC", "test-topic")
|
||||
def test_version_cap_fallback_to_current(self):
|
||||
"""Test version cap falls back to current version."""
|
||||
with mock.patch.object(rpcapi.versions, "RELEASE_MAPPING", {}):
|
||||
CONF.set_override("pin_release_version", None)
|
||||
api = rpcapi.NetworkingAPI()
|
||||
# Should use current RPC_API_VERSION
|
||||
self.assertIsNotNone(api.client)
|
||||
|
||||
@mock.patch.object(rpc, "NETWORKING_TOPIC", "test-topic")
|
||||
def test_version_cap_no_pin_release_version(self):
|
||||
"""Test version cap when pin_release_version is not set."""
|
||||
with mock.patch.object(
|
||||
rpcapi.versions,
|
||||
"RELEASE_MAPPING",
|
||||
{"zed": {"rpc": "1.0"}}
|
||||
):
|
||||
CONF.set_override("pin_release_version", None)
|
||||
api = rpcapi.NetworkingAPI()
|
||||
# Should use current RPC_API_VERSION
|
||||
self.assertIsNotNone(api.client)
|
||||
309
ironic/tests/unit/networking/test_utils.py
Normal file
309
ironic/tests/unit/networking/test_utils.py
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
#
|
||||
# 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.
|
||||
"""Unit tests for ``ironic.networking.utils``."""
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.networking import utils
|
||||
from ironic.tests import base
|
||||
|
||||
|
||||
class ParseVlanRangesTestCase(base.TestCase):
|
||||
"""Test cases for parse_vlan_ranges function."""
|
||||
|
||||
def test_parse_vlan_ranges_none(self):
|
||||
"""Test that None returns None."""
|
||||
result = utils.parse_vlan_ranges(None)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_parse_vlan_ranges_empty_list(self):
|
||||
"""Test that empty list returns empty set."""
|
||||
result = utils.parse_vlan_ranges([])
|
||||
self.assertEqual(set(), result)
|
||||
|
||||
def test_parse_vlan_ranges_single_vlan(self):
|
||||
"""Test parsing a single VLAN ID."""
|
||||
result = utils.parse_vlan_ranges(['100'])
|
||||
self.assertEqual({100}, result)
|
||||
|
||||
def test_parse_vlan_ranges_multiple_vlans(self):
|
||||
"""Test parsing multiple VLAN IDs."""
|
||||
result = utils.parse_vlan_ranges(['100', '200', '300'])
|
||||
self.assertEqual({100, 200, 300}, result)
|
||||
|
||||
def test_parse_vlan_ranges_simple_range(self):
|
||||
"""Test parsing a simple VLAN range."""
|
||||
result = utils.parse_vlan_ranges(['100-103'])
|
||||
self.assertEqual({100, 101, 102, 103}, result)
|
||||
|
||||
def test_parse_vlan_ranges_complex_spec(self):
|
||||
"""Test parsing complex specification with ranges and singles."""
|
||||
result = utils.parse_vlan_ranges(
|
||||
['100', '101', '102-104', '106']
|
||||
)
|
||||
self.assertEqual({100, 101, 102, 103, 104, 106}, result)
|
||||
|
||||
def test_parse_vlan_ranges_with_spaces(self):
|
||||
"""Test parsing with spaces in the specification."""
|
||||
result = utils.parse_vlan_ranges(
|
||||
[' 100 ', ' 102 - 104 ', ' 106']
|
||||
)
|
||||
self.assertEqual({100, 102, 103, 104, 106}, result)
|
||||
|
||||
def test_parse_vlan_ranges_invalid_vlan_too_low(self):
|
||||
"""Test that VLAN ID 0 raises an error."""
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.parse_vlan_ranges,
|
||||
['0']
|
||||
)
|
||||
|
||||
def test_parse_vlan_ranges_invalid_vlan_too_high(self):
|
||||
"""Test that VLAN ID 4095 raises an error."""
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.parse_vlan_ranges,
|
||||
['4095']
|
||||
)
|
||||
|
||||
def test_parse_vlan_ranges_invalid_range_start_too_low(self):
|
||||
"""Test that range starting at 0 raises an error."""
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.parse_vlan_ranges,
|
||||
['0-10']
|
||||
)
|
||||
|
||||
def test_parse_vlan_ranges_invalid_range_end_too_high(self):
|
||||
"""Test that range ending at 4095 raises an error."""
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.parse_vlan_ranges,
|
||||
['4090-4095']
|
||||
)
|
||||
|
||||
def test_parse_vlan_ranges_invalid_range_start_greater_than_end(self):
|
||||
"""Test that reversed range raises an error."""
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.parse_vlan_ranges,
|
||||
['104-100']
|
||||
)
|
||||
|
||||
def test_parse_vlan_ranges_invalid_format_not_a_number(self):
|
||||
"""Test that non-numeric VLAN raises an error."""
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.parse_vlan_ranges,
|
||||
['abc']
|
||||
)
|
||||
|
||||
def test_parse_vlan_ranges_invalid_format_bad_range(self):
|
||||
"""Test that malformed range raises an error."""
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.parse_vlan_ranges,
|
||||
['100-200-300']
|
||||
)
|
||||
|
||||
def test_parse_vlan_ranges_boundary_values(self):
|
||||
"""Test parsing with boundary VLAN values (1 and 4094)."""
|
||||
result = utils.parse_vlan_ranges(['1', '4094'])
|
||||
self.assertEqual({1, 4094}, result)
|
||||
|
||||
|
||||
class ValidateVlanAllowedTestCase(base.TestCase):
|
||||
"""Test cases for validate_vlan_allowed function."""
|
||||
|
||||
def test_validate_vlan_allowed_none_config(self):
|
||||
"""Test that None config allows all VLANs."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = None
|
||||
result = utils.validate_vlan_allowed(100)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_validate_vlan_allowed_empty_list_config(self):
|
||||
"""Test that empty list config denies all VLANs."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = []
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.validate_vlan_allowed,
|
||||
100
|
||||
)
|
||||
|
||||
def test_validate_vlan_allowed_vlan_in_list(self):
|
||||
"""Test that VLAN in allowed list is accepted."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = ['100', '200', '300']
|
||||
result = utils.validate_vlan_allowed(100)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_validate_vlan_allowed_vlan_not_in_list(self):
|
||||
"""Test that VLAN not in allowed list is rejected."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = ['100', '200', '300']
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.validate_vlan_allowed,
|
||||
150
|
||||
)
|
||||
|
||||
def test_validate_vlan_allowed_vlan_in_range(self):
|
||||
"""Test that VLAN in allowed range is accepted."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = ['100-200']
|
||||
result = utils.validate_vlan_allowed(150)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_validate_vlan_allowed_vlan_not_in_range(self):
|
||||
"""Test that VLAN not in allowed range is rejected."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = ['100-200']
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.validate_vlan_allowed,
|
||||
250
|
||||
)
|
||||
|
||||
def test_validate_vlan_allowed_complex_spec(self):
|
||||
"""Test validation with complex allowed VLAN specification."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = [
|
||||
'100', '101', '102-104', '106'
|
||||
]
|
||||
# Test allowed VLANs
|
||||
self.assertTrue(utils.validate_vlan_allowed(100))
|
||||
self.assertTrue(utils.validate_vlan_allowed(101))
|
||||
self.assertTrue(utils.validate_vlan_allowed(102))
|
||||
self.assertTrue(utils.validate_vlan_allowed(103))
|
||||
self.assertTrue(utils.validate_vlan_allowed(104))
|
||||
self.assertTrue(utils.validate_vlan_allowed(106))
|
||||
# Test disallowed VLAN
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.validate_vlan_allowed,
|
||||
105
|
||||
)
|
||||
|
||||
def test_validate_vlan_allowed_override_config(self):
|
||||
"""Test that allowed_vlans_config parameter overrides CONF."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = ['100']
|
||||
# Override should allow 200, not 100
|
||||
result = utils.validate_vlan_allowed(
|
||||
200,
|
||||
allowed_vlans_config=['200']
|
||||
)
|
||||
self.assertTrue(result)
|
||||
# Should reject 100 when using override
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.validate_vlan_allowed,
|
||||
100,
|
||||
allowed_vlans_config=['200']
|
||||
)
|
||||
|
||||
def test_validate_vlan_allowed_switch_config_override(self):
|
||||
"""Test that switch config overrides global config."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = ['100']
|
||||
switch_config = {'allowed_vlans': ['200']}
|
||||
# Switch config should allow 200, not 100
|
||||
result = utils.validate_vlan_allowed(
|
||||
200,
|
||||
switch_config=switch_config
|
||||
)
|
||||
self.assertTrue(result)
|
||||
# Should reject 100 when using switch config
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.validate_vlan_allowed,
|
||||
100,
|
||||
switch_config=switch_config
|
||||
)
|
||||
|
||||
def test_validate_vlan_allowed_switch_config_no_allowed_vlans(self):
|
||||
"""Test that switch config without allowed_vlans uses global."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = ['100']
|
||||
switch_config = {'some_other_key': 'value'}
|
||||
# Should fall back to global config
|
||||
result = utils.validate_vlan_allowed(
|
||||
100,
|
||||
switch_config=switch_config
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_validate_vlan_allowed_switch_config_empty_list(self):
|
||||
"""Test that switch config with empty list denies all VLANs."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = ['100']
|
||||
switch_config = {'allowed_vlans': []}
|
||||
# Switch config empty list should deny even though global allows
|
||||
self.assertRaises(
|
||||
exception.InvalidParameterValue,
|
||||
utils.validate_vlan_allowed,
|
||||
100,
|
||||
switch_config=switch_config
|
||||
)
|
||||
|
||||
def test_validate_vlan_allowed_switch_config_none(self):
|
||||
"""Test that switch config with None allows all VLANs."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking']) as mock_conf:
|
||||
mock_conf.ironic_networking.allowed_vlans = ['100']
|
||||
switch_config = {'allowed_vlans': None}
|
||||
# Switch config None should allow all, even though global restricts
|
||||
result = utils.validate_vlan_allowed(
|
||||
200,
|
||||
switch_config=switch_config
|
||||
)
|
||||
self.assertTrue(result)
|
||||
|
||||
|
||||
class RpcTransportTestCase(unittest.TestCase):
|
||||
"""Test cases for rpc_transport function."""
|
||||
|
||||
def test_rpc_transport_uses_networking_when_set(self):
|
||||
"""Test that networking.rpc_transport is used when set."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking', 'rpc_transport']
|
||||
) as mock_conf:
|
||||
mock_conf.ironic_networking.rpc_transport = 'json-rpc'
|
||||
mock_conf.rpc_transport = 'oslo_messaging'
|
||||
result = utils.rpc_transport()
|
||||
self.assertEqual('json-rpc', result)
|
||||
|
||||
def test_rpc_transport_falls_back_to_global(self):
|
||||
"""Test that global rpc_transport is used when networking is None."""
|
||||
with mock.patch('ironic.networking.utils.CONF',
|
||||
spec_set=['ironic_networking', 'rpc_transport']
|
||||
) as mock_conf:
|
||||
mock_conf.ironic_networking.rpc_transport = None
|
||||
mock_conf.rpc_transport = 'oslo_messaging'
|
||||
result = utils.rpc_transport()
|
||||
self.assertEqual('oslo_messaging', result)
|
||||
|
|
@ -203,6 +203,7 @@ ironic = "ironic.command.singleprocess:main"
|
|||
ironic-api = "ironic.command.api:main"
|
||||
ironic-dbsync = "ironic.command.dbsync:main"
|
||||
ironic-conductor = "ironic.command.conductor:main"
|
||||
ironic-networking = "ironic.command.networking:main"
|
||||
ironic-novncproxy = "ironic.command.novncproxy:main"
|
||||
ironic-status = "ironic.command.status:main"
|
||||
ironic-pxe-filter = "ironic.command.pxe_filter:main"
|
||||
|
|
|
|||
7
tools/config/ironic-networking-config-generator.conf
Normal file
7
tools/config/ironic-networking-config-generator.conf
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[DEFAULT]
|
||||
output_file = etc/ironic/ironic.networking.conf.sample
|
||||
wrap_width = 62
|
||||
namespace = ironic
|
||||
namespace = oslo.log
|
||||
namespace = oslo.service.service
|
||||
namespace = oslo.concurrency
|
||||
1
tox.ini
1
tox.ini
|
|
@ -67,6 +67,7 @@ commands =
|
|||
sitepackages = False
|
||||
commands =
|
||||
oslo-config-generator --config-file=tools/config/ironic-config-generator.conf
|
||||
oslo-config-generator --config-file=tools/config/ironic-networking-config-generator.conf
|
||||
|
||||
[testenv:genpolicy]
|
||||
sitepackages = False
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue