Scripts in this directory are called directly by the NACC backend when a show command or backup is triggered from the UI. Each script SSHs to the target device, runs the appropriate command, and prints the result to stdout.
Every script in this directory must follow this contract exactly.
python3 /opt/nacc/scripts/python/<script_name>.py <device_ip>
| Argument | Source | Description |
|---|---|---|
sys.argv[1] |
Backend service | Device IP address (always present) |
| Stream | Purpose | Shown in UI |
|---|---|---|
stdout |
Human-readable command output (print()) |
Frontend terminal |
stderr |
Diagnostics / error messages (print(..., file=sys.stderr)) |
job.error field |
| Code | Meaning | Job status in UI |
|---|---|---|
0 |
Success | success |
1+ |
Failure | failed |
| Variable | Used by |
|---|---|
NETWORK_USERNAME |
All scripts (SSH) |
NETWORK_PASSWORD |
All scripts (SSH) |
GITEA_URL |
backup_config.py |
GITEA_TOKEN |
backup_config.py |
GITEA_USER |
backup_config.py |
GITEA_REPO |
backup_config.py |
The script filename must exactly match the command name registered in
PYTHON_COMMANDS inside backend/src/routes/automation.ts:
show_version → show_version.py
show_interfaces → show_interfaces.py
backup_config → backup_config.py
#!/usr/bin/env python3
"""
<script_name>.py — Brief description of what this script shows.
Usage:
python3 <script_name>.py <device_ip>
"""
import sys
import os
def main():
if len(sys.argv) < 2:
print("Usage: <script_name>.py <device_ip>", file=sys.stderr)
sys.exit(1)
device_ip = sys.argv[1]
username = os.environ.get('NETWORK_USERNAME', '')
password = os.environ.get('NETWORK_PASSWORD', '')
if not username or not password:
print("NETWORK_USERNAME and NETWORK_PASSWORD must be set in .env", file=sys.stderr)
sys.exit(1)
try:
# Example using netmiko:
# from netmiko import ConnectHandler
# conn = ConnectHandler(
# device_type='cisco_ios',
# host=device_ip,
# username=username,
# password=password,
# )
# output = conn.send_command('show version')
# conn.disconnect()
# print(output)
pass
except Exception as e:
print(f"Error connecting to {device_ip}: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()This script is the full implementation of "Backup to Gitea". The Ansible
playbook (ansible/playbooks/backup_config.yml) only saves to a local file
and does not push to Gitea. The Python version completes the intended design.
Flow:
SSH to device
→ show running-config
→ base64-encode the output
→ PUT /api/v1/repos/{GITEA_USER}/{GITEA_REPO}/contents/{device_ip}.cfg
(Gitea API — creates file if new, updates if exists)
→ print summary to stdout
Gitea API reference: PUT /api/v1/repos/{owner}/{repo}/contents/{filepath}
- Returns
201for new files,200for updates - Requires
Authorization: token <GITEA_TOKEN>header - Body:
{ message: "...", content: "<base64>", sha: "<existing_sha_or_omit>" }
- Write the script here following the contract above.
- Add the command name to
PYTHON_COMMANDSinbackend/src/routes/automation.ts. - Add a description in
getPlaybookDescription()in the same file. - Restart the backend.
- Optionally add a button in
frontend/src/components/device/DeviceActions.tsx.
| Script | Command | Description |
|---|---|---|
show_version.py |
show_version |
Device version and hardware info |
show_interfaces.py |
show_interfaces |
Interface status and statistics |
show_vlan.py |
show_vlan |
VLAN configuration |
show_running.py |
show_running |
Running configuration |
show_power_inline.py |
show_power_inline |
PoE power inline status |
backup_config.py |
backup_config |
SSH → running config → Gitea API |
(Add rows here as new scripts are created)