Skip to content
26 changes: 24 additions & 2 deletions .github/workflows/build-switchgentool.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
echo "jinja2>=3.0.0" > requirements.txt
fi
pip install -r requirements.txt
pip install "pyinstaller==6.10.*"
pip install "pyinstaller==6.11.*"

- name: Quick source test (run as script)
shell: bash
Expand All @@ -75,14 +75,18 @@ jobs:
if: matrix.os == 'windows-latest'
shell: powershell
run: |
pyinstaller --onefile --clean --noconfirm `
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 `
--paths=src `
src/main.py
if (!(Test-Path "dist\\${{ matrix.exe_name }}")) { Write-Error "Build failed"; exit 1 }
Expand All @@ -93,12 +97,16 @@ 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 \
--paths=src \
src/main.py
test -f "dist/${{ matrix.exe_name }}" || (echo "Build failed" && exit 1)
Expand All @@ -109,6 +117,20 @@ jobs:
run: |
chmod +x "dist/${{ matrix.exe_name }}" || true
"dist/${{ matrix.exe_name }}" --help || (echo "Executable failed to run" && exit 1)

- name: Test BMC converter module inclusion
shell: bash
run: |
python -c "
import sys
sys.path.insert(0, 'src')
try:
from convertors.convertors_bmc_switch_json import convert_bmc_switches
print('[OK] BMC converter module can be imported')
except ImportError as e:
print('[ERROR] BMC converter module import failed:', e)
sys.exit(1)
"

- name: Stage artifact
shell: bash
Expand Down
13 changes: 13 additions & 0 deletions hook-convertors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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}")
18 changes: 12 additions & 6 deletions src/convertors/convertors_bmc_switch_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,25 +234,31 @@ def _build_interfaces(self, switch_data: Dict) -> List[Dict]:
model = switch_data.get("Model", "").upper()

# Load interface template for BMC switch
template_relative = Path("input/switch_interface_templates") / make / f"{model}.json"
template_relative = Path("input") / "switch_interface_templates" / make / f"{model}.json"

# Try multiple path resolution strategies
import sys
if getattr(sys, 'frozen', False):
# Running as PyInstaller bundle - use _MEIPASS
template_path = get_real_path(template_relative)
base_path = Path(sys._MEIPASS)
template_path = base_path / template_relative
else:
# Running as script - try both current directory and parent directory
template_path = get_real_path(template_relative)
template_path = template_relative.resolve()
if not template_path.exists():
# Try from parent directory (in case we're running from src/)
template_path = get_real_path(Path("..") / template_relative)
template_path = (Path("..") / template_relative).resolve()

if not template_path.exists():
import sys
debug_info = f"sys.frozen={getattr(sys, 'frozen', False)}, sys._MEIPASS={getattr(sys, '_MEIPASS', 'N/A')}"
raise FileNotFoundError(
f"[!] BMC interface template not found: {template_path}\n"
f"[!] BMC interface template not found!\n"
f" Looking for model: {model} (make: {make})\n"
f" Searched: {template_relative}"
f" Relative path: {template_relative}\n"
f" Resolved path: {template_path}\n"
f" File exists: {template_path.exists()}\n"
f" Debug: {debug_info}"
)

try:
Expand Down
41 changes: 25 additions & 16 deletions src/convertors/convertors_lab_switch_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@
from copy import deepcopy
from pathlib import Path
from collections import defaultdict

# IMPORTANT: Unconditional import for PyInstaller detection
# PyInstaller's static analysis needs to see this import at module level without any conditionals
from . import convertors_bmc_switch_json

try:
from ..loader import get_real_path # package style
except ImportError:
from loader import get_real_path # fallback script style

# Import BMC converter function with fallback handling
try:
from .convertors_bmc_switch_json import convert_bmc_switches
_bmc_available = True
except ImportError:
convert_bmc_switches = None
_bmc_available = False

# ── Static config ─────────────────────────────────────────────────────────
SWITCH_TYPES = ["TOR1", "TOR2"]
TOR1, TOR2 = "TOR1", "TOR2"
Expand Down Expand Up @@ -542,14 +555,16 @@ def convert_switch_input_json(input_data: dict, output_dir: str = DEFAULT_OUTPUT

print(f"[✓] Wrote {out_file}")

# Convert BMC switches
try:
from .convertors_bmc_switch_json import convert_bmc_switches
convert_bmc_switches(input_data, output_dir)
except ImportError as e:
print(f"[!] BMC converter not available: {e}")
except Exception as e:
print(f"[!] Error converting BMC switches: {e}")
# Convert BMC switches using the module-level import
if _bmc_available and convert_bmc_switches:
try:
convert_bmc_switches(input_data, output_dir)
except Exception as e:
print(f"[!] Error converting BMC switches: {e}")
import traceback
traceback.print_exc()
else:
print(f"[!] BMC converter not available - module not imported")


def convert_all_switches_json(input_data: dict, output_dir: str = DEFAULT_OUTPUT_DIR):
Expand All @@ -560,13 +575,7 @@ def convert_all_switches_json(input_data: dict, output_dir: str = DEFAULT_OUTPUT
print("[*] Converting ToR switches...")
convert_switch_input_json(input_data, output_dir)

# Import and call BMC converter (separate module for clean separation)
try:
from .convertors_bmc_switch_json import convert_bmc_switches
print("[*] Converting BMC switches...")
convert_bmc_switches(input_data, output_dir)
except ImportError as e:
print(f"[!] BMC converter not available: {e}")

# BMC converter already called within convert_switch_input_json
# No need to call again here to avoid duplicate conversion
print("[✓] All switch conversions completed.")

73 changes: 49 additions & 24 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@
from generator import generate_config # type: ignore
from loader import get_real_path, load_input_json # type: ignore # Only used for PyInstaller-packed assets

# Configure UTF-8 encoding for Windows console (fixes emoji display issues in executables)
if sys.platform == "win32":
try:
# Try to set console to UTF-8 mode
import os
os.system("chcp 65001 > nul 2>&1")
# Reconfigure stdout/stderr with UTF-8 encoding
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
except:
pass # If it fails, we'll use safe_print fallback

def safe_print(text):
"""
Safely print text, handling Unicode characters that might not be supported in console.
Falls back to ASCII-safe alternatives if encoding fails.
"""
try:
print(text)
except UnicodeEncodeError:
# Remove or replace problematic Unicode characters
safe_text = text.encode('ascii', errors='replace').decode('ascii')
print(safe_text)

def load_convertor(convertor_module_path):
"""
Dynamically load a convertor module and return its convert function.
Expand Down Expand Up @@ -65,8 +90,8 @@ def convert_to_standard_format(input_file_path, output_dir, convertor_module_pat
Convert lab format to standard format JSON files using specified convertor.
Returns list of generated standard format files.
"""
print("🔄 Converting from lab format to standard format...")
print(f"📦 Using convertor: {convertor_module_path}")
safe_print("🔄 Converting from lab format to standard format...")
safe_print(f"📦 Using convertor: {convertor_module_path}")

# Load lab format data
data = load_input_json(str(input_file_path))
Expand All @@ -86,7 +111,7 @@ def convert_to_standard_format(input_file_path, output_dir, convertor_module_pat
if not generated_files:
raise RuntimeError("No standard format files were generated during conversion")

print(f"✅ Generated {len(generated_files)} standard format files:")
safe_print(f"✅ Generated {len(generated_files)} standard format files:")
for file in generated_files:
print(f" - {file}")

Expand Down Expand Up @@ -124,11 +149,11 @@ def main():
else:
template_folder = template_folder_arg.resolve()

print(f"🧾 Input JSON File: {input_json_path}")
print(f"🧩 Template Folder: {template_folder}")
print(f"📁 Output Directory: {output_folder_path}")
safe_print(f"🧾 Input JSON File: {input_json_path}")
safe_print(f"🧩 Template Folder: {template_folder}")
safe_print(f"📁 Output Directory: {output_folder_path}")
if args.convertor != parser.get_default('convertor'):
print(f"🔄 Custom Convertor: {args.convertor}")
safe_print(f"🔄 Custom Convertor: {args.convertor}")

# === Validation ===
if not input_json_path.exists():
Expand All @@ -142,7 +167,7 @@ def main():
output_folder_path.mkdir(parents=True, exist_ok=True)

# === Step 1: Check if input is in standard format ===
print("🔍 Checking input format...")
safe_print("🔍 Checking input format...")
data = load_input_json(str(input_json_path))
if data is None:
print(f"[ERROR] Failed to load input JSON: {input_json_path}")
Expand All @@ -151,10 +176,10 @@ def main():
standard_format_files = []

if is_standard_format(data):
print("✅ Input is already in standard format")
safe_print("✅ Input is already in standard format")
standard_format_files = [input_json_path]
else:
print("⚠️ Input is in lab format - conversion required")
safe_print("⚠️ Input is in lab format - conversion required")
try:
# Create temporary subdirectory for conversion within output folder
temp_conversion_subdir = output_folder_path / ".temp_conversion"
Expand All @@ -167,22 +192,22 @@ def main():
args.convertor
)
except Exception as e:
print(f"❌ Failed to convert to standard format: {e}")
print(f"\n💡 Troubleshooting tips:")
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")
sys.exit(1)

# === Step 2: Generate configs for each standard format file ===
print(f"\n🏗️ Generating configs for {len(standard_format_files)} switch(es)...")
safe_print(f"\n🏗️ Generating configs for {len(standard_format_files)} switch(es)...")

total_success = 0
total_failed = 0
conversion_used = not is_standard_format(data)

for std_file in standard_format_files:
print(f"\n📝 Processing: {std_file.name}")
safe_print(f"\n📝 Processing: {std_file.name}")

try:
# Create subdirectory for each switch's output
Expand All @@ -194,42 +219,42 @@ def main():
import shutil
std_json_copy = switch_output_dir / f"std_{std_file.name}"
shutil.copy2(std_file, std_json_copy)
print(f"📄 Standard JSON saved: {std_json_copy.name}")
safe_print(f"📄 Standard JSON saved: {std_json_copy.name}")

generate_config(
input_std_json=str(std_file),
template_folder=str(template_folder),
output_folder=str(switch_output_dir)
)
total_success += 1
print(f"✅ Generated configs for {std_file.name} in {switch_output_dir}")
safe_print(f"✅ Generated configs for {std_file.name} in {switch_output_dir}")

except Exception as e:
print(f"❌ Failed to generate configs for {std_file.name}: {e}")
safe_print(f"❌ Failed to generate configs for {std_file.name}: {e}")
total_failed += 1

# === Cleanup conversion artifacts ===
if conversion_used:
# Clean up temporary conversion subdirectory
temp_conversion_subdir = output_folder_path / ".temp_conversion"
if temp_conversion_subdir.exists():
print(f"\n🧹 Cleaning up temporary conversion directory...")
safe_print(f"\n🧹 Cleaning up temporary conversion directory...")
shutil.rmtree(temp_conversion_subdir, ignore_errors=True)

# Keep the original converted JSON files in the root directory for user verification
print("� Original converted JSON files kept in output directory for verification")
safe_print("📋 Original converted JSON files kept in output directory for verification")

# === Summary ===
print(f"\n🎯 Summary:")
print(f" ✅ Successfully processed: {total_success} switch(es)")
safe_print(f"\n🎯 Summary:")
safe_print(f" ✅ Successfully processed: {total_success} switch(es)")
if total_failed > 0:
print(f" ❌ Failed to process: {total_failed} switch(es)")
print(f" 📁 Output directory: {output_folder_path}")
safe_print(f" ❌ Failed to process: {total_failed} switch(es)")
safe_print(f" 📁 Output directory: {output_folder_path}")

if total_failed > 0:
sys.exit(1)
else:
print("🎉 All configs generated successfully!")
safe_print("🎉 All configs generated successfully!")

if __name__ == "__main__":
main()