Rewrite Python to Go

This commit is contained in:
2025-07-05 23:38:28 +00:00
parent 50fbf802a3
commit 64a4d53cf7
6 changed files with 350 additions and 236 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
router_backup

View File

@ -1 +0,0 @@
paramiko>=2.9.0

View File

@ -1,235 +0,0 @@
#!/usr/bin/env python3
"""
SSH Router Backup Script
Connects to routers via SSH and runs commands, saving output to local files.
"""
import paramiko
import sys
import os
from datetime import datetime
import argparse
import json
import yaml
class RouterBackup:
def __init__(self, hostname, username, password=None, key_file=None, port=22):
self.hostname = hostname
self.username = username
self.password = password
self.key_file = key_file
self.port = port
self.client = None
def connect(self):
"""Establish SSH connection to the router"""
try:
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
if self.key_file:
self.client.connect(
hostname=self.hostname,
username=self.username,
key_filename=self.key_file,
port=self.port,
timeout=30
)
elif os.environ.get('SSH_AUTH_SOCK'):
# Use SSH agent if available
self.client.connect(
hostname=self.hostname,
username=self.username,
port=self.port,
timeout=30
)
else:
self.client.connect(
hostname=self.hostname,
username=self.username,
password=self.password,
port=self.port,
timeout=30
)
print(f"Successfully connected to {self.hostname}")
return True
except Exception as e:
print(f"Failed to connect to {self.hostname}: {e}")
return False
def run_command(self, command):
"""Execute a command on the router and return the output"""
if not self.client:
print("No active connection")
return None
try:
stdin, stdout, stderr = self.client.exec_command(command)
output = stdout.read().decode('utf-8')
error = stderr.read().decode('utf-8')
if error:
print(f"Error executing '{command}': {error}")
return None
return output
except Exception as e:
print(f"Failed to execute command '{command}': {e}")
return None
def backup_commands(self, commands, output_dir="backup"):
"""Run multiple commands and save outputs to files"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_info = {
"hostname": self.hostname,
"timestamp": timestamp,
"commands": {}
}
# Truncate output file at start
filename = f"{self.hostname}.txt"
filepath = os.path.join(output_dir, filename)
open(filepath, 'w').close()
for i, command in enumerate(commands):
print(f"Running command {i+1}/{len(commands)}: {command}")
output = self.run_command(command)
if output:
# Use hostname as filename
filename = f"{self.hostname}.txt"
filepath = os.path.join(output_dir, filename)
with open(filepath, 'a') as f:
f.write(f"## COMMAND: {command}\n")
f.write(output)
backup_info["commands"][command] = {
"filename": filename,
"success": True,
"output_length": len(output)
}
print(f"Output saved to {filepath}")
else:
backup_info["commands"][command] = {
"filename": None,
"success": False,
"output_length": 0
}
# Save backup info
info_file = os.path.join(output_dir, f"{self.hostname}_{timestamp}_info.json")
with open(info_file, 'w') as f:
json.dump(backup_info, f, indent=2)
return backup_info
def disconnect(self):
"""Close SSH connection"""
if self.client:
self.client.close()
print(f"Disconnected from {self.hostname}")
def main():
parser = argparse.ArgumentParser(description='SSH Router Backup Tool')
parser.add_argument('--config', required=True, help='YAML configuration file path')
parser.add_argument('--password', help='SSH password')
parser.add_argument('--key-file', help='SSH private key file path')
parser.add_argument('--port', type=int, default=22, help='SSH port (default: 22)')
parser.add_argument('--git-repo', default='/tmp', help='Git repository directory for command output files (default: /tmp)')
args = parser.parse_args()
# Load configuration
try:
with open(args.config, 'r') as f:
config = yaml.safe_load(f)
except Exception as e:
print(f"Failed to load config file {args.config}: {e}")
sys.exit(1)
# Use SSH key by default, fall back to password
if not args.password and not args.key_file:
# Check if SSH agent is available first
if os.environ.get('SSH_AUTH_SOCK'):
print("Using SSH agent for authentication")
else:
# Try default SSH key locations
default_keys = [
os.path.expanduser("~/.ssh/id_rsa"),
os.path.expanduser("~/.ssh/id_ed25519"),
os.path.expanduser("~/.ssh/id_ecdsa")
]
for key_path in default_keys:
if os.path.exists(key_path):
args.key_file = key_path
print(f"Using SSH key: {key_path}")
break
# If no key found and no SSH agent, prompt for password
if not args.key_file:
import getpass
args.password = getpass.getpass("No SSH key found. Enter SSH password: ")
# Process each device in config
devices = config.get('devices', {})
if not devices:
print("No devices found in config file")
sys.exit(1)
types = config.get('types', {})
success_count = 0
total_count = len(devices)
for hostname, device_config in devices.items():
print(f"\nProcessing device: {hostname}")
user = device_config.get('user')
commands = device_config.get('commands', [])
device_type = device_config.get('type')
# If device has a type, get commands from types section
if device_type and device_type in types:
commands = types[device_type].get('commands', [])
if not user:
print(f"No user specified for {hostname}, skipping")
continue
if not commands:
print(f"No commands specified for {hostname}, skipping")
continue
# Create backup instance
backup = RouterBackup(
hostname=hostname,
username=user,
password=args.password,
key_file=args.key_file,
port=args.port
)
# Connect and backup
if backup.connect():
try:
backup_info = backup.backup_commands(commands, args.git_repo)
print(f"Backup completed for {hostname}")
print(f"Summary: {sum(1 for cmd in backup_info['commands'].values() if cmd['success'])}/{len(commands)} commands successful")
success_count += 1
finally:
backup.disconnect()
else:
print(f"Failed to connect to {hostname}")
print(f"\nOverall summary: {success_count}/{total_count} devices processed successfully")
if __name__ == "__main__":
main()

15
src/go.mod Normal file
View File

@ -0,0 +1,15 @@
module router_backup
go 1.21
require (
github.com/spf13/cobra v1.8.0
golang.org/x/crypto v0.18.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.16.0 // indirect
)

19
src/go.sum Normal file
View File

@ -0,0 +1,19 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

315
src/router_backup.go Normal file
View File

@ -0,0 +1,315 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"net"
"os"
"path/filepath"
"time"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"gopkg.in/yaml.v2"
)
// Config structures
type Config struct {
Types map[string]DeviceType `yaml:"types"`
Devices map[string]Device `yaml:"devices"`
}
type DeviceType struct {
Commands []string `yaml:"commands"`
}
type Device struct {
User string `yaml:"user"`
Type string `yaml:"type,omitempty"`
Commands []string `yaml:"commands,omitempty"`
}
// RouterBackup handles SSH connections and command execution
type RouterBackup struct {
hostname string
username string
password string
keyFile string
port int
client *ssh.Client
}
// NewRouterBackup creates a new RouterBackup instance
func NewRouterBackup(hostname, username, password, keyFile string, port int) *RouterBackup {
return &RouterBackup{
hostname: hostname,
username: username,
password: password,
keyFile: keyFile,
port: port,
}
}
// Connect establishes SSH connection to the router
func (rb *RouterBackup) Connect() error {
config := &ssh.ClientConfig{
User: rb.username,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 30 * time.Second,
}
// Try SSH agent first if available
if sshAuthSock := os.Getenv("SSH_AUTH_SOCK"); sshAuthSock != "" {
if conn, err := net.Dial("unix", sshAuthSock); err == nil {
agentClient := agent.NewClient(conn)
config.Auth = []ssh.AuthMethod{ssh.PublicKeysCallback(agentClient.Signers)}
}
}
// If SSH agent didn't work, try key file
if len(config.Auth) == 0 && rb.keyFile != "" {
key, err := ioutil.ReadFile(rb.keyFile)
if err != nil {
return fmt.Errorf("unable to read private key: %v", err)
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return fmt.Errorf("unable to parse private key: %v", err)
}
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
}
// Fall back to password if available
if len(config.Auth) == 0 && rb.password != "" {
config.Auth = []ssh.AuthMethod{ssh.Password(rb.password)}
}
if len(config.Auth) == 0 {
return fmt.Errorf("no authentication method available")
}
address := fmt.Sprintf("%s:%d", rb.hostname, rb.port)
client, err := ssh.Dial("tcp", address, config)
if err != nil {
return fmt.Errorf("failed to connect to %s: %v", rb.hostname, err)
}
rb.client = client
fmt.Printf("Successfully connected to %s\n", rb.hostname)
return nil
}
// RunCommand executes a command on the router and returns the output
func (rb *RouterBackup) RunCommand(command string) (string, error) {
if rb.client == nil {
return "", fmt.Errorf("no active connection")
}
session, err := rb.client.NewSession()
if err != nil {
return "", fmt.Errorf("failed to create session: %v", err)
}
defer session.Close()
output, err := session.CombinedOutput(command)
if err != nil {
return "", fmt.Errorf("failed to execute command '%s': %v", command, err)
}
return string(output), nil
}
// BackupCommands runs multiple commands and saves outputs to files
func (rb *RouterBackup) BackupCommands(commands []string, gitRepo string) error {
if err := os.MkdirAll(gitRepo, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %v", gitRepo, err)
}
filename := fmt.Sprintf("%s.txt", rb.hostname)
filepath := filepath.Join(gitRepo, filename)
// Truncate file at start
file, err := os.Create(filepath)
if err != nil {
return fmt.Errorf("failed to create file %s: %v", filepath, err)
}
file.Close()
successCount := 0
for i, command := range commands {
fmt.Printf("Running command %d/%d: %s\n", i+1, len(commands), command)
output, err := rb.RunCommand(command)
if err != nil {
fmt.Printf("Error executing '%s': %v\n", command, err)
continue
}
// Append to file
file, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
fmt.Printf("Failed to open file for writing: %v\n", err)
continue
}
fmt.Fprintf(file, "## COMMAND: %s\n", command)
file.WriteString(output)
file.Close()
fmt.Printf("Output saved to %s\n", filepath)
successCount++
}
fmt.Printf("Summary: %d/%d commands successful\n", successCount, len(commands))
return nil
}
// Disconnect closes SSH connection
func (rb *RouterBackup) Disconnect() {
if rb.client != nil {
rb.client.Close()
fmt.Printf("Disconnected from %s\n", rb.hostname)
}
}
// loadConfig loads the YAML configuration file
func loadConfig(configPath string) (*Config, error) {
data, err := ioutil.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read config file %s: %v", configPath, err)
}
var config Config
err = yaml.Unmarshal(data, &config)
if err != nil {
return nil, fmt.Errorf("failed to parse YAML: %v", err)
}
return &config, nil
}
// findDefaultSSHKey looks for default SSH keys
func findDefaultSSHKey() string {
homeDir, err := os.UserHomeDir()
if err != nil {
return ""
}
defaultKeys := []string{
filepath.Join(homeDir, ".ssh", "id_rsa"),
filepath.Join(homeDir, ".ssh", "id_ed25519"),
filepath.Join(homeDir, ".ssh", "id_ecdsa"),
}
for _, keyPath := range defaultKeys {
if _, err := os.Stat(keyPath); err == nil {
fmt.Printf("Using SSH key: %s\n", keyPath)
return keyPath
}
}
return ""
}
func main() {
var configPath string
var password string
var keyFile string
var port int
var gitRepo string
var rootCmd = &cobra.Command{
Use: "router_backup",
Short: "SSH Router Backup Tool",
Long: "Connects to routers via SSH and runs commands, saving output to local files.",
Run: func(cmd *cobra.Command, args []string) {
// Load configuration
config, err := loadConfig(configPath)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Check authentication setup
if password == "" && keyFile == "" {
if os.Getenv("SSH_AUTH_SOCK") != "" {
fmt.Println("Using SSH agent for authentication")
} else {
keyFile = findDefaultSSHKey()
if keyFile == "" {
log.Fatal("No SSH key found and no password provided")
}
}
}
// Process devices
if len(config.Devices) == 0 {
log.Fatal("No devices found in config file")
}
successCount := 0
totalCount := len(config.Devices)
for hostname, deviceConfig := range config.Devices {
fmt.Printf("\nProcessing device: %s\n", hostname)
user := deviceConfig.User
commands := deviceConfig.Commands
deviceType := deviceConfig.Type
// If device has a type, get commands from types section
if deviceType != "" {
if typeConfig, exists := config.Types[deviceType]; exists {
commands = typeConfig.Commands
}
}
if user == "" {
fmt.Printf("No user specified for %s, skipping\n", hostname)
continue
}
if len(commands) == 0 {
fmt.Printf("No commands specified for %s, skipping\n", hostname)
continue
}
// Create backup instance
backup := NewRouterBackup(hostname, user, password, keyFile, port)
// Connect and backup
if err := backup.Connect(); err != nil {
fmt.Printf("Failed to connect to %s: %v\n", hostname, err)
continue
}
err = backup.BackupCommands(commands, gitRepo)
backup.Disconnect()
if err != nil {
fmt.Printf("Backup failed for %s: %v\n", hostname, err)
} else {
fmt.Printf("Backup completed for %s\n", hostname)
successCount++
}
}
fmt.Printf("\nOverall summary: %d/%d devices processed successfully\n", successCount, totalCount)
},
}
rootCmd.Flags().StringVar(&configPath, "config", "", "YAML configuration file path (required)")
rootCmd.Flags().StringVar(&password, "password", "", "SSH password")
rootCmd.Flags().StringVar(&keyFile, "key-file", "", "SSH private key file path")
rootCmd.Flags().IntVar(&port, "port", 22, "SSH port")
rootCmd.Flags().StringVar(&gitRepo, "git-repo", "/tmp", "Git repository directory for command output files")
rootCmd.MarkFlagRequired("config")
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}