Skip to content

Access Switch Audit

Deep Dive: Access Switch Port Audit Tool

"Enterprise Port Intelligence, Distilled to Pure Python."

A modular Python utility that connects to Cisco switches (optionally through an SSH jump host), collects comprehensive interface details, PoE information, and neighbor presence, then exports a professional, filters-only Excel workbook with a SUMMARY sheet and one sheet per device. Built for production reliability with YAML-based configuration, intelligent fallback parsing, and customizable credential management.

View Source Code on GitHub


What's New in Version 2.0

The Access Switch Audit tool has been restructured into a professional Python package!

✨ Key Improvements:

  • Modular Architecture: Code separated into focused modules (cli.py, device_auditor.py, excel_reporter.py, etc.)
  • Professional Package Structure: Follows enterprise Python best practices with proper package organization
  • Better Maintainability: Each module has a single, well-defined responsibility
  • Enhanced Extensibility: Easy to add new features, output formats, or device types
  • Improved Testing: Isolated components can be unit tested independently
  • New Entry Point: Use python -m switch_audit instead of python main.py

For End Users: Everything works the same! All CLI arguments, configuration options, and output formats are identical.

For Developers: See the Migration Guide for updated import paths and structure details.

πŸ“– Full details in MIGRATION.md


✨ What This Tool Does

  • Audits access ports across multiple Cisco switches in parallel
  • Normalises interface names (e.g., GigabitEthernet1/0/1 β†’ Gi1/0/1) for cross-command matching
  • Enriches interfaces with:
  • PoE draw and state (show power inline)
  • LLDP/CDP neighbour presence (show lldp neighbors detail, show cdp neighbors detail)
  • Classifies port mode & VLAN using show interfaces status (access / trunk / routed)
  • Flags stale access ports using conservative rules
  • Exports Excel with an at-a-glance SUMMARY and one sheet per device, with filters, frozen header, column auto-size, and conditional formatting
  • Shows a progress bar whilst running concurrent device jobs

Design note: The workbook intentionally uses filters only (no Excel tables), and places SUMMARY first.


🎯 The Nautomation Prime Philosophy in Action

Before diving into the code, understand how every design decision reflects our three core principles:

Principle 1: Line-by-Line Transparency

Every function in this tool includes explicit documentation of what it does and why it's structured this way. You'll see comments explaining the engineering tradeoffsβ€”why we parse with TextFSM and maintain a fallback parser, why we use conservative stale-detection logic, and why conditional formatting in Excel matters for operations teams.

Principle 2: Hardened for Production

Access layer audits run on infrastructure that cannot afford downtime. You'll notice patterns like concurrent connection pooling, per-device failure isolation, graceful fallbacks when commands fail, and secure credential rotation. These aren't "nice to have"β€”they're mandatory for enterprise reliability.

Principle 3: Vendor-Neutral

This tool is built on industry-standard Python libraries: Netmiko (multi-device SSH), Paramiko (jump host tunnelling), Pandas & OpenPyXL (Excel generation), and TextFSM (intelligent parsing). Your skills remain portable across vendors.


🧱 Project Layout

The tool has been restructured into a professional modular package (v2.0), separating concerns and following enterprise Python best practices:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
```text
.
β”œβ”€β”€ switch_audit/           # Main package (modular design)
β”‚   β”œβ”€β”€ __init__.py
β”‚   β”œβ”€β”€ __main__.py        # Entry point for python -m switch_audit
β”‚   β”œβ”€β”€ cli.py             # Command-line interface and orchestration
β”‚   β”œβ”€β”€ device_auditor.py  # Device connection and data collection
β”‚   β”œβ”€β”€ excel_reporter.py  # Excel workbook generation
β”‚   β”œβ”€β”€ credentials.py     # Secure credential management
β”‚   β”œβ”€β”€ jump_manager.py    # SSH jump host (bastion) support
β”‚   β”œβ”€β”€ netmiko_utils.py   # Network device connection utilities
β”‚   β”œβ”€β”€ formatters.py      # Excel formatting and interface name normalization
β”‚   β”œβ”€β”€ validators.py      # Input validation functions
β”‚   └── app_config.py      # Configuration access wrapper
β”œβ”€β”€ ProgramFiles/
β”‚   └── config_files/
β”‚       β”œβ”€β”€ __init__.py
β”‚       └── config_loader.py  # YAML configuration loader
β”œβ”€β”€ config.yaml            # User-editable configuration
β”œβ”€β”€ devices.txt            # Device list (one IP/hostname per line)
β”œβ”€β”€ main_new.py            # Backward compatibility entry point
β”œβ”€β”€ main.py                # Legacy entry point (deprecated)
β”œβ”€β”€ run.bat                # Windows launcher script
β”œβ”€β”€ requirements.txt       # Python dependencies
β”œβ”€β”€ MIGRATION.md           # Migration guide for v2.0
└── README.md
```

V2.0 Architecture: The restructure separates the monolithic main.py (1,300+ lines) into focused modules with single responsibilities. This improves maintainability, testability, and extensibility.

Backward Compatibility: The old main.py remains for reference, and main_new.py provides a compatibility shim. New users should use python -m switch_audit.

Module Responsibilities (v2.0)

Each module in the switch_audit/ package has a specific, well-defined role:

