// Copyright 2025, IPng Networks GmbH, Pim van Pelt package main import ( "fmt" "log" "os" "path/filepath" "github.com/spf13/cobra" ) const Version = "1.2.3" // Config and SSH types are now in separate packages // SSH connection methods are now in ssh.go // YAML processing is now handled by the config package // SSH helper functions are now in ssh.go func main() { var yamlFiles []string var password string var keyFile string var port int var outputDir string var hostFilter []string var rootCmd = &cobra.Command{ Use: "ipng-router-backup", Short: "SSH Router Backup Tool", Long: "Connects to routers via SSH and runs commands, saving output to local files.", Version: Version, Run: func(cmd *cobra.Command, args []string) { fmt.Printf("IPng Networks Router Backup v%s\n", Version) // Expand glob patterns in YAML files var expandedYamlFiles []string for _, pattern := range yamlFiles { matches, err := filepath.Glob(pattern) if err != nil { log.Fatalf("Invalid glob pattern '%s': %v", pattern, err) } if len(matches) == 0 { log.Fatalf("No files matched pattern '%s'", pattern) } expandedYamlFiles = append(expandedYamlFiles, matches...) } // Load configuration cfg, err := ConfigRead(expandedYamlFiles) 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(cfg.Devices) == 0 { log.Fatal("No devices found in config file") } // Filter devices if --host flags are provided devicesToProcess := cfg.Devices if len(hostFilter) > 0 { devicesToProcess = make(map[string]Device) for _, pattern := range hostFilter { patternMatched := false for hostname, deviceConfig := range cfg.Devices { if matched, _ := filepath.Match(pattern, hostname); matched { devicesToProcess[hostname] = deviceConfig patternMatched = true } } if !patternMatched { fmt.Printf("Warning: Host pattern '%s' did not match any devices\n", pattern) } } } successCount := 0 totalCount := len(devicesToProcess) for hostname, deviceConfig := range devicesToProcess { fmt.Printf("\nProcessing device: %s (type: %s)\n", hostname, deviceConfig.Type) user := deviceConfig.User commands := deviceConfig.Commands deviceType := deviceConfig.Type // If device has a type, get commands from types section if deviceType != "" { if typeConfig, exists := cfg.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, deviceConfig.Address, 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, outputDir) 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) // Set exit code based on results if successCount == 0 { os.Exit(11) // All devices failed } else if successCount < totalCount { os.Exit(10) // Some devices failed } // Exit code 0 (success) when all devices succeeded }, } rootCmd.Flags().StringSliceVar(&yamlFiles, "yaml", []string{}, "YAML configuration file paths (required, can be repeated)") 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(&outputDir, "output-dir", "/tmp", "Output directory for command output files") rootCmd.Flags().StringSliceVar(&hostFilter, "host", []string{}, "Specific host(s) to process (can be repeated, processes all if not specified)") rootCmd.MarkFlagRequired("yaml") if err := rootCmd.Execute(); err != nil { log.Fatal(err) } }