Skip to content

Cisco SG300 Automated Backup Engine ​

This document details the architecture and complete implementation of the automated backup engine running on the Synology NAS (10.10.30.30). It connects across VLAN boundaries to extract and archive the running configuration of the Cisco SG300-52 switch (10.10.60.2).


1. Technical Challenge & Architecture Solution ​

The Cisco SG300-52 (firmware v1.2.7.76) presents two technical hurdles for standard programmatic backups:

  1. Deprecated Cryptography: Modern OpenSSH drops support for SHA-1 key exchanges and CBC ciphers by default.
  2. Interactive CLI Menu & Prompts: The switch forces interactive non-standard prompts (User Name:, Password:) and pages terminal outputs with --More--. Standard non-interactive SSH commands fail to execute cleanly.

Solution: Python Native Pseudo-Terminal (PTY) ​

The NAS executes a Python 3 script using standard library modules (pty, os, select, re). By allocating a pseudo-terminal child process, the script emulates a real human operator typing into a console.


2. Complete Python Backup Script ​

Script file path on NAS: /volume1/Backup/scripts/cisco_backup.py

python
#!/usr/bin/env python3
"""
Automated Cisco SG300-52 Backup Script via Python PTY
Runs natively on Synology DSM without third-party dependencies (Netmiko/Paramiko).
"""

import os
import pty
import select
import time
import re
from datetime import datetime

# --- CONFIGURATION SETTINGS ---
SWITCH_IP = "10.10.60.2"
SWITCH_USER = "cisco"
SWITCH_PASS = "YourSecurePasswordHere"  # Update with switch credentials
BACKUP_DIR = "/volume1/Backup/System/CiscoSg300"
RETENTION_DAYS = 30

TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S")
OUTPUT_FILE = os.path.join(BACKUP_DIR, f"sg300_{TIMESTAMP}.cfg")

SSH_CMD = [
    "ssh",
    "-o", "StrictHostKeyChecking=no",
    "-o", "UserKnownHostsFile=/dev/null",
    "-o", "KexAlgorithms=+diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1",
    "-o", "HostKeyAlgorithms=+ssh-rsa",
    "-c", "aes128-cbc,aes256-cbc,3des-cbc",
    f"{SWITCH_USER}@{SWITCH_IP}"
]

def clean_output(raw_text):
    """Strip ANSI escape sequences and carriage returns."""
    ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
    cleaned = ansi_escape.sub('', raw_text)
    return cleaned.replace('\r', '')

def run_backup():
    if not os.path.exists(BACKUP_DIR):
        os.makedirs(BACKUP_DIR, exist_ok=True)

    master, slave = pty.openpty()
    pid = os.fork()

    if pid == 0:
        os.close(master)
        os.setsid()
        os.dup2(slave, 0)
        os.dup2(slave, 1)
        os.dup2(slave, 2)
        os.close(slave)
        os.execvp("ssh", SSH_CMD)
    else:
        os.close(slave)
        buffer = ""
        stage = "CONNECTING"
        config_captured = []
        capture_active = False

        start_time = time.time()
        timeout = 90

        try:
            while time.time() - start_time < timeout:
                r, _, _ = select.select([master], [], [], 1.0)
                if master in r:
                    try:
                        data = os.read(master, 1024).decode('utf-8', errors='ignore')
                    except OSError:
                        break
                    
                    if not data:
                        break

                    buffer += data

                    if stage == "CONNECTING":
                        if "User Name:" in buffer:
                            os.write(master, (SWITCH_USER + "\n").encode())
                            buffer = ""
                        elif "Password:" in buffer:
                            os.write(master, (SWITCH_PASS + "\n").encode())
                            buffer = ""
                            stage = "LOGGED_IN"
                    
                    elif stage == "LOGGED_IN":
                        if "#" in buffer or ">" in buffer:
                            os.write(master, b"terminal datadump\n")
                            time.sleep(1)
                            os.write(master, b"show running-config\n")
                            stage = "CAPTURING"
                            capture_active = True
                            buffer = ""
                    
                    elif stage == "CAPTURING":
                        config_captured.append(data)
                        if "switchf6eb8c#" in buffer or (buffer.endswith("#") and len(config_captured) > 5):
                            os.write(master, b"exit\n")
                            break

            full_config = clean_output("".join(config_captured))
            
            if "config-file-header" in full_config:
                parsed_config = full_config[full_config.find("config-file-header"):]
            else:
                parsed_config = full_config

            with open(OUTPUT_FILE, "w") as f:
                f.write(parsed_config.strip() + "\n")

            print(f"[SUCCESS] Backup saved: {OUTPUT_FILE}")

        finally:
            os.close(master)
            os.waitpid(pid, 0)

if __name__ == "__main__":
    run_backup()