Module Purpose Key Functions
__main__.py Package entry point Enable python -m switch_audit execution
cli.py Command-line interface Argument parsing, orchestration, progress tracking
device_auditor.py Device connection & data collection SSH connections, command execution, retry logic, data enrichment
excel_reporter.py Excel report generation Workbook creation, sheet formatting, conditional formatting
credentials.py Credential management Windows Credential Manager integration, interactive prompts
jump_manager.py SSH bastion support Persistent jump host connections, channel management
netmiko_utils.py Network device utilities Netmiko connection wrappers, timeout handling
formatters.py Data formatting utilities Interface name normalization, CLI parsing, Excel formatting
validators.py Input validation File existence checks, argument validation
app_config.py Configuration singleton Global config access wrapper

Configuration Package:

Module Purpose
ProgramFiles/config_files/config_loader.py YAML configuration loader with type-safe property accessors and validation

πŸ“¦ Requirements

  • Python: 3.8+
  • Python packages:
  • netmiko
  • paramiko
  • pandas
  • openpyxl
  • pywin32 (Windows only; used for Windows Credential Manager integration)

Install with pip:

pip install netmiko paramiko pandas openpyxl pywin32

  • TextFSM templates (NTC templates) for robust parsing of show interfaces when use_textfsm=True.
  • If templates are available and the NET_TEXTFSM environment variable points to them, parsing accuracy improves.
  • If not available, the script still works and falls back where needed (e.g., it has its own fixed-width parser for show interfaces status).

βš™οΈ Configuration System

The tool uses a modern YAML-based configuration system with centralized management in v2.0.

YAML Configuration File (config.yaml)

All configurable settings are centralized in config.yaml at the project root. The configuration is loaded via ProgramFiles/config_files/config_loader.py and accessed throughout the application via the app_config.py singleton wrapper.

Key Configuration Categories:

1. Network Settings:

1
2
3
4
5
network:
jump_host: "jump-gateway.example.com"  # Default bastion/jump host
device_type: "cisco_ios"                # Netmiko device type
ssh_port: 22                            # SSH port
read_timeout: 30                        # Command read timeout

2. Credential Settings:

1
2
3
credentials:
cred_target: "MyApp/ADM"  # Windows Credential Manager target
enable_target: ""          # Optional enable secret target

3. Performance & Concurrency:

1
2
3
4
concurrency:
default_workers: 10        # Max concurrent device sessions
retry_attempts: 3          # Connection retry count
retry_base_wait: 2         # Base wait time for exponential backoff

4. Excel Output:

1
2
3
4
5
6
output:
default_filename: "audit.xlsx"  # Default output filename

excel_formatting:
min_column_width: 10
max_column_width: 50

5. Stale Port Detection:

stale_detection:
default_stale_days: 30  # Days threshold for stale ports

Why YAML Configuration?

Benefit Explanation
Human-Readable No Python knowledge required to modify settings
Version Control Friendly Plain text format works seamlessly with Git
Safer No code execution risk (pure data)
Validated Config loader validates types and provides defaults
Hierarchical Natural grouping of related settings
Documented Inline comments explain each setting

Environment Variable Overrides

Specific settings can be overridden at runtime via environment variables (primarily for the jump host):

1
2
3
4
```powershell
# Override jump host at runtime
$env:JUMP_HOST = "temp-bastion.example.com"
```

Best Practice: Use config.yaml for organizational defaults; use CLI arguments (--direct, --workers, etc.) for per-run overrides.


The repository includes a professional Windows batch launcher (run.bat) that provides the easiest way to run the tool with default settings.

Why Use the Launcher?

  • Zero configuration required - Just double-click or run from command line
  • Automatic validation - Checks for Python environment and required files before execution
  • Helpful diagnostics - Clear error messages if something is missing
  • Professional interface - Clean output with status indicators and progress messages
  • Safe execution - Validates environment before running the script
  • Updated for v2.0 - Uses the new modular package structure automatically

Using run.bat

Option 1: Double-click

Simply double-click run.bat in Windows Explorer to launch the tool with default behavior.

Option 2: Command Line (Default Behavior)

1
2
3
```cmd
run.bat
```

This runs the Access Switch Audit using python -m switch_audit with all default settings from config.yaml.

What the Launcher Does

  1. Validates the environment:
  2. Checks that the portable_env virtual environment exists
  3. Verifies Python executable is present
  4. Confirms main.py exists
  5. Validates config.yaml and devices.txt are present

  6. Provides clear feedback:

  7. Shows [OK] for successful checks
  8. Shows [WARNING] for missing optional files with option to continue
  9. Shows [ERROR] for critical missing components
  10. Displays helpful troubleshooting tips on failure

  11. Runs the tool:

  12. Activates the virtual environment
  13. Executes the main script
  14. Captures and displays the exit code
  15. Provides common troubleshooting tips if errors occur

Example Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
```
================================================================================
                ACCESS SWITCH AUDIT TOOL
================================================================================

Starting validation checks...

[OK] Python Environment: Found at portable_env\Scripts\python.exe
[OK] Required support files found
[OK] All validation checks passed

================================================================================

Running Access Switch Audit...

================================================================================

[Script output appears here]

================================================================================

[SUCCESS] Script completed successfully

================================================================================
```

πŸš€ Advanced: Command Line with Arguments

For advanced users who need to customize behavior beyond the defaults, you can still run the tool directly with Python and command-line arguments.

When to Use Command Line Arguments

Use python -m switch_audit with arguments when you need to:

  • Override default settings from config.yaml
  • Specify a different devices file
  • Change output filename
  • Adjust worker thread count
  • Enable debug mode
  • Force direct connections (bypass jump host)

Method 1: Using the Launcher with Arguments

