Source code for ironic.networking.switch_drivers.driver_adapter

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

"""
Driver Configuration Adapter

This module provides functionality to translate user-friendly switch
configuration into driver-specific configuration formats. It allows users
to configure switches using a generic format while supporting multiple
switch driver implementations.
"""

import configparser
import glob
import os
import tempfile

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


[docs] class NetworkingDriverAdapter: """Adapter for translating switch config to driver-specific format.""" def __init__(self, driver_classes): """Initialize the driver adapter. :param driver_classes: Dictionary of driver name -> driver class. Classes must implement get_translator() classmethod. """ self.driver_translators = {} self._register_translators(driver_classes) def _register_translators(self, driver_classes): """Register available driver translators. :param driver_classes: Dictionary of driver name -> driver class. Works with both driver classes and instances since get_translator() is a classmethod. """ for name, driver_class in driver_classes.items(): self.register_translator(name, driver_class.get_translator()) LOG.debug( "Registered translators for drivers: %s", list(self.driver_translators.keys()), )
[docs] def register_translator(self, driver_type, translator_instance): """Register a custom translator for a driver type. :param driver_type: String identifier for the driver type :param translator_instance: Instance of a translator class """ self.driver_translators[driver_type] = translator_instance LOG.info( "Registered custom translator for driver type: %s", driver_type )
def _validate_switch_config(self, switch_name, config): """Validate switch configuration has required fields. :param switch_name: Name of the switch :param config: Dictionary of configuration options :raises: NetworkError if validation fails """ required_fields = [ 'driver_type', 'device_type', 'address', 'username', 'mac_address', ] missing_fields = [f for f in required_fields if f not in config] # Check for authentication: must have either password or key_file has_auth = 'password' in config or 'key_file' in config if missing_fields or not has_auth: error_parts = [] if missing_fields: error_parts.append( "missing required fields: %s" % ', '.join(missing_fields) ) if not has_auth: error_parts.append( "must specify either 'password' or 'key_file'" ) raise exception.NetworkError( _("Invalid configuration for switch '%(switch)s': %(errors)s") % {'switch': switch_name, 'errors': '; '.join(error_parts)} )
[docs] def preprocess_config(self, output_file): """Transform user config into driver-specific config files. Scans oslo.config for switch configurations and generates driver-specific config files that then get written to a driver-specific config file. :returns: Number of translations generated """ try: if not os.path.exists(CONF.ironic_networking.switch_config_file): raise exception.NetworkError( _("Switch configuration file %s does not exist") % CONF.ironic_networking.switch_config_file ) # Extract generic switch sections from config switch_sections = self._extract_switch_sections( CONF.ironic_networking.switch_config_file ) if not switch_sections: LOG.debug( "No user defined switch sections found in %s", CONF.ironic_networking.switch_config_file, ) return 0 # Generate driver-specific configs translations = {} for switch_name, config in switch_sections.items(): # Validate configuration before processing self._validate_switch_config(switch_name, config) driver_type = config["driver_type"] LOG.debug( "Translating switch %s with driver type %s", switch_name, driver_type, ) if driver_type in self.driver_translators: translator = self.driver_translators[driver_type] else: error_msg = (_("No driver translator registered for " "switch: %(switch_name)s, with driver " "type: %(driver_type)s") % {'switch_name': switch_name, 'driver_type': driver_type}) raise exception.ConfigInvalid(error_msg=error_msg) translation = translator.translate_config(switch_name, config) if translation: translations.update(translation) if translations: self._write_config_file(output_file, translations) CONF.reload_config_files() return len(translations) except Exception as e: LOG.exception("Failed to preprocess switch configuration: %s", e) raise exception.NetworkError( _("Configuration preprocessing failed: %s") % e )
def _config_files(self): """Generate which yields all config files in the required order""" for config_file in CONF.config_file: yield config_file for config_dir in CONF.config_dir: config_dir_glob = os.path.join(config_dir, "*.conf") for config_file in sorted(glob.glob(config_dir_glob)): yield config_file def _extract_switch_sections(self, config_file): """Extract switch configuration sections from oslo.config. Looks for sections with names like: - [switch:switch_name] :returns: Dictionary of section_name -> config_dict """ switch_sections = {} sections = {} parser = cfg.ConfigParser(config_file, sections) try: parser.parse() except Exception as e: LOG.warning("Failed to parse config file %s: %s", config_file, e) return {} for section_name, section_config in sections.items(): if section_name.startswith("switch:"): switch_name = section_name[7:] # Get all key/value pairs in this section switch_sections[switch_name] = { k: v[0] for k, v in section_config.items() } LOG.debug("Found %d switch sections", len(switch_sections)) return switch_sections def _write_config_file(self, output_file, switch_configs): """Generate driver-specific configuration file. :param output_file: Path to the output file :param switch_configs: Dictionary of switch_name -> config_dict """ # Create temp file in same directory as output file for atomic rename output_dir = os.path.dirname(output_file) temp_fd = None temp_path = None try: config = configparser.ConfigParser() # Add all sections and their key-value pairs for section_name, section_config in switch_configs.items(): config.add_section(section_name) for key, value in section_config.items(): config.set(section_name, key, str(value)) # Write to temporary file first temp_fd, temp_path = tempfile.mkstemp( dir=output_dir, prefix='.tmp_driver_config_', text=True ) with os.fdopen(temp_fd, 'w') as f: temp_fd = None # fdopen takes ownership f.write( "# Auto-generated config for driver-specific switch " "configurations\n" ) f.write( "# Generated from user defined switch configuration\n\n" ) config.write(f) # Atomically move temp file to final location os.replace(temp_path, output_file) temp_path = None # Successfully moved LOG.info("Generated driver config file: %s", output_file) except Exception: LOG.exception("Failed to generate config file") # Clean up temp file if it still exists if temp_fd is not None: try: os.close(temp_fd) except OSError as cleanup_error: LOG.debug("Failed to close temp file descriptor: %s", cleanup_error) if temp_path is not None: try: os.unlink(temp_path) except OSError as cleanup_error: LOG.debug("Failed to remove temp file %s: %s", temp_path, cleanup_error) raise
[docs] def reload_configuration(self, output_file): """Reload and regenerate switch configuration files. This method re-extracts switch configurations from the config files and regenerates the driver-specific configuration files. It should be called when the switch configuration file has been modified. :param output_file: Path to the output file for driver-specific configs :returns: Number of translations generated :raises: NetworkError if configuration reload fails """ LOG.info("Reloading switch configuration from config files") try: # Force oslo.config to reload configuration files CONF.reload_config_files() # Re-run the preprocessing steps count = self.preprocess_config(output_file) LOG.info( "Successfully reloaded switch configuration. " "Generated %d driver-specific config sections", count, ) return count except Exception as e: LOG.error("Failed to reload switch configuration: %s", e) raise exception.NetworkError( _("Configuration reload failed: %s") % e )