From f01de0fb60cf3bddbf3f531a4884a6a8258853dc Mon Sep 17 00:00:00 2001 From: liunick Date: Thu, 13 Nov 2025 11:02:55 -0800 Subject: [PATCH 1/6] optimize build process --- .github/workflows/build-switchgentool.yml | 10 ------ .gitignore | 1 + hook-convertors.py | 13 ------- src/__init__.py | 4 +++ src/convertors/__init__.py | 36 ++++++++++++++++++++ src/main.py | 41 +++++++++++++---------- tests/test_convertors.py | 4 +-- 7 files changed, 66 insertions(+), 43 deletions(-) delete mode 100644 hook-convertors.py create mode 100644 src/__init__.py create mode 100644 src/convertors/__init__.py diff --git a/.github/workflows/build-switchgentool.yml b/.github/workflows/build-switchgentool.yml index 99ffdca..c608da9 100644 --- a/.github/workflows/build-switchgentool.yml +++ b/.github/workflows/build-switchgentool.yml @@ -77,13 +77,8 @@ jobs: run: | pyinstaller --onefile --clean --noconfirm --noupx ` --name network_config_generator ` - --additional-hooks-dir=. ` --add-data "input;input" ` --collect-all jinja2 ` - --hidden-import=convertors ` - --hidden-import=convertors.convertors_lab_switch_json ` - --hidden-import=convertors.convertors_bmc_switch_json ` - --collect-submodules=convertors ` --exclude-module tkinter ` --exclude-module matplotlib ` --exclude-module PIL ` @@ -97,13 +92,8 @@ jobs: run: | pyinstaller --onefile --clean --noconfirm \ --name network_config_generator \ - --additional-hooks-dir=. \ --add-data "input:input" \ --collect-all jinja2 \ - --hidden-import=convertors \ - --hidden-import=convertors.convertors_lab_switch_json \ - --hidden-import=convertors.convertors_bmc_switch_json \ - --collect-submodules=convertors \ --exclude-module tkinter \ --exclude-module matplotlib \ --exclude-module PIL \ diff --git a/.gitignore b/.gitignore index 1894312..eb1666c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ # Test binary, built with `go test -c` *.test _* +!**/__init__.py # Output of the go coverage tool, specifically when used with LiteIDE *.out diff --git a/hook-convertors.py b/hook-convertors.py deleted file mode 100644 index e4f64a6..0000000 --- a/hook-convertors.py +++ /dev/null @@ -1,13 +0,0 @@ -# PyInstaller hook for convertors package -# This ensures all convertor modules are included in the bundle - -from PyInstaller.utils.hooks import collect_submodules, collect_data_files - -# Collect all submodules from the convertors package -hiddenimports = collect_submodules('convertors') - -# Also explicitly add the BMC converter module -if 'convertors.convertors_bmc_switch_json' not in hiddenimports: - hiddenimports.append('convertors.convertors_bmc_switch_json') - -print(f"[HOOK] convertors hiddenimports: {hiddenimports}") diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..8219b87 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,4 @@ +"""Package init for network config generator source code. + +Makes 'src' importable so that 'python -m src.main' works consistently. +""" diff --git a/src/convertors/__init__.py b/src/convertors/__init__.py new file mode 100644 index 0000000..806f1d9 --- /dev/null +++ b/src/convertors/__init__.py @@ -0,0 +1,36 @@ +""" +Convertors Package - Static Registry Pattern + +This package contains convertor modules that transform various input formats +into the standardized JSON format used by the network config generator. + +All convertors are statically imported here so PyInstaller can detect them +without needing hooks or hidden imports. + +Available convertors: +- convertors_lab_switch_json: Converts lab-style input JSON to standard format +- convertors_bmc_switch_json: Converts BMC switch definitions to standard format +""" + +# Static imports - PyInstaller can detect these automatically +from .convertors_lab_switch_json import convert_switch_input_json as convert_lab_switches +from .convertors_bmc_switch_json import convert_bmc_switches + +# Registry mapping convertor names to functions +# This allows main.py to use registry lookup instead of dynamic imports +CONVERTORS = { + # Primary convertor names (for backward compatibility) + 'convertors.convertors_lab_switch_json': convert_lab_switches, + 'convertors.convertors_bmc_switch_json': convert_bmc_switches, + + # Short aliases for convenience + 'lab': convert_lab_switches, + 'bmc': convert_bmc_switches, +} + +# Export public API +__all__ = [ + 'CONVERTORS', + 'convert_lab_switches', + 'convert_bmc_switches', +] diff --git a/src/main.py b/src/main.py index 463e8cf..b531c80 100644 --- a/src/main.py +++ b/src/main.py @@ -3,7 +3,6 @@ import sys import json import shutil -import importlib # Support both execution styles: # 1. python src/main.py (src not a package on sys.path root) @@ -41,30 +40,32 @@ def safe_print(text): safe_text = text.encode('ascii', errors='replace').decode('ascii') print(safe_text) -def load_convertor(convertor_module_path): +def load_convertor(convertor_name): """ - Dynamically load a convertor module and return its convert function. + Load a convertor function from the static registry. Args: - convertor_module_path: String path to convertor module (e.g., "convertors.convertors_lab_switch_json") + convertor_name: String name of convertor (e.g., "convertors.convertors_lab_switch_json" or "lab") Returns: Function that can convert input data to standard format """ try: - # Import the module - module = importlib.import_module(convertor_module_path) + from convertors import CONVERTORS - # Look for the conversion function (assuming it's named convert_switch_input_json) - if hasattr(module, 'convert_switch_input_json'): - return module.convert_switch_input_json + if convertor_name in CONVERTORS: + return CONVERTORS[convertor_name] else: - raise AttributeError(f"Module {convertor_module_path} does not have 'convert_switch_input_json' function") + available = ', '.join(CONVERTORS.keys()) + raise ValueError( + f"Unknown convertor '{convertor_name}'.\n" + f"Available convertors: {available}" + ) except ImportError as e: - raise ImportError(f"Failed to import convertor module '{convertor_module_path}': {e}") + raise ImportError(f"Failed to import convertors package: {e}") except Exception as e: - raise RuntimeError(f"Failed to load convertor from '{convertor_module_path}': {e}") + raise RuntimeError(f"Failed to load convertor '{convertor_name}': {e}") def is_standard_format(data): """ @@ -119,22 +120,26 @@ def convert_to_standard_format(input_file_path, output_dir, convertor_module_pat def main(): parser = argparse.ArgumentParser( - description="Network config generator - automatically detects input format and converts if needed, then generates configs.", - epilog="Workflow: 1) Check if input is standard format 2) If not, convert using specified convertor 3) Generate config files from standard format" + epilog=""" +Examples: + %(prog)s --input_json input/standard_input.json --output_folder output/ + %(prog)s --input_json my_lab_input.json --output_folder configs/ --convertor lab + """, + formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument("--input_json", required=True, - help="Path to input JSON file (can be lab format or standard format)") + help="Path to input JSON file (lab or standard format)") parser.add_argument("--template_folder", default="input/jinja2_templates", - help="Root folder containing vendor templates (default: input/jinja2_templates)") + help="Folder containing Jinja2 templates (default: input/jinja2_templates)") parser.add_argument("--output_folder", default=".", - help="Directory to save generated config files (default: current directory)" + help="Directory to save generated configs (default: current directory)" ) parser.add_argument("--convertor", default="convertors.convertors_lab_switch_json", - help="Python module path for the convertor to use when input is not in standard format. Only used if conversion is needed. (default: convertors.convertors_lab_switch_json)") + help="Convertor to use for non-standard input formats (default: convertors.convertors_lab_switch_json)") args = parser.parse_args() diff --git a/tests/test_convertors.py b/tests/test_convertors.py index b041804..15dbfa4 100644 --- a/tests/test_convertors.py +++ b/tests/test_convertors.py @@ -15,7 +15,7 @@ sys.path.insert(0, str(SRC_PATH)) # === Imports from your project === -from convertors.convertors_lab_switch_json import convert_switch_input_json +from convertors import convert_lab_switches from loader import load_input_json # === Helper function for better diff reporting === @@ -83,7 +83,7 @@ def run_convert_and_compare(folder_name, input_file): old_stdout = sys.stdout sys.stdout = io.StringIO() try: - convert_switch_input_json(input_data, output_dir) + convert_lab_switches(input_data, output_dir) finally: sys.stdout = old_stdout From b04c34463ebf1a01f1b68f7f3c96344781ac9bc6 Mon Sep 17 00:00:00 2001 From: liunick Date: Thu, 13 Nov 2025 16:46:46 -0800 Subject: [PATCH 2/6] update readme accordingly --- .devcontainer/Dockerfile | 20 --------------- .devcontainer/devcontainer.json | 43 --------------------------------- README.md | 24 ++++++++++++------ 3 files changed, 17 insertions(+), 70 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index cf4857d..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/go/.devcontainer/base.Dockerfile - -# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.16, 1.17, 1-bullseye, 1.16-bullseye, 1.17-bullseye, 1-buster, 1.16-buster, 1.17-buster -ARG VARIANT="1.18-bullseye" -FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT} - -# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 -ARG NODE_VERSION="none" -RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi - -# [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends - -# [Optional] Uncomment the next lines to use go get to install anything else you need -# USER vscode -# RUN go get -x - -# [Optional] Uncomment this line to install global node packages. -# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 2507c81..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,43 +0,0 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/go -{ - "name": "Go", - "build": { - "dockerfile": "Dockerfile", - "args": { - // Update the VARIANT arg to pick a version of Go: 1, 1.18, 1.17 - // Append -bullseye or -buster to pin to an OS version. - // Use -bullseye variants on local arm64/Apple Silicon. - "VARIANT": "1.17", - // Options - "NODE_VERSION": "lts/*" - } - }, - "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], - - // Set *default* container specific settings.json values on container create. - "settings": { - "go.toolsManagement.checkForUpdates": "local", - "go.useLanguageServer": true, - "go.gopath": "/go" - }, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "golang.Go", - "premparihar.gotestexplorer", - "quicktype.quicktype" - ], - - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - - // Use 'postCreateCommand' to run commands after the container is created. - // "postCreateCommand": "go version", - - // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode", - "features": { - "github-cli": "latest" - } -} diff --git a/README.md b/README.md index 6e83a2d..8283f79 100644 --- a/README.md +++ b/README.md @@ -180,11 +180,14 @@ root/ │ ├── cisco/ │ └── dellemc/ ├── src/ # Source code +│ ├── __init__.py # Package initialization │ ├── main.py # Entry point for the tool │ ├── generator.py # Main generation logic │ ├── loader.py # Input file loading and parsing │ └── convertors/ # Input format converters -│ └── convertors_lab_switch_json.py # Lab format to standard JSON converter +│ ├── __init__.py # Convertor registry +│ ├── convertors_lab_switch_json.py # Lab format converter +│ └── convertors_bmc_switch_json.py # BMC switch converter ├── tests/ # Test files │ ├── test_generator.py # Unit tests for generator logic │ ├── test_convertors.py # Unit tests for input conversion @@ -247,21 +250,28 @@ The tool accepts these parameters: | Parameter | Required | Description | |-------------------|----------|-------------| -| `--input_json` | ✅ Yes | Path to your input JSON file | -| `--output_folder` | ✅ Yes | Directory to save generated configs | -| `--convertor` | ❌ No | Custom converter for non-standard formats | +| `--input_json` | ✅ Yes | Path to your input JSON file (lab or standard format) | +| `--output_folder` | ❌ No | Directory to save generated configs (default: current directory) | +| `--template_folder` | ❌ No | Folder containing Jinja2 templates (default: input/jinja2_templates) | +| `--convertor` | ❌ No | Convertor to use for non-standard formats (default: convertors.convertors_lab_switch_json) | ### Quick Examples ```bash # Basic usage - auto-detects input format -python src/main.py --input_json your_input.json --output_folder outputs/ +python src/main.py --input_json input/standard_input.json --output_folder output/ + +# With custom output directory +python src/main.py --input_json my_input.json --output_folder configs/ + +# Using short convertor alias +python src/main.py --input_json lab_input.json --output_folder output/ --convertor lab # Using the standalone executable (Windows) -.\network_config_generator.exe --input_json your_input.json --output_folder outputs\ +network_config_generator.exe --input_json input/standard_input.json --output_folder output/ # Using the standalone executable (Linux) -./network_config_generator --input_json your_input.json --output_folder outputs/ +./network_config_generator --input_json input/standard_input.json --output_folder output/ ``` > [!IMPORTANT] From 7fddf948a504ea516477b4a237e8dac627d60855 Mon Sep 17 00:00:00 2001 From: liunick Date: Tue, 25 Nov 2025 14:01:16 -0800 Subject: [PATCH 3/6] add validation and debug for vlan input --- input/jinja2_templates/cisco/nxos/system.j2 | 6 + .../dellemc/os10/full_config.j2 | 2 + .../dellemc/os10/interface.j2 | 18 +++ input/jinja2_templates/dellemc/os10/system.j2 | 17 +++ input/jinja2_templates/dellemc/os10/vlt.j2 | 45 +++++++ .../dellemc/N3248TE-ON.json | 14 +- .../dellemc/s5248f-on.json | 13 +- src/convertors/convertors_lab_switch_json.py | 124 +++++++++++++++--- src/generator.py | 21 ++- src/main.py | 28 +++- 10 files changed, 244 insertions(+), 44 deletions(-) create mode 100644 input/jinja2_templates/dellemc/os10/vlt.j2 diff --git a/input/jinja2_templates/cisco/nxos/system.j2 b/input/jinja2_templates/cisco/nxos/system.j2 index 7834f3d..e4ba43d 100644 --- a/input/jinja2_templates/cisco/nxos/system.j2 +++ b/input/jinja2_templates/cisco/nxos/system.j2 @@ -66,3 +66,9 @@ ip load-sharing address source-destination port source-destination ip icmp-errors source-interface [MGMT_VLAN] cli alias name wr copy running-config startup-config + +{# -------- SNMP Settings -------- #} +snmp-server community [PLACEHOLDER] ro +snmp-server community [PLACEHOLDER] rW +snmp-server contact "Contact Support" +snmp-server location {{ switch.site }} \ No newline at end of file diff --git a/input/jinja2_templates/dellemc/os10/full_config.j2 b/input/jinja2_templates/dellemc/os10/full_config.j2 index 335f153..47166d6 100644 --- a/input/jinja2_templates/dellemc/os10/full_config.j2 +++ b/input/jinja2_templates/dellemc/os10/full_config.j2 @@ -10,6 +10,8 @@ {% include "port_channel.j2" %} +{% include "vlt.j2" %} + {% include "prefix_list.j2" %} {% include "bgp.j2" %} diff --git a/input/jinja2_templates/dellemc/os10/interface.j2 b/input/jinja2_templates/dellemc/os10/interface.j2 index 40d4f39..c53cb3a 100644 --- a/input/jinja2_templates/dellemc/os10/interface.j2 +++ b/input/jinja2_templates/dellemc/os10/interface.j2 @@ -56,6 +56,24 @@ interface {% if is_range %}range {% endif %}{{ intf_name }} {%- endif %} {% endfor %} +{#- -------- MLAG Peer Link Interfaces -------- #} +{% for iface in interfaces if iface.type == "MLAG" %} +{%- set intf_range = iface.intf if iface.intf is defined else iface.start_intf ~ (('-' ~ iface.end_intf) if iface.end_intf is defined and iface.end_intf != iface.start_intf else '') %} +{%- set intf_name = iface.intf_type ~ ' ' ~ intf_range %} +{%- set is_range = '-' in intf_range | string %} +interface {% if is_range %}range {% endif %}{{ intf_name }} + description {{ iface.name }} + no switchport + mtu 9216 + flowcontrol receive off + priority-flow-control mode on + {%- if iface.shutdown is defined and iface.shutdown %} + shutdown + {%- else %} + no shutdown + {%- endif %} +{% endfor %} + {#- -------- L3 Interfaces (Ethernet only) -------- #} {% for iface in interfaces if iface.type == "L3" and iface.intf_type | lower == "ethernet" %} {%- set intf_range = iface.intf if iface.intf is defined else iface.start_intf ~ (('-' ~ iface.end_intf) if iface.end_intf is defined and iface.end_intf != iface.start_intf else '') %} diff --git a/input/jinja2_templates/dellemc/os10/system.j2 b/input/jinja2_templates/dellemc/os10/system.j2 index fa73451..d2d1dda 100644 --- a/input/jinja2_templates/dellemc/os10/system.j2 +++ b/input/jinja2_templates/dellemc/os10/system.j2 @@ -41,5 +41,22 @@ mac address-table aging-time 1000000 no ip dhcp-relay information-option no ip dhcp snooping +{# -------- VRRP Settings (skip for BMC) -------- #} +{%- if switch.type | upper != 'BMC' %} vrrp version 3 vrrp delay reload 180 +{%- endif %} + +{# -------- SNMP Settings -------- #} +! system.j2 - snmp_settings +snmp-server community [SNMP_COMMUNITY_RO] ro +snmp-server community [SNMP_COMMUNITY_RW] rw +snmp-server contact "Contact Support" +snmp-server location {{ switch.site }} + +{# -------- Management VRF Configuration -------- #} +! system.j2 - management_vrf +ip vrf management + interface management + +management route 0.0.0.0/0 [MGMT_GATEWAY_IP] \ No newline at end of file diff --git a/input/jinja2_templates/dellemc/os10/vlt.j2 b/input/jinja2_templates/dellemc/os10/vlt.j2 new file mode 100644 index 0000000..5f83f22 --- /dev/null +++ b/input/jinja2_templates/dellemc/os10/vlt.j2 @@ -0,0 +1,45 @@ +{%- set ns = namespace(ibgp_peer_ip='', mlag_intf_range='') -%} + +{#- Find MLAG peer link interface range from interfaces -#} +{%- if interfaces is defined -%} + {%- for iface in interfaces -%} + {%- if iface.type == 'MLAG' -%} + {%- if iface.start_intf is defined and iface.end_intf is defined -%} + {%- set ns.mlag_intf_range = 'ethernet' + iface.start_intf + '-' + iface.end_intf -%} + {%- elif iface.intf is defined -%} + {%- set ns.mlag_intf_range = 'ethernet' + iface.intf -%} + {%- endif -%} + {%- endif -%} + {%- endfor -%} +{%- endif -%} + +{#- Find iBGP peer IP from BGP neighbors -#} +{%- if bgp is defined and bgp.neighbors is defined -%} + {%- for neighbor in bgp.neighbors -%} + {%- if neighbor.description == 'iBGP_PEER' -%} + {%- set ns.ibgp_peer_ip = neighbor.ip -%} + {%- endif -%} + {%- endfor -%} +{%- endif -%} + +{#- Determine priority based on switch type -#} +{%- if switch.type == 'TOR1' -%} + {%- set priority = 1 -%} +{%- elif switch.type == 'TOR2' -%} + {%- set priority = 2 -%} +{%- else -%} + {%- set priority = 1 -%} +{%- endif -%} + +{#- Render VLT configuration if both iBGP peer IP and MLAG interface are found -#} +{%- if ns.ibgp_peer_ip and ns.mlag_intf_range -%} +! vlt.j2 +vlt-domain 1 + backup destination {{ ns.ibgp_peer_ip }} + discovery-interface {{ ns.mlag_intf_range }} + peer-routing + primary-priority {{ priority }} + vlt-mac de:ad:00:be:ef:01 +{%- elif ns.ibgp_peer_ip and not ns.mlag_intf_range -%} +! vlt.j2 - NOTICE: VLT configuration skipped - MLAG peer link interface not found +{%- endif -%} \ No newline at end of file diff --git a/input/switch_interface_templates/dellemc/N3248TE-ON.json b/input/switch_interface_templates/dellemc/N3248TE-ON.json index 32ce2f9..3328716 100644 --- a/input/switch_interface_templates/dellemc/N3248TE-ON.json +++ b/input/switch_interface_templates/dellemc/N3248TE-ON.json @@ -21,8 +21,15 @@ "intf_type": "Ethernet", "start_intf": "1/1/1", "end_intf": "1/1/40", - "access_vlan": "125", - "shutdown": false + "access_vlan": "125" + }, + { + "name": "HLH_BMC", + "type": "Access", + "description": "HLH BMC Connection", + "intf_type": "Ethernet", + "intf": "1/1/46", + "access_vlan": "125" }, { "name": "HLH_OS", @@ -31,8 +38,7 @@ "intf_type": "Ethernet", "start_intf": "1/1/49", "end_intf": "1/1/50", - "access_vlan": "7", - "shutdown": false + "access_vlan": "7" }, { "name": "To_TORs", diff --git a/input/switch_interface_templates/dellemc/s5248f-on.json b/input/switch_interface_templates/dellemc/s5248f-on.json index 8090a1f..12d7f2f 100644 --- a/input/switch_interface_templates/dellemc/s5248f-on.json +++ b/input/switch_interface_templates/dellemc/s5248f-on.json @@ -21,6 +21,13 @@ "intf": "loopback0", "ipv4": "" }, + { + "name": "MLAG_Peer_Links", + "type": "MLAG", + "intf_type": "Ethernet", + "start_intf": "1/1/49", + "end_intf": "1/1/52" + }, { "name": "P2P_Border1", "type": "L3", @@ -50,7 +57,7 @@ "type": "Trunk", "intf_type": "Ethernet", "start_intf": "1/1/1", - "end_intf": "1/1/18", + "end_intf": "1/1/40", "native_vlan": "M", "tagged_vlans": "M,C,S", "service_policy": { @@ -99,10 +106,10 @@ "description": "P2P_IBGP", "type": "L3", "ipv4": "", - "members": ["1/1/39", "1/1/40"] + "members": ["1/1/45", "1/1/46"] }, { - "id": 101, + "id": 102, "description": "ToR_Peer_Link", "type": "Trunk", "native_vlan": "99", diff --git a/src/convertors/convertors_lab_switch_json.py b/src/convertors/convertors_lab_switch_json.py index e255bb1..575cd50 100644 --- a/src/convertors/convertors_lab_switch_json.py +++ b/src/convertors/convertors_lab_switch_json.py @@ -48,7 +48,8 @@ "type" : "", "hostname": "", "version" : "", - "firmware": "" + "firmware": "", + "site" : "" } SVI_TEMPLATE = { @@ -78,6 +79,7 @@ def __init__(self, input_data: dict): self.ip_map = defaultdict(list) self.bgp_map = defaultdict(dict) self.deployment_pattern = input_data.get("InputData", {}).get("DeploymentPattern", "").lower() + self.site = input_data.get("InputData", {}).get("MainEnvData", [{}])[0].get("Site", "") # Translate hyperconverged to fully_converged for template compatibility if self.deployment_pattern == "hyperconverged": @@ -119,7 +121,8 @@ def build_switch(self, switch_type: str): type = sw_type, hostname = sw.get("Hostname", "").lower(), version = sw.get("Firmware", "").lower(), - firmware = firmware + firmware = firmware, + site = self.site ) self.sections["switch"][sw_type] = sw_entry @@ -349,25 +352,46 @@ def _resolve_interface_vlans(self, switch_type: str, vlans_name_string: str) -> for part in vlan_parts: part = part.strip() - if "S" in part: - # if TOR1 then resolve to S1, else S2 + # Explicit storage handling with fallback + if part == "S": + # Prefer ToR-specific storage lists if present + s_list = [] if switch_type == TOR1: - resolved_vlans.extend([str(vid) for vid in self.vlan_map["S1"]]) + s_list = self.vlan_map.get("S1", []) elif switch_type == TOR2: - resolved_vlans.extend([str(vid) for vid in self.vlan_map["S2"]]) - elif part in self.vlan_map and self.vlan_map[part]: - # Direct mapping from vlan_map - resolved_vlans.extend([str(vid) for vid in self.vlan_map[part]]) - elif part in self.vlan_map and not self.vlan_map[part]: - # Known VLAN symbol but empty list - skip this part + s_list = self.vlan_map.get("S2", []) + + if s_list: + resolved_vlans.extend([str(vid) for vid in s_list]) + else: + # Fallback to generic storage list if available + resolved_vlans.extend([str(vid) for vid in self.vlan_map.get("S", [])]) + continue + + # Allow explicit S1/S2 references + if part in ("S1", "S2"): + resolved_vlans.extend([str(vid) for vid in self.vlan_map.get(part, [])]) continue - else: - # Literal VLAN ID - keep as is (only if it's numeric) - if part.isdigit(): - resolved_vlans.append(part) - # Unknown symbols are skipped (not added) + + # Direct mapping for other symbolic sets (e.g., M, C, UNUSED, NATIVE) + if part in self.vlan_map: + resolved_vlans.extend([str(vid) for vid in self.vlan_map.get(part, [])]) + continue + + # Literal VLAN ID - keep as is (only if it's numeric) + if part.isdigit(): + resolved_vlans.append(part) + # Unknown symbols are skipped (not added) - return ",".join(resolved_vlans) + # De-duplicate while preserving order + seen = set() + unique_vlans = [] + for v in resolved_vlans: + if v not in seen: + seen.add(v) + unique_vlans.append(v) + + return ",".join(unique_vlans) def _get_l3_ip_for_interface(self, switch_type: str, interface: dict) -> str: """ @@ -529,7 +553,58 @@ def convert_switch_input_json(input_data: dict, output_dir: str = DEFAULT_OUTPUT builder.sections = {} # reset between runs builder.build_switch(sw_type) builder.build_vlans(sw_type) + + # Debug: Print key VLAN symbol mappings for visibility + m_vlans = builder.vlan_map.get("M", []) + c_vlans = builder.vlan_map.get("C", []) + s_vlans = builder.vlan_map.get("S", []) + s1_vlans = builder.vlan_map.get("S1", []) + s2_vlans = builder.vlan_map.get("S2", []) + print(f"[DEBUG] VLAN sets for {sw_type}: M={m_vlans} C={c_vlans} S={s_vlans} S1={s1_vlans} S2={s2_vlans}") + + # Validation: M and C must not be empty; S optional (warn if empty) + missing_critical = [] + if not m_vlans: + missing_critical.append("M") + if not c_vlans: + missing_critical.append("C") + + if missing_critical: + raise ValueError( + "Required VLAN set(s) missing for {sw}: {sets}. " + "Input Supernets must define Infrastructure (M) and Tenant/Compute (C) VLANs.".format( + sw=sw_type, + sets=", ".join(missing_critical) + ) + ) + + if not s_vlans and not s1_vlans and not s2_vlans: + print(f"[WARN] Storage VLAN set S is empty for {sw_type}; proceeding without storage tagging.") + builder.build_interfaces(sw_type) + # Per-interface VLAN security belt: ensure required VLAN values resolved + for iface in builder.sections.get("interfaces", []): + itype = iface.get("type", "") + name = iface.get("name", "(unnamed)") + if itype == "Access": + if not str(iface.get("access_vlan", "")).strip(): + raise ValueError(f"Access interface '{name}' has empty access_vlan. Define a valid VLAN ID in input template.") + elif itype == "Trunk": + if not str(iface.get("native_vlan", "")).strip(): + raise ValueError(f"Trunk interface '{name}' has empty native_vlan after resolution. Ensure Infrastructure (M) mapping or literal VLAN ID is present.") + if not str(iface.get("tagged_vlans", "")).strip(): + raise ValueError(f"Trunk interface '{name}' has empty tagged_vlans after resolution. Provide Tenant/Compute (C) / Storage (S) VLANs or explicit IDs.") + + # Port-channel VLAN validation (trunk-type port-channels) + for pc in builder.sections.get("port_channels", []): + pctype = pc.get("type", "") + desc = pc.get("description", f"ID {pc.get('id','?')}") + if pctype == "Trunk": + if not str(pc.get("native_vlan", "")).strip(): + raise ValueError(f"Port-channel '{desc}' missing native_vlan. Define native VLAN or remove trunk type.") + # tagged_vlans may be intentionally empty (e.g., only native) so only warn + if not str(pc.get("tagged_vlans", "")).strip(): + print(f"[WARN] Port-channel '{desc}' has no tagged_vlans; only native VLAN will be carried.") builder.build_bgp(sw_type) builder.build_prefix_lists() builder.build_qos() @@ -546,7 +621,20 @@ def convert_switch_input_json(input_data: dict, output_dir: str = DEFAULT_OUTPUT "port_channels": builder.sections["port_channels"], "bgp": builder.sections["bgp"], "prefix_lists": builder.sections["prefix_lists"], - "qos": builder.sections["qos"] + "qos": builder.sections["qos"], + "debug": { + "vlan_map": dict(builder.vlan_map), + "ip_map": dict(builder.ip_map), + "resolved_trunks": [ + { + "name": iface.get("name", ""), + "native_vlan": iface.get("native_vlan", ""), + "tagged_vlans": iface.get("tagged_vlans", "") + } + for iface in builder.sections.get("interfaces", []) + if iface.get("type") == "Trunk" + ] + } } out_file = out_path / f"{hostname}{OUTPUT_FILE_EXTENSION}" diff --git a/src/generator.py b/src/generator.py index 948beac..0bb44b5 100644 --- a/src/generator.py +++ b/src/generator.py @@ -36,34 +36,31 @@ def generate_config(input_std_json, template_folder, output_folder): if not template_dir.exists(): raise FileNotFoundError(f"[ERROR] Template path not found: {template_dir}") - template_files = list(template_dir.glob("*.j2")) + template_files = sorted(template_dir.glob("*.j2")) if not template_files: raise FileNotFoundError(f"[WARN] No templates found in: {template_dir}") - print(f"[INFO] Found {len(template_files)} templates to render") + print(f"[INFO] Rendering {len(template_files)} templates (compact mode)") - # ✅ Step 4: Render each template + # ✅ Step 4: Render each template (compact single-line logging) os.makedirs(output_folder_path, exist_ok=True) for template_path in template_files: template_name = template_path.name - print(f"\n[-] Rendering template: {template_name}") - + stem = template_path.stem try: template = load_template(str(template_dir), template_name) rendered = template.render(data) - + if not rendered.strip(): - print(f"[SKIP] Template {template_name} produced empty output — skipping file") + print(f"[–] {template_name}: skipped (empty)") continue - output_file = output_folder_path / f"generated_{template_path.stem}.cfg" + output_file = output_folder_path / f"generated_{stem}.cfg" with open(output_file, "w", encoding="utf-8") as f: f.write(rendered) - - print(f"[✓] Generated: {output_file.name}") - + print(f"[✓] {template_name} -> {output_file.name}") except Exception as e: - warnings.warn(f"[WARN] Failed to render {template_name}: {e}", UserWarning) + warnings.warn(f"[!] {template_name} failed: {e}", UserWarning) print(f"\n=== Done generating for: {input_std_json_path.name} ===\n") diff --git a/src/main.py b/src/main.py index b531c80..8022def 100644 --- a/src/main.py +++ b/src/main.py @@ -189,19 +189,33 @@ def main(): # Create temporary subdirectory for conversion within output folder temp_conversion_subdir = output_folder_path / ".temp_conversion" temp_conversion_subdir.mkdir(parents=True, exist_ok=True) - + # Convert to standard format using temporary subdirectory standard_format_files = convert_to_standard_format( - input_json_path, + input_json_path, str(temp_conversion_subdir), args.convertor ) except Exception as e: - safe_print(f"❌ Failed to convert to standard format: {e}") - safe_print(f"\n💡 Troubleshooting tips:") - print(f" - Ensure your input file is in the correct format for convertor: {args.convertor}") - print(f" - Check if the convertor module exists and has 'convert_switch_input_json' function") - print(f" - For custom convertors, use: --convertor your.custom.convertor.module") + err_msg = str(e) + safe_print(f"❌ Failed to convert to standard format: {err_msg}") + + # Specialized guidance for missing VLAN symbol sets + if "Required VLAN set(s) missing" in err_msg: + safe_print("\n➡ Action Required:") + safe_print(" 1. Open the input JSON (the --input_json file).") + safe_print(" 2. Under 'Supernets', add entries so the following symbolic VLAN sets exist:") + safe_print(" - Infrastructure (M): GroupName starting 'Infrastructure' or similar.") + safe_print(" - Tenant/Compute (C): GroupName starting 'Tenant', 'L3Forward', or 'HNVPA'.") + safe_print(" - (Optional) Storage (S): GroupName starting 'Storage' for storage VLAN placeholders.") + safe_print(" 3. Re-run the command once these are defined.") + safe_print(" 4. If you cannot update the file, file a GitHub issue referencing this error message.") + else: + safe_print("\n💡 Basic Checks:") + safe_print(f" - Confirm the input JSON matches the expected lab schema for convertor '{args.convertor}'.") + safe_print(" - Verify 'Supernets' contains all required VLAN groups.") + safe_print(" - If still failing, file an issue with the error string above.") + sys.exit(1) # === Step 2: Generate configs for each standard format file === From 3a37575c93a67a7c31d89c887f944063656d28d3 Mon Sep 17 00:00:00 2001 From: liunick Date: Tue, 2 Dec 2025 21:39:59 -0800 Subject: [PATCH 4/6] Dell S5248F-ON Interface Template Updates --- .../dellemc/os10/interface.j2 | 5 ++ .../dellemc/os10/port_channel.j2 | 54 +++++++++++------- .../dellemc/s5248f-on.json | 20 +++++-- src/convertors/convertors_lab_switch_json.py | 55 +++++++++++++++---- src/main.py | 12 +++- 5 files changed, 108 insertions(+), 38 deletions(-) diff --git a/input/jinja2_templates/dellemc/os10/interface.j2 b/input/jinja2_templates/dellemc/os10/interface.j2 index c53cb3a..d7bda25 100644 --- a/input/jinja2_templates/dellemc/os10/interface.j2 +++ b/input/jinja2_templates/dellemc/os10/interface.j2 @@ -6,6 +6,11 @@ {%- set intf_range = iface.intf if iface.intf is defined else iface.start_intf ~ (('-' ~ iface.end_intf) if iface.end_intf is defined and iface.end_intf != iface.start_intf else '') %} {%- set intf_name = iface.intf_type ~ ' ' ~ intf_range %} {%- set is_range = '-' in intf_range | string %} +{%- if iface.shutdown is defined and iface.shutdown %} +! NOTE: This interface configuration initializes ports in shutdown state for security. +! This is a recommended best practice to prevent unauthorized access on unused ports. +! User may choose to apply this initial configuration or customize based on deployment requirements. +{%- endif %} interface {% if is_range %}range {% endif %}{{ intf_name }} description {{ iface.name }} switchport mode access diff --git a/input/jinja2_templates/dellemc/os10/port_channel.j2 b/input/jinja2_templates/dellemc/os10/port_channel.j2 index 5385563..21f3fc3 100644 --- a/input/jinja2_templates/dellemc/os10/port_channel.j2 +++ b/input/jinja2_templates/dellemc/os10/port_channel.j2 @@ -7,15 +7,18 @@ interface port-channel{{ pc.id }} description {{ pc.description }} {%- if pc.type | lower == 'trunk' %} - switchport + no shutdown switchport mode trunk - switchport trunk native vlan {{ pc.native_vlan }} + switchport access vlan {{ pc.native_vlan }} {%- if pc.tagged_vlans is defined %} switchport trunk allowed vlan {{ pc.tagged_vlans }} {%- endif %} + {%- if pc.vlt_port is defined and pc.vlt_port %} + vlt-port-channel {{ pc.id }} + {%- elif 'peer' in (pc.description | lower) %} + vlt-port-channel {{ pc.id }} + {%- endif %} priority-flow-control mode on - spanning-tree port type network - logging event port link-status {%- if pc.vpc_peer_link is defined and pc.vpc_peer_link %} vpc peer-link @@ -25,29 +28,43 @@ interface port-channel{{ pc.id }} {%- endif %} {%- elif pc.type | lower == 'l3' %} + no shutdown no switchport - priority-flow-control mode on - ip address {{ pc.ipv4 }} + mtu 9216 + ip address {{ pc.ipv4 }}/30 {%- endif %} + {%- if pc.type | lower == 'trunk' %} mtu 9216 - no shutdown + {%- endif %} {# -------- Member Interfaces -------- #} {%- for member in pc.members %} interface Ethernet {{ member }} description {{ pc.description }} + no shutdown + channel-group {{ pc.id }} mode active + + {%- if pc.type | lower == 'l3' %} + no switchport mtu 9216 flowcontrol receive off - priority-flow-control mode on - - {%- if pc.type | lower == 'trunk' %} - switchport - switchport mode trunk - switchport trunk native vlan {{ pc.native_vlan }} - {%- if pc.tagged_vlans is defined %} - switchport trunk allowed vlan {{ pc.tagged_vlans }} - {%- endif %} - spanning-tree port type network + {%- elif pc.type | lower == 'trunk' %} + {# For VLT/BMC peer-link members (po102), render as no switchport per reference #} + {%- if pc.id == 102 or ('tor_bmc' in (pc.description | lower)) %} + no switchport + mtu 9216 + flowcontrol receive off + {%- else %} + switchport + switchport mode trunk + switchport trunk native vlan {{ pc.native_vlan }} + {%- if pc.tagged_vlans is defined %} + switchport trunk allowed vlan {{ pc.tagged_vlans }} + {%- endif %} + spanning-tree port type network + mtu 9216 + flowcontrol receive off + {%- endif %} {%- endif %} {#- -------- QoS Policy -------- #} @@ -57,9 +74,6 @@ interface Ethernet {{ member }} qos-map traffic-class AZS_SERVICES_TrafficClass ets mode on {%- endif %} - - channel-group {{ pc.id }} mode active - no shutdown {% endfor %} {%- endfor %} diff --git a/input/switch_interface_templates/dellemc/s5248f-on.json b/input/switch_interface_templates/dellemc/s5248f-on.json index 12d7f2f..ebbe3a4 100644 --- a/input/switch_interface_templates/dellemc/s5248f-on.json +++ b/input/switch_interface_templates/dellemc/s5248f-on.json @@ -32,14 +32,14 @@ "name": "P2P_Border1", "type": "L3", "intf_type": "Ethernet", - "intf": "1/1/48", + "intf": "1/1/48:1", "ipv4": "" }, { "name": "P2P_Border2", "type": "L3", "intf_type": "Ethernet", - "intf": "1/1/47", + "intf": "1/1/47:1", "ipv4": "" }, { @@ -51,7 +51,7 @@ "tagged_vlans": "125" } ], - "fully_converged": [ + "fully_converged1": [ { "name": "HyperConverged_To_Host", "type": "Trunk", @@ -65,6 +65,16 @@ } } ], + "fully_converged2": [ + { + "name": "HyperConverged_For_vENV", + "type": "Access", + "intf_type": "Ethernet", + "start_intf": "1/1/1", + "end_intf": "1/1/40", + "access_vlan": "M" + } + ], "switched": [ { "name": "Switched_Compute_To_Host", @@ -106,7 +116,7 @@ "description": "P2P_IBGP", "type": "L3", "ipv4": "", - "members": ["1/1/45", "1/1/46"] + "members": ["1/1/45:1", "1/1/46:1"] }, { "id": 102, @@ -114,7 +124,7 @@ "type": "Trunk", "native_vlan": "99", "tagged_vlans": "", - "members": ["1/1/49", "1/1/50", "1/1/51", "1/1/52"] + "members": ["1/1/44:1"] } ] } \ No newline at end of file diff --git a/src/convertors/convertors_lab_switch_json.py b/src/convertors/convertors_lab_switch_json.py index 575cd50..3ce1df7 100644 --- a/src/convertors/convertors_lab_switch_json.py +++ b/src/convertors/convertors_lab_switch_json.py @@ -81,7 +81,10 @@ def __init__(self, input_data: dict): self.deployment_pattern = input_data.get("InputData", {}).get("DeploymentPattern", "").lower() self.site = input_data.get("InputData", {}).get("MainEnvData", [{}])[0].get("Site", "") - # Translate hyperconverged to fully_converged for template compatibility + # Store original pattern for VLAN-based logic + self.original_pattern = self.deployment_pattern + + # Initial translation for template compatibility if self.deployment_pattern == "hyperconverged": self.deployment_pattern = "fully_converged" @@ -245,19 +248,32 @@ def build_interfaces(self, switch_type: str): def _build_interfaces_from_template(self, switch_type: str, template_data: dict): """ Build interfaces from template data, processing both Common and deployment pattern specific interfaces. + Intelligently selects template variant based on available VLAN sets. """ templates = template_data.get("interface_templates", {}) common_templates = templates.get("common", []) - pattern_templates = templates.get(self.deployment_pattern, []) + + # Smart template selection for HyperConverged deployments + effective_pattern = self.deployment_pattern + if self.original_pattern == "hyperconverged" and self.deployment_pattern == "fully_converged": + # Check which VLAN sets are available + has_m = bool(self.vlan_map.get("M", [])) + has_c = bool(self.vlan_map.get("C", [])) + has_s = bool(self.vlan_map.get("S", [])) or bool(self.vlan_map.get("S1", [])) or bool(self.vlan_map.get("S2", [])) + + # If only M exists (no C or S), use fully_converged2 (Access mode) + if has_m and not has_c and not has_s: + effective_pattern = "fully_converged2" + print(f"[INFO] Using fully_converged2 template (Access mode) - only Infrastructure VLANs detected") + # Otherwise use fully_converged (Trunk mode with M,C,S) + else: + effective_pattern = "fully_converged1" + + pattern_templates = templates.get(effective_pattern, []) # Build IP mapping for BGP and L3 interfaces self._build_ip_mapping() - # # Debug output - # print(f"VLAN Map: {dict(self.vlan_map)}") - # print(f"IP Map: {dict(self.ip_map)}") - # print(f"Deployment Pattern: {self.deployment_pattern}") - interfaces = [] # Process Common interfaces @@ -326,6 +342,11 @@ def _process_interface_template(self , switch_type: str, template: dict) -> dict """ interface = deepcopy(template) + # Handle VLAN reference resolution for access interfaces + if interface.get("type") == "Access": + if "access_vlan" in interface: + interface["access_vlan"] = self._resolve_interface_vlans(switch_type, interface["access_vlan"]) + # Handle VLAN reference resolution for trunk interfaces if interface.get("type") == "Trunk": if "native_vlan" in interface: @@ -562,12 +583,26 @@ def convert_switch_input_json(input_data: dict, output_dir: str = DEFAULT_OUTPUT s2_vlans = builder.vlan_map.get("S2", []) print(f"[DEBUG] VLAN sets for {sw_type}: M={m_vlans} C={c_vlans} S={s_vlans} S1={s1_vlans} S2={s2_vlans}") - # Validation: M and C must not be empty; S optional (warn if empty) + # Validation: Check VLAN requirements based on deployment pattern missing_critical = [] if not m_vlans: missing_critical.append("M") - if not c_vlans: - missing_critical.append("C") + + # For HyperConverged: allow M-only (uses fully_converged2/Access mode) or M+C+S (uses fully_converged1/Trunk mode) + is_hyperconverged = builder.original_pattern == "hyperconverged" + has_c = bool(c_vlans) + has_s = bool(s_vlans) or bool(s1_vlans) or bool(s2_vlans) + + if is_hyperconverged: + # HyperConverged: M-only is valid (Access mode), or require full M+C for Trunk mode + if m_vlans and not has_c and not has_s: + print(f"[INFO] HyperConverged deployment with M-only: using Access mode (fully_converged2)") + elif not has_c: + missing_critical.append("C") + else: + # Non-HyperConverged: always require C + if not has_c: + missing_critical.append("C") if missing_critical: raise ValueError( diff --git a/src/main.py b/src/main.py index 8022def..0c62ee1 100644 --- a/src/main.py +++ b/src/main.py @@ -134,8 +134,8 @@ def main(): parser.add_argument("--template_folder", default="input/jinja2_templates", help="Folder containing Jinja2 templates (default: input/jinja2_templates)") - parser.add_argument("--output_folder", default=".", - help="Directory to save generated configs (default: current directory)" + parser.add_argument("--output_folder", default=None, + help="Directory to save generated configs (default: same directory as input file)" ) parser.add_argument("--convertor", default="convertors.convertors_lab_switch_json", @@ -145,7 +145,13 @@ def main(): # Resolve paths input_json_path = Path(args.input_json).resolve() - output_folder_path = Path(args.output_folder).resolve() + + # Auto-detect output folder: default to same directory as input file + if args.output_folder is None: + output_folder_path = input_json_path.parent + else: + output_folder_path = Path(args.output_folder).resolve() + template_folder_arg = Path(args.template_folder) # Only use get_real_path if user did NOT override default From f66079f5f8e1b790d45acb85373a4d7f1417ffb5 Mon Sep 17 00:00:00 2001 From: liunick Date: Wed, 3 Dec 2025 12:54:17 -0800 Subject: [PATCH 5/6] Dell S5248F-ON BGP Template update --- input/jinja2_templates/dellemc/os10/bgp.j2 | 4 +- src/convertors/convertors_lab_switch_json.py | 65 +++++++++++++++++--- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/input/jinja2_templates/dellemc/os10/bgp.j2 b/input/jinja2_templates/dellemc/os10/bgp.j2 index dcc8b9b..245fc2f 100644 --- a/input/jinja2_templates/dellemc/os10/bgp.j2 +++ b/input/jinja2_templates/dellemc/os10/bgp.j2 @@ -5,8 +5,8 @@ router bgp {{ bgp.asn }} router-id {{ bgp.router_id }} bestpath as-path multipath-relax log-neighbor-changes - maximum-paths 8 maximum-paths ibgp 8 + maximum-paths ebgp 8 address-family ipv4 unicast {%- for network in bgp.networks %} network {{ network }} @@ -37,7 +37,6 @@ router bgp {{ bgp.asn }} {%- endif %} address-family ipv4 unicast activate - sender-side-loop-detection {%- if neighbor.af_ipv4_unicast is defined %} {%- if neighbor.af_ipv4_unicast.prefix_list_in is defined %} prefix-list {{ neighbor.af_ipv4_unicast.prefix_list_in }} in @@ -46,6 +45,7 @@ router bgp {{ bgp.asn }} prefix-list {{ neighbor.af_ipv4_unicast.prefix_list_out }} out {%- endif %} {%- endif %} + next-hop-self {%- endif %} {%- endfor %} diff --git a/src/convertors/convertors_lab_switch_json.py b/src/convertors/convertors_lab_switch_json.py index 3ce1df7..7026eba 100644 --- a/src/convertors/convertors_lab_switch_json.py +++ b/src/convertors/convertors_lab_switch_json.py @@ -449,27 +449,74 @@ def build_bgp(self, switch_type: str): """ Build BGP config with neighbor and network structure. IPs are abstracted with placeholders for future parsing. + + Network advertisement policy: + - Advertise P2P subnets (Border1/Border2) using subnet prefixes, not host IPs + - Advertise iBGP P2P subnet from the port-channel peer link (/30 derived from peer IP) + - Advertise VLAN interface subnets (e.g., BMC Mgmt, Infrastructure) by computing network from interface IP/CIDR + - Avoid duplicates and ignore blank entries + These rules ensure all necessary routes are announced upstream without missing required subnets. """ + import ipaddress switch = self.sections["switch"].get(switch_type) if not switch: print(f"[!] No switch info for BGP") return - # Build networks list by flattening all network entries - networks = [ - self.ip_map.get(f"P2P_BORDER1_{switch_type.upper()}", [""])[0], - self.ip_map.get(f"P2P_BORDER2_{switch_type.upper()}", [""])[0], - self.ip_map.get(f"LOOPBACK0_{switch_type.upper()}", [""])[0], - ] - # Extend with all tenant/compute networks from "C" key + # Build networks list using subnet prefixes + networks: list[str] = [] + + # 1) P2P subnets to Border routers (stored as subnet strings in ip_map) + b1_key = f"P2P_BORDER1_{switch_type.upper()}" + b2_key = f"P2P_BORDER2_{switch_type.upper()}" + b1_subnet = self.ip_map.get(b1_key, [""])[0] + b2_subnet = self.ip_map.get(b2_key, [""])[0] + if b1_subnet: + networks.append(b1_subnet) + if b2_subnet: + networks.append(b2_subnet) + + # 2) Loopback0 host route (always advertise as /32) + loopback = self.ip_map.get(f"LOOPBACK0_{switch_type.upper()}", [""])[0] + if loopback: + networks.append(loopback) + + # 3) iBGP P2P subnet: derive /30 network from peer IP + ibgp_peer_ip = "" + if switch_type == TOR1: + ibgp_peer_ip = self.ip_map.get("P2P_IBGP_TOR2", [""])[0] + elif switch_type == TOR2: + ibgp_peer_ip = self.ip_map.get("P2P_IBGP_TOR1", [""])[0] + if ibgp_peer_ip: + try: + ibgp_net = ipaddress.ip_network(f"{ibgp_peer_ip}/30", strict=False) + networks.append(ibgp_net.with_prefixlen) + except Exception: + pass + + # 4) VLAN interface subnets: compute from SVI IP/CIDR (e.g., BMC Mgmt, Infra) + for vlan in self.sections.get("vlans", []): + iface = vlan.get("interface") + if iface and iface.get("ip") and iface.get("cidr"): + try: + svi_net = ipaddress.ip_network(f"{iface['ip']}/{iface['cidr']}", strict=False) + networks.append(svi_net.with_prefixlen) + except Exception: + continue + + # 5) Include any additional compute/tenant networks from ip_map["C"] networks.extend(self.ip_map.get("C", [])) + # De-duplicate while preserving order + seen = set() + networks = [n for n in networks if n and (n not in seen and not seen.add(n))] + # iBGP Peer IPs ibgp_ip = "" if switch_type == TOR1: - ibgp_ip = self.ip_map.get(f"P2P_IBGP_TOR2", [""])[0] + ibgp_ip = self.ip_map.get("P2P_IBGP_TOR2", [""])[0] elif switch_type == TOR2: - ibgp_ip = self.ip_map.get(f"P2P_IBGP_TOR1", [""])[0] + ibgp_ip = self.ip_map.get("P2P_IBGP_TOR1", [""])[0] neighbors = [ { From d609f69d6a7fdaa01b8ec36ce0ab399b915d31ac Mon Sep 17 00:00:00 2001 From: liunick Date: Tue, 9 Dec 2025 10:09:28 -0800 Subject: [PATCH 6/6] update codeql action version --- .github/workflows/codeql-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4ab36c0..25ed646 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file.