You can pass arguments to run.bat and they will be forwarded to the Python script:

1
2
3
```cmd
run.bat --devices my-switches.txt --output custom-audit.xlsx --workers 5
```

Method 2: Direct Python Execution (New in v2.0)

Activate the virtual environment and run the package as a module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
```bash
# Windows (recommended)
portable_env\Scripts\activate
python -m switch_audit --devices my-switches.txt --output audit-report.xlsx

# Linux/macOS  
source portable_env/bin/activate
python -m switch_audit --devices my-switches.txt --output audit-report.xlsx

# Backward compatibility (still works)
python main_new.py --devices my-switches.txt --output audit-report.xlsx
```

Available Command-Line Arguments

Argument Description Default
--devices, -d Path to devices file devices.txt
--output, -o Output Excel filename audit.xlsx
--workers, -w Number of concurrent threads 10
--stale-days Days threshold for stale ports 30
--direct Skip jump host, connect directly False
--debug Enable debug-level logging False

Example: Custom audit with direct connections:

1
2
3
```bash
python -m switch_audit --devices critical-switches.txt --output critical-audit.xlsx --direct --debug
```

πŸ—οΈ Technical Architecture

The v2.0 restructure transformed the tool from a monolithic script into a professional Python package with clear separation of concerns:

Module Responsibility Why It Matters
cli.py Command-line interface and orchestration Entry point handling, argument parsing, progress tracking
device_auditor.py Device connection and data collection Parallel SSH connections, command execution, retry logic
excel_reporter.py Excel workbook generation and formatting Professional reports with conditional formatting and filters
credentials.py Secure credential retrieval from OS stores Passwords never touch plaintext or config files
jump_manager.py Persistent SSH tunnelling through bastion Centralises network access control; supports air-gapped environments
netmiko_utils.py Network device connection utilities Connection wrapper with timeout and error handling
formatters.py Interface name normalization and Excel formatting Cross-command data correlation and professional output
validators.py Input validation functions Pre-flight checks for files and arguments
app_config.py Configuration access wrapper Singleton pattern for config access across modules
config_loader.py YAML parsing and validation Type-safe settings with environment overrides

Key Design Patterns (v2.0)

1. Package-Based Architecture:

  • Each module has single, well-defined responsibility
  • Clear dependency hierarchy (cli β†’ device_auditor β†’ excel_reporter)
  • Easy to test, extend, and maintain
  • Follows Python packaging best practices

2. Separation of Concerns:

  • Presentation layer (cli.py): User interaction and progress display
  • Business logic layer (device_auditor.py): Data collection and processing
  • Data layer (excel_reporter.py, formatters.py): Output generation
  • Infrastructure layer (credentials.py, jump_manager.py, netmiko_utils.py): Supporting services

3. Configuration Centralization:

  • All config in ProgramFiles/config_files/config_loader.py
  • Accessed via singleton pattern in app_config.py
  • Environment variables override YAML settings
  • Type-safe property accessors

4. Intelligent Fallback Parsing:

  • Primary: TextFSM templates (when available)
  • Fallback: Custom fixed-width parsers in formatters.py
  • Ensures reliability even without external dependencies

5. Multi-Threaded Execution:

  • ThreadPoolExecutor with configurable worker count
  • Thread-safe data accumulation with locks
  • Per-device failure isolation
  • Event-driven progress bar

