341 views
# DevNet Lab 9 -- Configure Open vSwitch using the Python ovsdbapp library [toc] --- > Copyright (c) 2025 Philippe Latu. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License". https://inetdoc.net ### Scenario The purpose of this lab is to provide an initial illustration of atomic switch fabric programmability. In the context of the private cloud infrastructure we are using, hypervisors are deployed using an Open vSwitch distribution switch called dsw-host. In addition, a large number of tap interfaces are also provisioned and declared as switch ports. When students start their first labs, they are preassigned sets of these tap interfaces to run virtual machines or routers. The activities in this lab will show how to connect to the Open vSwitch database server service named ovsdb-server and configure existing switch ports from a Python script code. This is a very first step into the world of network programmability. ![communication diagram between app and ovsdb-server](https://md.inetdoc.net/uploads/f14643b1-0bea-444f-9868-1ebfe5667c31.png) ### Objectives The primary objectives of this lab are to equip students with hands-on experience in network programmability using Open vSwitch and Python. By the end of this lab, students will be able to: - Connect to the Open vSwitch Database Server : Establish a secure connection to the OVSDB server using Python scripts, enabling interaction with the Open vSwitch configuration database. - Retrieve and Manage Switch Port Configurations : Use Python to list existing switches, retrieve detailed attributes of specific ports, and determine their VLAN configurations (access or trunk mode). - Apply Declarative Configuration : Load and apply network configurations from a YAML file to switch ports, demonstrating a DevOps approach to infrastructure management. - Verify Applied Configurations : Manually verify the applied configurations on the hypervisor to ensure consistency between the declared state and the actual network setup. ## Part 1: Setup the lab environment In this part we analyse the conditions for setting up a development communication channel between our Python code and the ovsdb-server service running on the hypervisor. ### Step 1: Identify the access conditions for the ovsdb-server service We first have to connect to the hypervisor in order to identify the ovsdb-server process attributes. ```bash ps aux | grep ovsdb-server ``` ```bash= root 2050 0.0 0.0 38856 23248 ? S<s mars23 0:58 ovsdb-server /etc/openvswitch/conf.db -vconsole:emer -vsyslog:err -vfile:info --remote=punix:/var/run/openvswitch/db.sock --private-key=db:Open_vSwitch,SSL,private_key --certificate=db:Open_vSwitch,SSL,certificate --bootstrap-ca-cert=db:Open_vSwitch,SSL,ca_cert --no-chdir --log-file=/var/log/openvswitch/ovsdb-server.log --pidfile=/var/run/openvswitch/ovsdb-server.pid --detach ``` From this somewhat long line, we can identify the connection socket to the database service. ```bash --remote=punix:/var/run/openvswitch/db.sock ``` Then we can determine who has access to that socket by looking at the permissions in the socket file. ```bash ls -lAh /var/run/openvswitch/db.sock ``` ```bash= srwxrwx--- 1 root kvm 0 23 mars 07:49 /var/run/openvswitch/db.sock ``` The main point here is that members of the kvm system group have full access to the ovsdb-server socket. It is this access granted by group membership that allows us to program switching functions. ### Step 2: Access the ovsdb-server from the DevNet development system The question now is how to access this socket from the DevNet virtual machine, which is our development system. OpenSSH's `LocalForward` feature allows us to securely forward Unix domain sockets from the hypervisor to the DevNet VM, providing secure access to the ovsdb-server service running on the hypervisor by creating a local socket that proxies requests to the remote socket. Here is a snippet of the SSH client configuration file `~/.ssh/config` that shows how to configure the `LocalForward` feature in our context. ``` Host hypervisor_name HostName fe80::VVVV:1%%enp0s1 User etudianttest Port 2222 StreamLocalBindUnlink yes LocalForward /tmp/ovs-forwarded.sock /var/run/openvswitch/db.sock ``` - `/tmp/ovs-forwarded.sock` : local Unix domain socket on the development system. - `/var/run/openvswitch/db.sock` : remote Unix domain socket on the hypervisor running the ovsdb-server service. This lab setup can be considered a good security practice, as it limits the exposure of the service to only the necessary development or configuration times. 1. Here is an example `ssh` command that forks a new process with `-f` and doesn't execute a remote command with `-N`. ```bash ssh -fN hypervisor_name ``` 2. Here is the instruction to close the SSH tunnel by killing the process started by the above command. ```bash pkill -fu $USER "ssh -fN" ``` :::info The main limitation is that we need to keep the SSH connection open between the DevNet system and the hypervisor while we are programming. This is not too annoying, as we may have to evaluate the results of the Python scripts on the hypervisor. ::: ## Part 2: Connect to the Open vSwitch database and retrieve switch port configuration In this part, we start by setting up the Python virtual environment that contains the ovsdbapp module code. Then we code two first simple scripts that prove we are able to interact with the ovsd-server service on the hypervisor. ### Step 1: Configure ovsdbapp on the DevNet VM We start by creating the working directory and creating a Python virtual environment. 1. Make the `$HOME/labs/lab09` directory for example and navigate to this folder ```bash mkdir -p $HOME/labs/lab09 && cd $HOME/labs/lab09 ``` 2. Install **ovsdbapp** Python virtual environement We choose to install `ovsdbapp` in a Python virtual environment to take advantage of the latest release. We start by creating a `requirements.txt` file. ```bash cat << EOF > requirements.txt ovsdbapp tabulate PyYAML EOF ``` Then we install the tools in a virtual environment called `ovsdb`. ```bash python3 -m venv ovsdb source ./ovsdb/bin/activate pip3 install -r requirements.txt ``` ### Step 2: Start a first connection to the OvS database Here is Python script code named `01_ovsdb_connect.py` that provides an object-oriented interface to communicate with the Open vSwitch Database (OVSDB) via a Unix socket forwarded through SSH. In this script, the `OVSDBManager` class encapsulates connection handling and database operations, allowing Open vSwitch network configurations to be managed programmatically. This first example allows existing virtual switches to be listed. ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys from ovsdbapp.backend.ovs_idl import connection, idlutils from ovsdbapp.schema.open_vswitch import impl_idl as ovs_impl_idl class OVSDBManager: """Class for managing OVSDB connections and operations""" # Class constants OVS_CONNECTION = "unix:/tmp/ovs-forwarded.sock" OVSDB_CONNECTION_TIMEOUT = 30 def __init__(self, auto_connect=True): """Initialize the OVSDB manager and establish connection if auto_connect is True""" self.conn = None self.ovs = None if auto_connect: try: self.connect() except Exception as e: print(f"Warning: Failed to connect automatically: {e}") print( "You'll need to call connect() manually before using other methods." ) def connect(self): """Establish a connection to Open vSwitch via Unix socket""" helper = idlutils.get_schema_helper(self.OVS_CONNECTION, "Open_vSwitch") helper.register_all() idl = connection.OvsdbIdl(self.OVS_CONNECTION, helper) self.conn = connection.Connection( idl=idl, timeout=self.OVSDB_CONNECTION_TIMEOUT ) self.ovs = ovs_impl_idl.OvsdbIdl(self.conn) return self.ovs def print_switch_list(self): """ Lists existing switches in Open vSwitch database CLI equivalent: ovs-vsctl list-br """ try: # Get all existing bridges sw_list = self.ovs.list_br().execute() print(f"Switches found ({len(sw_list)}):") for sw in sw_list: print(f" - {sw}") return sw_list except Exception as e: print(f"Error retrieving bridges: {e}") return [] def main(): print("Attempting to connect to remote Open vSwitch server via Unix socket...") try: # Create manager with automatic connection manager = OVSDBManager() # List existing bridges switch_list = manager.print_switch_list() # Display success message if switch_list: print("Connection and switch list retrieval successful!") sys.exit(0) # Exit with success code else: print("Connection successful, but no switches found.") sys.exit(1) # Exit with error code except Exception as e: print(f"Connection error: {e}") sys.exit(1) # Exit with error code if __name__ == "__main__": main() ``` Open the SSH connection that will establish the Unix domain socket proxy in a terminal session. ```bash ssh hypervisor_name ``` Next, run the Pyton script to test the first ovsdb-server query, which lists existing switches. ```bash python3 01_ovsdb_connect.py ``` ```bash= Attempting to connect to remote Open vSwitch server via Unix socket... Switches found (1): - dsw-host Connection and switch list retrieval successful! ``` :::success We now have a verified communication channel between the DevNet system and the Open vSwitch hypervisor database. ::: ### Step 3: Get the attributes of a switch port This new Python script is an extension of the previous step code. As in the first version, it is designed to connect to an Open vSwitch Database (OVSDB) where the switch named "dsw-host" is already configured with multiple tap interfaces defined as its ports. This new version of the script prompts the user for a tap interface name. The code then determines which bridge that particular port belongs to and retrieves detailed attributes of that particular port. Here is a copy of the `02_ovsdb_list_port.py` script code: ```python= #!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys from ovsdbapp.backend.ovs_idl import connection, idlutils from ovsdbapp.schema.open_vswitch import impl_idl as ovs_impl_idl from tabulate import tabulate # Import tabulate module for formatted output # Define constant SWITCH_NAME = "dsw-host" class OVSDBManager: """Class for managing OVSDB connections and operations""" # Class constants OVS_CONNECTION = "unix:/tmp/ovs-forwarded.sock" OVSDB_CONNECTION_TIMEOUT = 30 def __init__(self, auto_connect=True): """Initialize the OVSDB manager and establish connection if auto_connect is True""" self.conn = None self.ovs = None if auto_connect: try: self.connect() except Exception as e: print(f"Warning: Failed to connect automatically: {e}") print( "You'll need to call connect() manually before using other methods." ) def connect(self): """Establish a connection to Open vSwitch via Unix socket""" helper = idlutils.get_schema_helper(self.OVS_CONNECTION, "Open_vSwitch") helper.register_all() idl = connection.OvsdbIdl(self.OVS_CONNECTION, helper) self.conn = connection.Connection( idl=idl, timeout=self.OVSDB_CONNECTION_TIMEOUT ) self.ovs = ovs_impl_idl.OvsdbIdl(self.conn) return self.ovs def print_switch_list(self): """ Lists existing switches in Open vSwitch database CLI equivalent: ovs-vsctl list-br """ try: # Get all existing bridges sw_list = self.ovs.list_br().execute() print(f"Switches found ({len(sw_list)}):") for sw in sw_list: print(f" - {sw}") return sw_list except Exception as e: print(f"Error retrieving bridges: {e}") return [] def list_port_attributes(self, port_name, bridge_name=SWITCH_NAME): """ Lists all attributes for a specific port on a given bridge CLI equivalent: ovs-vsctl list port <port_name> """ try: # Check if the bridge exists bridges = self.ovs.list_br().execute() if bridge_name not in bridges: print(f"Error: Bridge '{bridge_name}' does not exist.") return None # Check if the port exists on the bridge ports = self.ovs.list_ports(bridge_name).execute() if port_name not in ports: print( f"Error: Port '{port_name}' does not exist on bridge '{bridge_name}'." ) return None # Get port details using direct OVSDB command cmd = self.ovs.db_find("Port", ("name", "=", port_name)) port_records = cmd.execute() if not port_records: print(f"No details found for port '{port_name}'.") return None print(f"\nAttributes for port '{port_name}':") # Prepare data for tabulate for record in port_records: # Convert record to a list of [key, value] pairs and sort alphabetically by key table_data = sorted( [[k, str(v)] for k, v in record.items()], key=lambda x: x[0] ) # Display using tabulate with fancy_grid format print( tabulate( table_data, headers=["Attribute", "Value"], tablefmt="fancy", ) ) return port_records except Exception as e: print(f"Error retrieving port attributes: {e}") return None def get_bridge_for_port(self, port_name): """ Determines which bridge a port belongs to CLI equivalent: ovs-vsctl port-to-br <port_name> """ try: # Get all bridges bridges = self.ovs.list_br().execute() # Check each bridge for the port for bridge in bridges: ports = self.ovs.list_ports(bridge).execute() if port_name in ports: return bridge # Port not found on any bridge return None except Exception as e: print(f"Error determining bridge for port: {e}") return None def main(): print("Attempting to connect to remote Open vSwitch server via Unix socket...") try: # Create manager with automatic connection manager = OVSDBManager() # Ask user for port name to examine port_name = input("\nEnter the port name to examine (e.g. tap123): ") # Check which bridge this port belongs to bridge = manager.get_bridge_for_port(port_name) if bridge is None: print(f"Error: Port '{port_name}' does not exist on any switch.") sys.exit(1) # Exit with error code elif bridge != SWITCH_NAME: print( f"Error: Port '{port_name}' belongs to '{bridge}', not to '{SWITCH_NAME}'." ) sys.exit(1) # Exit with error code else: print(f"Confirmed: Port '{port_name}' belongs to '{SWITCH_NAME}'.") # Get detailed port attributes port_records = manager.list_port_attributes(port_name) if port_records: sys.exit(0) # Exit with success code else: print("Failed to retrieve port attributes.") sys.exit(1) # Exit with error code except Exception as e: print(f"Connection error: {e}") sys.exit(1) # Exit with error code if __name__ == "__main__": main() ``` Here is a concise documentation of the methods and the logic of the main function in the script: __init__(self, auto_connect=True) : Initializes the OVSDB manager. As auto_connect is True, it attempts to establish a connection to the OVSDB server automatically. connect(self) : Establishes a connection to the Open vSwitch server via a Unix socket. print_switch_list(self) : Lists all existing switches in the Open vSwitch database. This method is not used here. list_port_attributes(self, port_name, bridge_name="dsw-host") : Lists all attributes for a specific port on the `dsw-host` given bridge. get_bridge_for_port(self, port_name) : Determines which bridge a port belongs to. The `main()` function of the script creates an instance of the OVSDB manager, prompts the user for a port name, checks which bridge the port belongs to, and then lists the detailed attributes of the port if it belongs to `dsw-host`. Here is an example of the code execution: ```bash python3 02_ovsdb_list_port.py ``` ```bash= Attempting to connect to remote Open vSwitch server via Unix socket... Enter the port name to examine (e.g. tap123): tap700 Confirmed: Port 'tap700' belongs to 'dsw-host'. Attributes for port 'tap700': Attribute Value ----------------- ---------------------------------------------- _uuid 0a79920b-9e45-4cea-8292-5779ade33d2d bond_active_slave [] bond_downdelay 0 bond_fake_iface False bond_mode [] bond_updelay 0 cvlans [] external_ids {} fake_bridge False interfaces [UUID('30b469a1-b5a6-4c23-8d8f-0d863b6bff1d')] lacp [] mac [] name tap700 other_config {} protected False qos [] rstp_statistics {} rstp_status {} statistics {} status {} tag [] trunks [] vlan_mode trunk ``` ### Step 4: Get `vlan_mode` and VLAN id(s) of a switch port To go one step further, we want to be more specific about the `vlan_mode` attribute and the VLAN IDs depending on its value: `access` or `trunk`. We want this new step script named `03_ovsdb_list_port_mode.py` to extend the functionality of `02_ovsdb_list_port.py` by implementing a more sophisticated approach to port information retrieval and VLAN configuration analysis. We want this new step script named `03_ovsdb_list_port_mode.py` to extend the functionality of `02_ovsdb_list_port.py` by implementing a more sophisticated approach to port information retrieval and VLAN configuration analysis. In order to avoid redundant database queries, we have to add two private methods to the OVSDBManager class: `_get_port_details()` : Collects all port attributes at once and stores the records in a private `_port_cache` dictionary. `_clear_cache()` : Clears records for a specific port or the entire dictionary. Here is the code of these two new private methods to insert in the OVSDBManager class: ```python= def _get_port_details(self, port_name, force_refresh=False): """ Private method that retrieves port details, using cache when available Args: port_name (str): Name of the port to get details for force_refresh (bool): If True, ignore cache and fetch fresh data CLI equivalent: ovs-vsctl list port <port_name> Returns: dict: Port details record or None if port not found """ try: # Check if we already have this port's details in cache if not force_refresh and port_name in self._port_cache: return self._port_cache[port_name] # Get port details via OVSDB command cmd = self.ovs.db_find("Port", ("name", "=", port_name)) port_records = cmd.execute() if not port_records: print(f"No details found for port '{port_name}'.") return None # Cache the result for future use self._port_cache[port_name] = port_records[0] return self._port_cache[port_name] except Exception as e: print(f"Error retrieving port details: {e}") return None def _clear_cache(self, port_name=None): """ Private method that clears the port details cache Args: port_name (str, optional): If provided, clear only this port's cache If None, clear the entire cache """ if port_name: if port_name in self._port_cache: del self._port_cache[port_name] else: self._port_cache.clear() ``` Once the private methods are in place, we can add specialized methods for VLAN configuration analysis: `get_port_mode()` : Determines if a port is in access or trunk mode by examining the vlan_mode attribute `get_port_access_vlan()` : Retrieves the VLAN tag for access ports `get_port_trunk_vlan_list()` : Retrieves the list of allowed VLANs for trunk ports ```python= def get_port_mode(self, port_name): """ Determines if a port is in access or trunk mode by checking vlan_mode attribute CLI equivalent: ovs-vsctl get port <port_name> vlan_mode Returns: "access" - if the port is in access mode "trunk" - if the port is in trunk mode None - if port not found or error """ # Get port details record = self._get_port_details(port_name) if not record: return None # Check the vlan_mode attribute to determine port mode if "vlan_mode" in record: return record["vlan_mode"] else: print(f"Error: vlan_mode attribute not found for port '{port_name}'.") return None def get_port_access_vlan(self, port_name): """ Returns the VLAN ID (tag) for a port in access mode CLI equivalent: ovs-vsctl get port <port_name> tag Returns: VLAN ID (integer) if port is in access mode and has a VLAN None if port is not in access mode or has no VLAN or not found """ # Get port details record = self._get_port_details(port_name) if not record: return None # Check if this is an access port with a VLAN tag port_mode = self.get_port_mode(port_name) if port_mode == "access" and "tag" in record and record["tag"]: return record["tag"] else: return None def get_port_trunk_vlan_list(self, port_name): """ Returns the list of allowed VLAN IDs for a port in trunk mode CLI equivalent: ovs-vsctl get port <port_name> trunks Returns: List of VLAN IDs if port is in trunk mode Empty list if port is in trunk mode but no VLANs are configured None if port is not in trunk mode or not found """ # Get port details record = self._get_port_details(port_name) if not record: return None # Check if this is a trunk port port_mode = self.get_port_mode(port_name) if port_mode == "trunk": if "trunks" in record and record["trunks"]: # Return the list of allowed VLANs return record["trunks"] else: # Trunk port but no specific VLANs configured (all allowed) return [] else: return None ``` Instead of displaying all port attributes, the main function provides a focused output that displays only the relevant VLAN information based on the port's operating mode. This targeted approach provides more meaningful insight into the network segmentation configuration, while maintaining better performance through data caching and optimized database interactions. Here is a copy of the main function of the `03_ovsdb_list_port_mode.py` script: ```python= def main(): print("Attempting to connect to remote Open vSwitch server via Unix socket...") try: # Create manager with automatic connection manager = OVSDBManager() # Ask user for port name to examine port_name = input("\nEnter the port name to examine (e.g. tap123): ") # Check which bridge this port belongs to bridge = manager.get_bridge_for_port(port_name) if bridge is None: print(f"Error: Port '{port_name}' does not exist on any switch.") sys.exit(1) # Exit with error code elif bridge != SWITCH_NAME: print( f"Error: Port '{port_name}' belongs to '{bridge}', not to '{SWITCH_NAME}'." ) sys.exit(1) # Exit with error code else: print(f"Confirmed: Port '{port_name}' belongs to '{SWITCH_NAME}'.") # No need to explicitly call get_port_details anymore # Get and display port mode port_mode = manager.get_port_mode(port_name) print(f"\nPort mode: {port_mode}") if port_mode is None: sys.exit(1) # Exit with error code if port mode cannot be determined # Get and display VLAN information based on port mode if port_mode == "access": vlan_id = manager.get_port_access_vlan(port_name) if vlan_id is not None: print(f"Access VLAN ID: {vlan_id}") else: print("No VLAN configured for this access port") elif port_mode == "trunk": vlan_list = manager.get_port_trunk_vlan_list(port_name) if vlan_list: print(f"Allowed VLANs: {vlan_list}") else: print("All VLANs are allowed on this trunk port") else: print("Unable to determine port mode") sys.exit(1) # Exit with error code # If we reach here, everything was successful sys.exit(0) # Exit with success code except Exception as e: print(f"Connection error: {e}") sys.exit(1) # Exit with error code ``` Once all the script code is assembled, we are ready to run tests on different switch port configurations: ```bash python3 03_ovsdb_list_port_mode.py ``` In the first case, the `tap7` switch port is in **access mode**. ```bash= Attempting to connect to remote Open vSwitch server via Unix socket... Enter the port name to examine (e.g. tap123): tap7 Confirmed: Port 'tap7' belongs to 'dsw-host'. Port mode: access Access VLAN ID: 52 ``` In the second example, the `tap20` port is in **trunk mode**. ```bash= Attempting to connect to remote Open vSwitch server via Unix socket... Enter the port name to examine (e.g. tap123): tap20 Confirmed: Port 'tap20' belongs to 'dsw-host'. Port mode: trunk Allowed VLANs: [52, 1220, 1221] ``` ## Part 2: Declare switch configuration using a YAML file In this part, we have reached the point where we want to apply a new configuration to switch ports. We will to use the declarative method using a YAML file as the desired configuration state or source of truth. Designing a source of truth is not an easy task. Here, we just need a starting point to illustrate the configuration of a switch with a few ports and their attributes. Here is our very first YAML configuration file: ### Step 1: Build your own YAML switch configuration file Create a new file named `switch_config.yaml` in the `$HOME/labs/lab09` directory. ```yaml= --- ovs: switches: - name: dsw-host ports: - name: tapXX2 type: OVSPort vlan_mode: access tag: 2X8 - name: tapXX5 type: OVSPort vlan_mode: access tag: 4X0 - name: tapXX6 type: OVSPort vlan_mode: trunk trunks: [4XX, 5XX, 6XX] ``` :::warning Make sure to use the tap interface and VLAN numbers assigned to you :zap: Edit and replace all XX marks. ::: ### Step 2: Add a new ConfigLoader class The `ConfigLoader` class we create in this step is a specialized component designed to parse, validate, and extract network configuration from YAML files, providing an interface for accessing switch and port configurations. Start by copying the existing script code to a new file: ```bash cp 03_ovsdb_list_port_mode.py 04_ovsdb_apply_config.py ``` Insert the following `ConfigLoader` class code to the new script named `04_ovsdb_apply_config.py`. ```python= class ConfigLoader: """Class for loading and parsing YAML configuration files""" def __init__(self, config_file): """ Initialize the configuration loader Args: config_file (str): Path to the YAML configuration file """ self.config_file = config_file self.config = None def load_config(self): """ Load the configuration from the YAML file Returns: dict: The loaded configuration or None if there was an error """ try: if not os.path.exists(self.config_file): print(f"Error: Configuration file '{self.config_file}' not found.") return None with open(self.config_file, "r") as file: self.config = yaml.safe_load(file) return self.config except Exception as e: print(f"Error loading configuration: {e}") return None def get_switches(self): """ Get the list of switches from the configuration Returns: list: List of switch configurations or empty list if not found """ if not self.config: return [] return self.config.get("ovs", {}).get("switches", []) def get_ports_for_switch(self, switch_name): """ Get the list of ports for a specific switch Args: switch_name (str): Name of the switch Returns: list: List of port configurations or empty list if not found """ switches = self.get_switches() for switch in switches: if switch.get("name") == switch_name: return switch.get("ports", []) return [] def print_config_summary(self): """Print a summary of the loaded configuration""" if not self.config: print("No configuration loaded.") return switches = self.get_switches() print(f"Configuration loaded: {len(switches)} switch(es) defined") for switch in switches: switch_name = switch.get("name", "Unknown") ports = switch.get("ports", []) print(f"- Switch '{switch_name}': {len(ports)} port(s) defined") # Print port summary in a table if ports: port_data = [] for port in ports: mode = port.get("vlan_mode", "unknown") if mode == "access": vlan_info = f"VLAN {port.get('tag', 'None')}" elif mode == "trunk": vlan_info = f"VLANs {port.get('trunks', [])}" else: vlan_info = "No VLAN info" port_data.append([port.get("name", "Unknown"), mode, vlan_info]) print( tabulate( port_data, headers=["Port", "Mode", "VLAN Config"], tablefmt="simple", ) ) ``` Here is a short description of the `ConfigLoader` class methods. `__init__(self, config_file)` : Initializes the loader with the path to a YAML configuration file and prepares the internal state. `load_config(self)` : Reads and parses the YAML file, handling file existence checks and parsing errors, returning the configuration data structure or None if errors occur. `get_switches(self)` : Extracts and returns the list of switch configurations from the loaded data, following the expected structure where switches are defined under the `ovs.switches` path. `get_ports_for_switch(self, switch_name)` : Retrieves the port configurations specific to a named switch, facilitating targeted access to port details. `print_config_summary(self)` : Generates and displays a human-readable summary of the configuration, including switch count, ports per switch, and a formatted table showing VLAN configurations for each port. ### Step 3: Edit the script main function The `main()` must be edited to use the YAML configuration declaration file. The command-line interface is managed using Python's argparse module, which parses the required configuration file paths as positional arguments and supports a `--dry-run` flag that allows administrators to validate configuration changes by displaying a summary of intended network changes without actually applying them to the switch. This provides a preview mechanism before committing any changes to the production environment. Therefore, we need to add Python module import statements at the top of the script code. ```python= import argparse import os.path import sys import yaml ``` Here is a copy of the edited `main()` function code. ```python= def main(): # Parse command line arguments parser = argparse.ArgumentParser( description="Apply YAML configuration to Open vSwitch" ) parser.add_argument("config_file", help="Path to the YAML configuration file") parser.add_argument( "--dry-run", action="store_true", help="Print configuration but do not apply it" ) args = parser.parse_args() try: # Load configuration config_loader = ConfigLoader(args.config_file) config = config_loader.load_config() if not config: print("Failed to load configuration.") sys.exit(1) # Display configuration summary config_loader.print_config_summary() # If dry run, exit here if args.dry_run: print("\nDry run completed. No changes were made.") sys.exit(0) except Exception as e: print(f"Error: {e}") sys.exit(1) ``` ### Step 4: Test the `--dry-run` flag Here is a sample run of the `04_ovsdb_apply_config.py` script with the `--dry-run` flag ```bash python3 04_ovsdb_apply_config.py switch_config.yml --dry-run ``` ```bash= Configuration loaded: 1 switch(es) defined - Switch 'dsw-host': 3 port(s) defined Port Mode VLAN Config ------ ------ -------------------- tap2 access VLAN 52 tap5 access VLAN 410 tap9 trunk VLANs [52, 410, 420] Dry run completed. No changes were made. ``` The output shows that the YAML declarations were parsed correctly. ## Part 3: Apply switch ports configuration We are now ready to apply the configuration parameters declared in the YAML file to the switch ports. To do this, we need to extend the functionality of the `OVSDBManager` class by transforming it from a pure information tool to a configuration management system. The focus here is on a caching architecture that tracks both port details and bridge information, resulting in faster execution and fewer database queries. In addition, this implementation enforces proper VLAN configuration practices by automatically resolving conflicting settings when switching between access and trunk modes, ensuring configuration integrity across the virtual switch infrastructure. ### Step 1: Transform the OVSDBManager class code Here is a new copy of the `OVSDBManager` class code that includes the extended caching features and the switch port configuration methods. ```python= class OVSDBManager: """Class for managing OVSDB connections and operations""" # Class constants OVS_CONNECTION = "unix:/tmp/ovs-forwarded.sock" OVSDB_CONNECTION_TIMEOUT = 30 def __init__(self, auto_connect=True): """Initialize the OVSDB manager and establish connection if auto_connect is True""" self.conn = None self.ovs = None # Cache to store details of previously queried ports self._port_cache = {} # Cache to store list of bridges self._bridges_cache = None if auto_connect: try: self.connect() except Exception as e: print(f"Warning: Failed to connect automatically: {e}") print( "You'll need to call connect() manually before using other methods." ) def connect(self): """Establish a connection to Open vSwitch via Unix socket""" helper = idlutils.get_schema_helper(self.OVS_CONNECTION, "Open_vSwitch") helper.register_all() idl = connection.OvsdbIdl(self.OVS_CONNECTION, helper) self.conn = connection.Connection( idl=idl, timeout=self.OVSDB_CONNECTION_TIMEOUT ) self.ovs = ovs_impl_idl.OvsdbIdl(self.conn) return self.ovs def _get_bridges(self, force_refresh=False): """ Private method that retrieves the list of bridges, using cache when available Args: force_refresh (bool): If True, ignore cache and fetch fresh data CLI equivalent: ovs-vsctl list-br Returns: list: List of bridge names or empty list if error """ try: # Check if we already have the bridges in cache if not force_refresh and self._bridges_cache is not None: return self._bridges_cache # Get bridges from database bridges = self.ovs.list_br().execute() # Cache the result for future use self._bridges_cache = bridges return self._bridges_cache except Exception as e: print(f"Error retrieving bridges: {e}") return [] def _get_port_details(self, port_name, force_refresh=False): """ Private method that retrieves port details, using cache when available Args: port_name (str): Name of the port to get details for force_refresh (bool): If True, ignore cache and fetch fresh data CLI equivalent: ovs-vsctl list port <port_name> Returns: dict: Port details record or None if port not found """ try: # Check if we already have this port's details in cache if not force_refresh and port_name in self._port_cache: return self._port_cache[port_name] # Get port details via OVSDB command cmd = self.ovs.db_find("Port", ("name", "=", port_name)) port_records = cmd.execute() if not port_records: print(f"No details found for port '{port_name}'.") return None # Cache the result for future use self._port_cache[port_name] = port_records[0] return self._port_cache[port_name] except Exception as e: print(f"Error retrieving port details: {e}") return None def _clear_cache(self, port_name=None, clear_bridges=False): """ Private method that clears the caches Args: port_name (str, optional): If provided, clear only this port's cache If None, clear all port caches clear_bridges (bool): If True, clear the bridges cache """ if port_name: if port_name in self._port_cache: del self._port_cache[port_name] else: self._port_cache.clear() if clear_bridges: self._bridges_cache = None def list_port_attributes(self, port_name, bridge_name=SWITCH_NAME): """ Lists all attributes for a specific port on a given bridge CLI equivalent: ovs-vsctl list port <port_name> """ try: # Check if the bridge exists bridges = self._get_bridges() if bridge_name not in bridges: print(f"Error: Bridge '{bridge_name}' does not exist.") return None # Check if the port exists on the bridge ports = self.ovs.list_ports(bridge_name).execute() if port_name not in ports: print( f"Error: Port '{port_name}' does not exist on bridge '{bridge_name}'." ) return None # Get port details record = self._get_port_details(port_name) if not record: return None print(f"\nAttributes for port '{port_name}':") # Prepare data for tabulate # Convert record to a list of [key, value] pairs and sort alphabetically by key table_data = sorted( [[k, str(v)] for k, v in record.items()], key=lambda x: x[0] ) # Display using tabulate with fancy_grid format print( tabulate( table_data, headers=["Attribute", "Value"], tablefmt="fancy", ) ) return [record] # Return in same format as before for compatibility except Exception as e: print(f"Error retrieving port attributes: {e}") return None def get_bridge_for_port(self, port_name): """ Determines which bridge a port belongs to CLI equivalent: ovs-vsctl port-to-br <port_name> """ try: # Get all bridges bridges = self._get_bridges() # Check each bridge for the port for bridge in bridges: ports = self.ovs.list_ports(bridge).execute() if port_name in ports: return bridge # Port not found on any bridge return None except Exception as e: print(f"Error determining bridge for port: {e}") return None def get_port_mode(self, port_name): """ Determines if a port is in access or trunk mode by checking vlan_mode attribute CLI equivalent: ovs-vsctl get port <port_name> vlan_mode Returns: "access" - if the port is in access mode "trunk" - if the port is in trunk mode None - if port not found or error """ # Get port details record = self._get_port_details(port_name) if not record: return None # Check the vlan_mode attribute to determine port mode if "vlan_mode" in record: return record["vlan_mode"] else: print(f"Error: vlan_mode attribute not found for port '{port_name}'.") return None def get_port_access_vlan(self, port_name): """ Returns the VLAN ID (tag) for a port in access mode CLI equivalent: ovs-vsctl get port <port_name> tag Returns: VLAN ID (integer) if port is in access mode and has a VLAN None if port is not in access mode or has no VLAN or not found """ # Get port details record = self._get_port_details(port_name) if not record: return None # Check if this is an access port with a VLAN tag port_mode = self.get_port_mode(port_name) if port_mode == "access" and "tag" in record and record["tag"]: return record["tag"] else: return None def get_port_trunk_vlan_list(self, port_name): """ Returns the list of allowed VLAN IDs for a port in trunk mode CLI equivalent: ovs-vsctl get port <port_name> trunks Returns: List of VLAN IDs if port is in trunk mode Empty list if port is in trunk mode but no VLANs are configured None if port is not in trunk mode or not found """ # Get port details record = self._get_port_details(port_name) if not record: return None # Check if this is a trunk port port_mode = self.get_port_mode(port_name) if port_mode == "trunk": if "trunks" in record and record["trunks"]: # Return the list of allowed VLANs return record["trunks"] else: # Trunk port but no specific VLANs configured (all allowed) return [] else: return None def apply_port_config(self, port_config, bridge_name=SWITCH_NAME): """ Apply configuration to a port Args: port_config (dict): Port configuration dictionary bridge_name (str): Name of the bridge to which the port belongs Returns: bool: True if successful, False otherwise """ try: port_name = port_config.get("name") if not port_name: print("Error: Port configuration missing 'name' field.") return False # Check if the bridge exists bridges = self._get_bridges() if bridge_name not in bridges: print(f"Error: Bridge '{bridge_name}' does not exist.") return False # Verify the port belongs to the bridge actual_bridge = self.get_bridge_for_port(port_name) if actual_bridge != bridge_name: print( f"Error: Port '{port_name}' does not exist on bridge '{bridge_name}'." ) return False # Clear the cache for this port to ensure fresh data self._clear_cache(port_name) # Apply VLAN mode configuration vlan_mode = port_config.get("vlan_mode") if vlan_mode: print(f"Setting port '{port_name}' VLAN mode to '{vlan_mode}'") self.ovs.db_set("Port", port_name, ("vlan_mode", vlan_mode)).execute() # Apply mode-specific cleanup if vlan_mode == "access": # If setting to access mode, clear trunks self.ovs.db_set("Port", port_name, ("trunks", [])).execute() elif vlan_mode == "trunk": # If setting to trunk mode, clear tag self.ovs.db_set("Port", port_name, ("tag", [])).execute() # Apply VLAN tag for access ports if vlan_mode == "access" and "tag" in port_config: tag = port_config.get("tag") print(f"Setting port '{port_name}' access VLAN tag to {tag}") self.ovs.db_set("Port", port_name, ("tag", tag)).execute() # Apply VLAN trunks for trunk ports if vlan_mode == "trunk" and "trunks" in port_config: trunks = port_config.get("trunks") print(f"Setting port '{port_name}' trunk VLANs to {trunks}") self.ovs.db_set("Port", port_name, ("trunks", trunks)).execute() # Success return True except Exception as e: print(f"Error applying port configuration: {e}") return False def apply_switch_config(self, switch_config): """ Apply configuration to a switch and its ports Args: switch_config (dict): Switch configuration dictionary Returns: bool: True if successful, False otherwise """ try: switch_name = switch_config.get("name") if not switch_name: print("Error: Switch configuration missing 'name' field.") return False # Check if the switch exists bridges = self._get_bridges( force_refresh=True ) # Force refresh to get latest state if switch_name not in bridges: print(f"Error: Switch '{switch_name}' does not exist.") return False # Apply port configurations ports = switch_config.get("ports", []) success_count = 0 for port_config in ports: if self.apply_port_config(port_config, switch_name): success_count += 1 print( f"Applied configuration to {success_count} out of {len(ports)} ports." ) # After applying configurations, clear bridges cache to ensure fresh data next time self._clear_cache(clear_bridges=True) return success_count == len(ports) except Exception as e: print(f"Error applying switch configuration: {e}") return False ``` Here is a short decsription of the new methods introduced in this step. `_get_bridges(force_refresh=False)` : A private method that retrieves the list of bridges with efficient caching behavior, reducing redundant database queries. `_clear_cache(port_name=None, clear_bridges=False)` : An enhanced cache management method that can selectively clear port cache entries and/or the bridge cache. `apply_port_config(port_config, bridge_name)` : Applies VLAN configuration to a specific port, including setting the VLAN mode, access VLAN tag, and trunk VLAN lists while enforcing proper configuration by clearing conflicting settings. `apply_switch_config(switch_config)` : Orchestrates the configuration of an entire switch and its ports by iterating through port configurations and applying them with appropriate verification. ### Step 2: Apply the switch ports configuration Now that our Python code is complete, we are ready to apply the declared switch ports configuration. ```bash python3 04_ovsdb_apply_config.py switch_config.yml ``` ```bash= Configuration loaded: 1 switch(es) defined - Switch 'dsw-host': 3 port(s) defined Port Mode VLAN Config ------ ------ -------------------- tap2 access VLAN 52 tap5 access VLAN 410 tap9 trunk VLANs [52, 410, 420] Setting port 'tap2' VLAN mode to 'access' Setting port 'tap2' access VLAN tag to 52 Setting port 'tap5' VLAN mode to 'access' Setting port 'tap5' access VLAN tag to 410 Setting port 'tap9' VLAN mode to 'trunk' Setting port 'tap9' trunk VLANs to [52, 410, 420] Applied configuration to 3 out of 3 ports. Configuration applied successfully! ``` ### Step 3: Verify the Applied Configuration from the Hypervisor's SSH Connection To complete this lab process, we need to manually verify the switch ports parameters from the hypervisor console. Therefore, we will list the attributes of two different ports: one in access mode and the other in trunk mode. - The `tap5` switch port is configured in access mode and belongs to VLAN 410: ```bash sudo ovs-vsctl list port tap5 ``` ```bash= _uuid : 268a9e67-b9cb-4478-8410-2e99c3b43812 bond_active_slave : [] bond_downdelay : 0 bond_fake_iface : false bond_mode : [] bond_updelay : 0 cvlans : [] external_ids : {} fake_bridge : false interfaces : [72d6b44a-0d32-48e7-a6ab-89e3edd41a45] lacp : [] mac : [] name : tap5 other_config : {} protected : false qos : [] rstp_statistics : {} rstp_status : {} statistics : {} status : {} tag : 410 trunks : [] vlan_mode : access ``` - The `tap9` switch port is configured in trunk mode with three VLANs allowed: ```bash sudo ovs-vsctl list port tap9 ``` ```bash= _uuid : fc5b244a-8e43-4f64-83b1-bbd8176fa4ac bond_active_slave : [] bond_downdelay : 0 bond_fake_iface : false bond_mode : [] bond_updelay : 0 cvlans : [] external_ids : {} fake_bridge : false interfaces : [173e12f5-0cb1-4046-8ad9-45de33d7cad6] lacp : [] mac : [] name : tap9 other_config : {} protected : false qos : [] rstp_statistics : {} rstp_status : {} statistics : {} status : {} tag : [] trunks : [52, 410, 420] vlan_mode : trunk ``` ## Conclusion This lab has provided a foundational understanding of network programmability by leveraging Open vSwitch and Python. Students have successfully connected to the OVSDB server, managed switch port configurations, and applied declarative configurations using YAML files. These skills are crucial in a DevOps environment where infrastructure as code (IaC) is increasingly important. The code and techniques presented here serve as a starting point for more advanced network programming tasks. As students progress, they will be able to build upon this foundation to program network flows in a more complex switch fabric. This could involve integrating OpenFlow controllers to dynamically manage traffic flows or exploring other network virtualization technologies like Open Virtual Network (OVN). The ability to automate and manage network configurations programmatically is essential for efficient and scalable network operations in modern data centers and cloud environments.