6. Graceful Error Handling:

  • Exponential backoff retry logic
  • Per-device error capture (doesn't stop entire audit)
  • Comprehensive logging for troubleshooting

πŸ”„ Migration Guide (v1.0 β†’ v2.0)

If you're upgrading from the older monolithic version, see the MIGRATION.md file in the repository for detailed migration instructions.

What Changed in v2.0?

1. Modular Package Design - Code separated into focused modules (cli.py, device_auditor.py, excel_reporter.py, etc.) - Modules/ directory components moved to switch_audit/ package - Config loader relocated to ProgramFiles/config_files/

2. New Entry Point - Old: python main.py --devices devices.txt - New: python -m switch_audit --devices devices.txt (recommended) - Backward Compatible: python main_new.py --devices devices.txt

3. Updated Imports (for developers)

1
2
3
4
5
6
7
8
# Old imports:
from Modules.config_loader import Config
from Modules.credentials import get_secret_with_fallback

# New imports:
from ProgramFiles.config_files.config_loader import Config
from switch_audit.credentials import get_secret_with_fallback
from switch_audit.app_config import config  # Singleton wrapper

Benefits of v2.0 Restructure

Benefit Description
Better Organization Each module has a single, clear responsibility
Easier Testing Isolated components can be unit tested
Professional Architecture Follows enterprise Python package standards
Improved Maintainability Changes to one module don't cascade
Enhanced Extensibility Easy to add new features or output formats
Onboarding New contributors can understand structure quickly

No User Impact

For end users, the tool works identically. All CLI arguments, configuration options, and output formats remain the same. The run.bat launcher has been automatically updated to use the new structure.


πŸ“Š Intelligent Parsing: The Heart of the Tool

Note: In v2.0, parsing logic has been modularized into switch_audit/formatters.py for better maintainability and reusability.

Why Intelligent Parsing Matters

The Problem: Cisco CLI output varies by device model, IOS version, and platform. show interfaces status might be formatted differently on a Catalyst 2960 vs a 9300. TextFSM templates might not exist for your specific platform.

The Solution: Multi-tier parsing strategy with intelligent fallbacks.

Parsing Strategy: TextFSM + Custom Fallback

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
```python
def get_interfaces_via_show_interfaces(conn) -> List[Dict[str, Any]]:
    """
    Use TextFSM to parse 'show interfaces' for all ports.
    Falls back gracefully if templates unavailable.
    """
    try:
        output = conn.send_command("show interfaces", use_textfsm=True)
        if isinstance(output, list):
            return output
        return []
    except Exception:
        return []  # Graceful degradation
```

Why This Approach:

  • TextFSM provides structured parsing when templates exist
  • Returns empty list (not exception) if parsing fails
  • Main logic continues with custom parsers

Custom Fixed-Width Parser: parse_show_interfaces_status()

This is the authoritative source for port mode and VLAN classification.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
```python
def parse_show_interfaces_status(output: str) -> List[Dict[str, str]]:
    """
    Robust fixed-width parser for 'show interfaces status'.
    Handles:
    - Multiple header formats
    - Variable column widths
    - Missing/malformed data
    """
```

Step 1: Identify Header Row

def is_header(ln: str) -> bool:
    return ("Port" in ln and "Status" in ln and "Vlan" in ln and "Speed" in ln)

Why: Header detection must be flexible. Different IOS versions capitalize differently.

Step 2: Extract Column Positions

def _find_columns(header_line: str) -> Dict[str, slice]:
    """
    Calculate exact character positions for each column.
    Returns slice objects for substring extraction.
    """
    tokens = ["Port", "Name", "Status", "Vlan", "Duplex", "Speed", "Type"]
    positions = {}
    for i, tok in enumerate(tokens):
        start = header_line.find(tok)
        end = header_line.find(tokens[i+1]) if i+1 < len(tokens) else len(header_line)
        positions[tok.lower()] = slice(start, end)
    return positions

Why This Matters:

  • Fixed-width parsing is more reliable than regex for tabular CLI output
  • Dynamically calculated positions adapt to slight formatting variations
  • Slice objects provide clean substring extraction

Step 3: Parse Data Rows

for line in lines:
    if line.startswith(("--", "Port")) or not line.strip():
        continue  # Skip separators and empty lines

    record = {}
    for key, col_slice in slices.items():
        record[key] = line[col_slice].strip()

    # Normalize status values
    status_raw = record.get('status', '').lower()
    if 'connect' in status_raw:
        record['status'] = 'connected'
    elif 'notconnect' in status_raw:
        record['status'] = 'notconnect'
    elif 'disabled' in status_raw:
        record['status'] = 'disabled'
    elif 'err' in status_raw:
        record['status'] = 'err-disabled'

Why Status Normalization:

  • Different IOS versions use slight variations ("connected" vs "connect")
  • Normalized values enable reliable conditional formatting in Excel
  • Consistent categorization across device types

Port Mode Classification

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
```python
# Determine mode from VLAN column
vlan_value = record.get('vlan', '').lower()

if vlan_value in ('trunk', 'rspan'):
    mode = 'trunk'
elif vlan_value == 'routed':
    mode = 'routed'
else:
    mode = 'access'  # Default assumption
```

Why This Logic:

  • VLAN column is the most reliable indicator of port mode
  • Trunk ports show "trunk" or "rspan" in VLAN field
  • Routed ports show "routed"
  • Everything else is access (may show VLAN number)

πŸ”Œ PoE Enrichment: Multi-Source Data Fusion

The PoE Challenge

Problem: PoE data (show power inline) uses different interface naming than show interfaces status. Example:

  • Status command: Gi1/0/1
  • PoE command: GigabitEthernet1/0/1

Solution: Interface name aliasing and multi-key lookups.

Interface Name Normalization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
```python
def normalize_ifname(ifname: str) -> Tuple[str, str]:
    """
    Normalize interface names to canonical short and long forms.
    Returns: (short_form, long_form)
    Example: "Gi1/0/1" β†’ ("Gi1/0/1", "GigabitEthernet1/0/1")
    """
    # Extract prefix and port number
    m = re.match(r"([A-Za-z]+)([0-9/\.]+.*)", ifname)
    if not m:
        return (ifname, ifname)

    prefix_raw = m.group(1)
    rest = m.group(2)

    # Map to short form
    short_prefix = _IF_MAP.get(prefix_raw.lower(), prefix_raw)

    # Generate long form
    long_prefix = {
        "Gi": "GigabitEthernet",
        "Fa": "FastEthernet",
        "Te": "TenGigabitEthernet",
        "Eth": "Ethernet",
    }.get(short_prefix, prefix_raw)

    return (f"{short_prefix}{rest}", f"{long_prefix}{rest}")
```

Why This Matters:

  • Enables reliable cross-command matching
  • Handles all common Cisco interface types
  • Works across different IOS versions and platforms

Alias-Based Lookup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
```python
def all_aliases(ifname: str) -> List[str]:
    """
    Return all possible alias strings for an interface.
    Used for PoE data matching.
    """
    short, long = normalize_ifname(ifname)
    return [short, long, ifname]  # Try all variations

# During PoE enrichment:
for alias in all_aliases(port_name):
    if alias in poe_map:
        poe_data = poe_map[alias]
        break
```

Why Multiple Aliases:

  • Different commands use different naming conventions
  • Maximizes successful PoE data correlation
  • Prevents data loss due to naming mismatches

🚨 Stale Port Detection: Conservative Risk Assessment

The Business Problem

Scenario: You have 1,000 switch ports. How do you identify which ones are truly unused vs. temporarily disconnected vs. connected to equipment that's powered off?

False Positives Are Expensive:

  • Marking an active port as "stale" disrupts operations
  • Users lose network access
  • Help desk tickets spike

False Negatives Waste Resources:

  • Unused ports consume switch capacity
  • Security risk (unauthorized devices can plug in)

Conservative Detection Strategy

1
2
3
4
5
6
7
```python
def _categorize_port(row: Dict[str, Any], stale_days: int) -> str:
    """
    Classify port as: active, stale, or available.
    Uses conservative logic to minimize false positives.
    """
```

Rule 1: Only Classify Access Ports

1
2
3
mode = row.get('Mode', '')
if mode != 'access':
    return 'active'  # Trunk and routed ports are infrastructure

Why: Trunk and routed ports connect switches to each other. They should never be flagged as stale.

Rule 2: Connected Ports β€” Check Activity

1
2
3
4
5
6
status = row.get('Status', '')
if status == 'connected':
    last_input_secs = row.get('Last Input Seconds')
    if last_input_secs and last_input_secs >= (stale_days * 86400):
        return 'stale'
    return 'active'

Why:

  • Port is physically connected
  • But hasn't passed traffic in N days
  • Likely a powered-off device or misconfigured endpoint

Rule 3: Disconnected Ports β€” Check for Indicators

if status in ('notconnect', 'disabled', 'err-disabled'):
    # Conservative: require BOTH conditions to flag as stale
    has_poe = row.get('PoE Power (W)')
    has_neighbor = row.get('LLDP/CDP Neighbor')

    poe_w = None
    if has_poe:
        try:
            poe_w = float(str(has_poe).split()[0])
        except:
            pass

    # Stale only if: no PoE draw AND no neighbor
    if (poe_w is None or poe_w == 0.0) and not has_neighbor:
        return 'stale'

    return 'available'  # May be in use (PoE or neighbor present)

Why This Conservative Approach:

Indicator Interpretation
PoE draw > 0 Device is powered (IP phone, camera, AP)
LLDP/CDP neighbor Device is network-aware (switch, phone, AP)
Both absent Likely unused cable or dead device

Example Scenarios:

Status PoE Neighbor Last Input Classification Reasoning
connected 7.0W Yes 10 days active IP phone actively drawing power
connected 0W No 45 days stale Connected but no traffic for 45+ days
notconnect 0W No N/A stale Disconnected, no indicators of use
notconnect 6.5W No N/A available PoE device present (might be powered off)
notconnect 0W Yes N/A available Neighbor detected (might be rebooting)

Time Parsing: Handling Cisco Duration Formats

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
```python
def _parse_last_input_seconds(s: str) -> float | None:
    """
    Parse Cisco 'Last input' timer into seconds.
    Handles: "00:01:23", "1d2h30m", "never"
    """
    s = (s or "").strip().lower()
    if not s or s == "never":
        return None

    # Format 1: hh:mm:ss
    if re.match(r"^\d{1,2}:\d{2}:\d{2}$", s):
        hh, mm, ss = s.split(":")
        return int(hh) * 3600 + int(mm) * 60 + int(ss)

    # Format 2: Compact duration (1y2w3d4h5m6s)
    m = _TIME_RE.fullmatch(s.replace(" ", ""))
    if m:
        y = int(m.group("y") or 0)
        w = int(m.group("w") or 0)
        d = int(m.group("d") or 0)
        h = int(m.group("h").rstrip("h") or 0)
        m_val = int(m.group("m").rstrip("m") or 0)
        s_val = int(m.group("s").rstrip("s") or 0)

        days = y * 365 + w * 7 + d
        return days * 86400 + h * 3600 + m_val * 60 + s_val
```

Why Multiple Format Support:

  • Different IOS versions use different time formats
  • Ensures accurate stale detection across all platforms

πŸ” Credentials & Security

The script retrieves device credentials using switch_audit/credentials.py:

  • Primary: Windows Credential Manager (target name from config.yaml: default MyApp/ADM)
  • Fallback: Interactive prompt for username and password (secure, not echoed)
  • Enable secret: Retrieved by get_enable_secret() if USE_ENABLE environment variable is set, otherwise not required

Note: If you are running on Linux/macOS, ensure credentials.py prompts for credentials or implements your preferred secure store. On Windows, pywin32 enables Credential Manager access.

Important: Never hard-code credentials in the repository. Use the secure store or environment prompts.

Configuration: Credential Manager targets are set in config.yaml under the credentials section:

1
2
3
4
5
```yaml
credentials:
cred_target: "MyApp/ADM"  # Primary credential target
enable_target: ""          # Optional enable secret target
```

πŸ›°οΈ Jump Host (Bastion) Behaviour

  • main.py reads jump_host from the network section of config.yaml
  • Default: the script uses the jump host if --direct is not supplied
  • --direct will skip the jump host entirely and attempt direct SSH connections

Example configuration in config.yaml:

network:
jump_host: "jump-gateway.example.com"  # or "" to disable by default

The JumpManager (now in switch_audit/jump_manager.py) maintains a persistent SSH session to the bastion and proxies device connections through it.

How JumpManager Works:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
```python
class JumpManager:
    def __init__(self, jump_host: str, username: str, password: str):
        self.jump_host = jump_host
        self.username = username
        self.password = password
        self.client = None  # Paramiko SSH client

    def connect(self) -> None:
        """Establish persistent SSH connection to bastion."""
        self.client = paramiko.SSHClient()
        self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.client.connect(
            self.jump_host,
            username=self.username,
            password=self.password,
            timeout=10
        )

    def open_channel(self, target_ip: str, target_port: int):
        """Open direct-tcpip channel through bastion."""
        return self.client.get_transport().open_channel(
            'direct-tcpip',
            (target_ip, target_port),
            ('localhost', 0)
        )
```

Why direct-tcpip Channel:

  • No port forwarding needed on bastion
  • All traffic stays within authenticated SSH session
  • Cleaner than local port forwarding
  • Works with restrictive bastion configurations

�️ Device List File

Provide a plain-text file with one device per line. Lines that are blank or start with # are ignored.

1
2
3
4
5
6
7
```text
# devices.txt
192.0.2.11
192.0.2.12  # inline comments are not parsed; this whole token must be a host/IP only
core-switch-01
access-sw-22
```

Note: Hostnames must be resolvable from the machine (or via the jump host, depending on your SSH setup).


πŸš€ Quick Start

  1. Install dependencies (see Requirements).
  2. Create devices.txt with your targets (see Device list file).
  3. (Optional) Configure config.yaml with your jump_host and other settings.
  4. Run the audit:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    ```bash
    # Using jump host from config.yaml
    python -m switch_audit --devices devices.txt --output access_port_audit.xlsx
    
    # Direct connections (no bastion), 5 workers, different stale threshold
    python -m switch_audit --direct -w 5 --stale-days 60 -d devices.txt -o results.xlsx
    
    # Verbose debugging
    python -m switch_audit --debug -d devices.txt
    ```
    

🧭 CLI Reference

switch_audit exposes the following command-line options:

1
2
3
4
5
6
7
8
```text
--devices, -d    (required)  Path to the devices file (one IP/hostname per line; '#' comments allowed)
--output,  -o    (optional)  Output Excel file name. Default: audit.xlsx
--workers, -w    (optional)  Max concurrent device sessions (threads). Default: 10
--stale-days     (optional)  Days threshold for stale access ports. 0 disables stale flagging. Default: 30
--direct         (optional)  Connect directly (do not use jump host)
--debug          (optional)  Enable verbose logging/prints
```

Required vs Optional

  • Required: --devices
  • Optional: everything else

Usage Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
```bash
# Standard audit with jump host
python -m switch_audit --devices devices.txt --output report.xlsx

# Direct connections, custom workers
python -m switch_audit --devices devices.txt --direct --workers 20

# Debug mode with custom stale threshold
python -m switch_audit --devices devices.txt --stale-days 60 --debug

# Using the launcher
run.bat --devices devices.txt --output audit.xlsx
```

οΏ½πŸ”Œ PortAuditor: The Threaded Collection Engine

Why Parallel Port Auditing is Essential

The Problem: Auditing 50 switches serially with 5 commands per device = 250 SSH round-trips. At 2 seconds per connection, that's 8+ minutes of waiting.

The Solution: Thread pool with 10 concurrent workers = 10 simultaneous SSH sessions. Same 50 switches audited in 1-2 minutes.

Thread-Safe Architecture

1
2
3
4
5
6
7
```python
# Thread-safe accumulators (protected by locks)
self.device_records = []       # Parsed results: one row per device
self.interface_details = []    # Detailed per-interface data
self.failed_devices = {}       # {ip: error_message}
self.progress_lock = threading.Lock()  # Protects shared state
```

Why Thread Locks Matter:

  • Without locks, multiple threads writing to the same list causes data corruption
  • The lock ensures atomic append operations
  • Minimal lock contention because we hold locks for microseconds, not seconds

Command Collection Strategy

For each device, the tool collects five commands in sequence:

Command Purpose Fallback
show version Extract hostname, OS version, uptime Use management IP if hostname parse fails
show interfaces Parse interface types, error counters, activity Use TextFSM if available; use internal parser otherwise
show interfaces status Extract port mode, VLAN, status using fixed-width parsing Built-in fallback parser (no external dependency)
show power inline Collect PoE admin/oper state and power draw Empty dict if device is non-PoE or command fails
show cdp/lldp neighbors detail Detect peer devices on each port Boolean flag (true if neighbour present)

Why This Command Set?

  • Comprehensive but minimal: each command provides data no other command offers
  • Covers the three dimensions of port health: configuration (mode/VLAN), activity (errors, last input), attachment (PoE, neighbours)

The Intelligence Layer: Port Classification

1
2
3
4
5
6
7
8
9
```python
def classify_port(interface_record):
    """
    Assign a port to one of three categories:
    - 'access': Single VLAN, typically hosts
    - 'trunk': Multiple VLANs, typically uplinks
    - 'routed': No VLAN (layer 3), typically inter-device links
    """
```

Classification Logic:

From show interfaces status, inspect the VLAN column:

  • If trunk or rspan β†’ Trunk
  • If routed β†’ Routed
  • Otherwise β†’ Access

Why This Matters:

  • Different port types require different stale-detection rules
  • Access ports should be connected to hosts; trunk ports connect infrastructure
  • This classification enables intelligent filtering and reporting

πŸ›‘ Stale Logic β€” How Ports Are Flagged

A conservative approach is used only for ports in access mode and when --stale-days > 0:

  • If Status = connected β†’ mark stale = True only if Last input β‰₯ <stale-days>
  • If Status β‰  connected β†’ mark stale = True when both conditions hold:
  • No PoE draw (PoE power is blank/-/0.0), and
  • No LLDP/CDP neighbour present on the port

This tends to avoid false positives on trunk/routed ports and on access ports actively in use.

Note: You can disable stale flagging entirely by setting --stale-days 0.


πŸ§ͺ What the Script Collects

For each device the script attempts to gather:

  • Hostname (from show running-config | include ^hostname or CLI prompt fallback)
  • Interfaces via TextFSM (show interfaces) when available
  • Port mode & VLAN via a robust, fixed-width parser of show interfaces status
  • PoE details: admin/oper state, power draw (W), class, device (show power inline)
  • Neighbour presence: LLDP/CDP seen on the port (boolean)
  • Error counters: input, output, CRC (from show interfaces parsed data)
  • Activity indicator: "Last input" time (seconds parsed when present)

πŸ“€ Excel Output Structure

The workbook contains:

1) SUMMARY Sheet (First)

  • One row per device, with a final TOTAL row (sums numeric columns)
  • Columns include:
  • Device, Mgmt IP, Total Ports (phy)
  • Access Ports, Trunk Ports, Routed Ports
  • Connected, Not Connected, Admin Down, Err-Disabled
  • % Access of Total, % Trunk of Total, % Routed of Total, % Connected of Total

2) One Sheet Per Device

Columns typically include (when available):

  • Device, Mgmt IP, Interface (long form), Description
  • Status (normalised: connected / notconnect / administratively down / err-disabled)
  • AdminDown, Connected, ErrDisabled (booleans for quick filters)
  • Mode (access/trunk/routed), VLAN
  • Duplex, Speed, Type
  • Input Errors, Output Errors, CRC Errors
  • Last Input (raw text)
  • PoE Power (W), PoE Oper, PoE Admin
  • LLDP/CDP Neighbour (boolean)
  • Stale (β‰₯<N> d) (boolean)

Formatting

  • Frozen header (A2) and AutoFilter across all columns
  • Auto-sized columns with sensible min/max widths
  • Conditional formatting:
  • Status = connected β†’ green
  • Status = notconnect / err-disabled β†’ red
  • Status = administratively down β†’ grey
  • Any *Errors > 0 β†’ red
  • PoE Power (W) > 0 β†’ green
  • Stale (β‰₯N d) = TRUE β†’ red

βš™οΈ Performance & Concurrency

  • Uses a ThreadPoolExecutor with --workers threads (default 10)
  • An event-driven progress bar updates as jobs start/finish
  • Each device is independent; a failure on one does not stop others

The script prints an event-driven progress bar like:

1
2
3
```
Progress: [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘] 12/30 started: 15/30
```

On completion, the Excel workbook is written to the filename you specify (default audit.xlsx).


οΏ½ Logging, Debug, and Errors

  • Add --debug to surface additional prints (e.g., enable mode attempts, jump host info, file counts)
  • Per-device errors are captured into the device's summary row (and a minimal sheet may be created with the error text so the workbook always reflects all devices)

Common runtime issues & tips:

  • Authentication failures β†’ check Credential Manager entry or typed credentials
  • SSH connectivity β†’ verify reachability from the workstation or via the jump host
  • TextFSM templates missing β†’ parsing still proceeds, but some fields may be blank
  • Channel/line rate limits on older devices β†’ consider lowering --workers

πŸ”§ Extending and Customizing

  • Credentials: Adapt switch_audit/credentials.py to your environment (Linux keyring, Azure Key Vault, etc.)
  • Jump host: Tune switch_audit/jump_manager.py (keep-alive, ciphers, auth methods) as needed
  • Connection behaviour: Modify switch_audit/netmiko_utils.py for device types, timeouts, or SSH options
  • Configuration: Edit config.yaml to set organizational defaults:
  • network.jump_host: Default bastion server
  • concurrency.default_workers: Concurrent device sessions
  • stale_detection.default_stale_days: Stale port threshold
  • credentials.cred_target: Credential Manager target name
  • output.default_filename: Default Excel output filename
  • Output columns: Adjust record construction in switch_audit/device_auditor.py (search for data collection logic)
  • Conditional formatting: Tweak formatting in switch_audit/excel_reporter.py or switch_audit/formatters.py
  • Parsers: Modify parsing logic in switch_audit/formatters.py for custom CLI output handling

Example config.yaml for Enterprise:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
```yaml
network:
jump_host: "bastion.corp.example.com"
read_timeout: 45  # Slower WAN links

credentials:
cred_target: "NetworkAudit/Production"

concurrency:
default_workers: 20  # Fast discovery
retry_attempts: 5    # More retries for flaky network

stale_detection:
default_stale_days: 90  # Longer threshold

output:
default_filename: "port_audit_report.xlsx"

excel_formatting:
min_column_width: 12
max_column_width: 60
```

πŸ”’ Security Considerations

  • Prefer secure stores over plaintext
  • Limit who can run the tool and who can read the generated Excel
  • When using a jump host, ensure strong authentication and proper network segmentation

🧩 Compatibility

  • Target devices: Cisco IOS/IOS-XE access and distribution switches reachable via SSH
  • The tool relies on Netmiko; specify the right device type(s) inside switch_audit/netmiko_utils.py
  • TextFSM/NTC templates significantly improve interface parsing fidelity but are not strictly required

Tested Devices

This tool has been tested and verified on the following Cisco IOS and IOS-XE platforms:

  • Catalyst 9200 Series
  • Catalyst 3650 Series
  • Catalyst 3650C
  • Catalyst 3650CG
  • Catalyst 3650CX
  • Catalyst 2960X Series
  • Catalyst 2960 Series

Note: The tool should work with any Cisco IOS/IOS-XE device that supports the required show commands (interfaces, status, power inline, CDP/LLDP). The devices listed above have been explicitly tested and validated.


βœ… Examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
```bash
# Basic, with jump host (new modular entry point)
python -m switch_audit -d devices.txt -o audit.xlsx

# Direct (no bastion), 20 workers, stale disabled
python -m switch_audit --direct -w 20 --stale-days 0 -d devices.txt -o audit.xlsx

# Conservative concurrency, higher stale threshold, verbose
python -m switch_audit -w 4 --stale-days 90 --debug -d devices.txt -o siteA.xlsx

# Backward compatibility (still works)
python main_new.py --devices devices.txt --output audit.xlsx
```

🧠 FAQs

Q: Do I need NTC TextFSM templates?
A: They are recommended for better show interfaces parsing. Without them, the script still works and uses its internal parser for show interfaces status and best-effort logic elsewhere.

Q: Where do credentials come from?
A: On Windows, from Credential Manager (default target MyApp/ADM). Otherwise, you are prompted interactively or you can adapt switch_audit/credentials.py to your secret store.

Q: How is Mode determined?
A: From show interfaces status: if VLAN column is trunk/rspan β†’ trunk; if routed β†’ routed; otherwise access.

Q: How is a port considered stale?
A: Only for access ports and when --stale-days > 0. Connected ports are flagged stale only if Last input β‰₯ N days. Disconnected ports require both no PoE draw and no LLDP/CDP neighbour to be flagged stale.

Q: What changed in v2.0?
A: The monolithic main.py was restructured into a professional Python package (switch_audit/) with modular components. The functionality is identical, but the code is now organized following enterprise best practices. See MIGRATION.md for details.



πŸŽ“ Learning Outcomes

After studying this code, you should understand:

βœ… YAML Configuration Management β€” How to separate configuration from code using YAML with Python
βœ… Python Package Design β€” Structuring modular packages with clear entry points and separation of concerns
βœ… Singleton Pattern β€” Using configuration singletons for application-wide settings access
βœ… Fixed-Width Parsing β€” Reliable CLI output parsing without external dependencies
βœ… Multi-Source Data Fusion β€” Correlating data across different commands using interface name aliasing
βœ… Conservative Risk Assessment β€” Stale port detection logic that minimizes false positives
βœ… Thread-Safe Concurrency β€” Parallel device audits with proper lock management
βœ… Intelligent Fallback Strategy β€” TextFSM + custom parsers for maximum compatibility
βœ… SSH Tunneling β€” Jump host integration with Paramiko direct-tcpip channels
βœ… Excel Automation β€” Professional workbook generation with conditional formatting
βœ… Exponential Backoff β€” Retry logic for transient network failures
βœ… Credential Management β€” Secure OS-level credential storage integration
βœ… Modular Architecture β€” Separating CLI, business logic, and presentation layers

Key Code Patterns Demonstrated

Pattern 1: Modular Package Structure

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
```python
# Entry point (__main__.py)
from .cli import main
if __name__ == "__main__":
    main()

# CLI layer delegates to business logic
from .device_auditor import audit_device
results = audit_device(ip, username, password, ...)

# Business logic delegates to reporting
from .excel_reporter import ExcelReporter
reporter = ExcelReporter()
reporter.generate(results)
```

Pattern 2: Configuration Singleton

1
2
3
4
5
6
7
8
9
```python
# app_config.py - Single source of truth
from ProgramFiles.config_files.config_loader import Config
config = Config()

# Used throughout application
from .app_config import config
workers = config.default_workers
```

Pattern 3: Graceful Degradation**

1
2
3
4
5
6
```python
try:
    data = parse_with_textfsm(output)  # Preferred method
except:
    data = parse_with_custom_logic(output)  # Fallback
```

Pattern 4: Multi-Key Lookup

1
2
3
4
5
6
```python
for alias in all_aliases(interface_name):
    if alias in poe_map:
        poe_data = poe_map[alias]
        break
```

Pattern 5: Thread-Safe Accumulation

1
2
3
4
```python
with lock:
    results.append(new_data)  # Atomic operation
```

Pattern 6: Conservative Classification

1
2
3
4
5
6
```python
if condition_A and condition_B:  # Both must be true
    mark_as_risky()
else:
    mark_as_safe()  # Default to safe
```

Pattern 7: Type-Safe Configuration

1
2
3
4
5
```python
@property
def default_workers(self) -> int:
    return self._get_nested("concurrency", "default_workers", default=10)
```

πŸš€ Distribution & Execution

Consistent with the Nautomation Prime delivery model, this tool is available in multiple formats:

  • Zero-Install Portable Bundle: A self-contained package including the Python interpreter and all libraries (Netmiko, Pandas, OpenPyXL) for use on restricted Windows jump boxes.

  • Scheduled Docker Appliance: A pre-built container designed for autonomous execution and periodic port auditing.

  • Source Code: Full access to customize parsing logic, add vendor-specific commands, or integrate with your CMDB.


Get Started Now:

Learn More:

Explore Similar Topics:


οΏ½πŸ“‹ Licence

GNU General Public Licence v3.0

πŸ‘€ Author

Christopher Davies


Mission: To empower network engineers through the PRIME Frameworkβ€”delivering automation with measurable ROI, production-grade quality, and sustainable team capability built on the PRIME Philosophy of transparency, measurability, ownership, safety, and empowerment.