From c8df809c295ec31bbe1c7da4348faf16c79e1f4f Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Mon, 7 Jul 2025 00:39:56 +0200 Subject: [PATCH] Add types.exclude pattern --- etc/00-device-types.yaml | 3 ++ src/config.go | 1 + src/main.go | 6 ++-- src/ssh.go | 31 +++++++++++++++-- src/ssh_test.go | 75 ++++++++++++++++++++++++++++++---------- 5 files changed, 94 insertions(+), 22 deletions(-) diff --git a/etc/00-device-types.yaml b/etc/00-device-types.yaml index b753742..d8dd135 100644 --- a/etc/00-device-types.yaml +++ b/etc/00-device-types.yaml @@ -56,3 +56,6 @@ types: - system license print # License information - / interface print # Interfaces - / export terse # Configuration + exclude: + - "^# ....-..-.. ..:..:.. by RouterOS" + - "^# .../../.... ..:..:.. by RouterOS" diff --git a/src/config.go b/src/config.go index bacc0ee..361928b 100644 --- a/src/config.go +++ b/src/config.go @@ -18,6 +18,7 @@ type Config struct { type DeviceType struct { Commands []string `yaml:"commands"` + Exclude []string `yaml:"exclude,omitempty"` } type Device struct { diff --git a/src/main.go b/src/main.go index f981b22..460d370 100644 --- a/src/main.go +++ b/src/main.go @@ -100,11 +100,13 @@ func main() { user := deviceConfig.User commands := deviceConfig.Commands deviceType := deviceConfig.Type + var excludePatterns []string - // If device has a type, get commands from types section + // If device has a type, get commands and exclude patterns from types section if deviceType != "" { if typeConfig, exists := cfg.Types[deviceType]; exists { commands = typeConfig.Commands + excludePatterns = typeConfig.Exclude } } @@ -127,7 +129,7 @@ func main() { continue } - err = backup.BackupCommands(commands, outputDir) + err = backup.BackupCommands(commands, excludePatterns, outputDir) backup.Disconnect() if err != nil { diff --git a/src/ssh.go b/src/ssh.go index d12f007..4ec8c2d 100644 --- a/src/ssh.go +++ b/src/ssh.go @@ -8,6 +8,7 @@ import ( "net" "os" "path/filepath" + "regexp" "strconv" "strings" "time" @@ -202,8 +203,33 @@ func (rb *RouterBackup) RunCommand(command string) (string, error) { return string(output), nil } +// filterOutput removes lines matching exclude patterns from the output +func filterOutput(output string, excludePatterns []string) string { + if len(excludePatterns) == 0 { + return output + } + + lines := strings.Split(output, "\n") + var filteredLines []string + + for _, line := range lines { + exclude := false + for _, pattern := range excludePatterns { + if matched, _ := regexp.MatchString(pattern, line); matched { + exclude = true + break + } + } + if !exclude { + filteredLines = append(filteredLines, line) + } + } + + return strings.Join(filteredLines, "\n") +} + // BackupCommands runs multiple commands and saves outputs to files -func (rb *RouterBackup) BackupCommands(commands []string, outputDir string) error { +func (rb *RouterBackup) BackupCommands(commands []string, excludePatterns []string, outputDir string) error { if err := os.MkdirAll(outputDir, 0755); err != nil { return fmt.Errorf("failed to create directory %s: %v", outputDir, err) } @@ -241,7 +267,8 @@ func (rb *RouterBackup) BackupCommands(commands []string, outputDir string) erro } fmt.Fprintf(file, "## COMMAND: %s\n", command) - file.WriteString(output) + filteredOutput := filterOutput(output, excludePatterns) + file.WriteString(filteredOutput) file.Close() successCount++ diff --git a/src/ssh_test.go b/src/ssh_test.go index b6c9446..e023dce 100644 --- a/src/ssh_test.go +++ b/src/ssh_test.go @@ -57,17 +57,22 @@ func TestBackupCommandsDirectoryCreation(t *testing.T) { // This should create the directory even without a connection // and fail gracefully when trying to run commands - _ = rb.BackupCommands([]string{"show version"}, outputDir) + err := rb.BackupCommands([]string{"show version"}, []string{}, outputDir) // Should not error on directory creation if _, statErr := os.Stat(outputDir); os.IsNotExist(statErr) { t.Error("Expected output directory to be created") } - // Should create the output file even if commands fail + // Should return error when commands fail + if err == nil { + t.Error("Expected error when commands fail") + } + + // Should NOT create the output file when commands fail (atomic behavior) expectedFile := filepath.Join(outputDir, "testhost") - if _, statErr := os.Stat(expectedFile); os.IsNotExist(statErr) { - t.Error("Expected output file to be created") + if _, statErr := os.Stat(expectedFile); !os.IsNotExist(statErr) { + t.Error("Expected output file to NOT be created when commands fail") } } @@ -76,15 +81,15 @@ func TestBackupCommandsEmptyCommands(t *testing.T) { rb := NewRouterBackup("testhost", "", "testuser", "testpass", "", 22) - err := rb.BackupCommands([]string{}, tempDir) - if err != nil { - t.Errorf("Expected no error for empty commands list, got %v", err) + err := rb.BackupCommands([]string{}, []string{}, tempDir) + if err == nil { + t.Error("Expected error for empty commands list (no successful commands)") } - // Should still create the output file + // Should NOT create the output file when no commands succeed expectedFile := filepath.Join(tempDir, "testhost") - if _, statErr := os.Stat(expectedFile); os.IsNotExist(statErr) { - t.Error("Expected output file to be created even for empty commands") + if _, statErr := os.Stat(expectedFile); !os.IsNotExist(statErr) { + t.Error("Expected output file to NOT be created when no commands succeed") } } @@ -160,20 +165,19 @@ func TestBackupCommandsFileOperations(t *testing.T) { // Create some fake commands (they will fail but we can test file operations) commands := []string{"show version", "show interfaces"} - err := rb.BackupCommands(commands, tempDir) - if err != nil { - t.Errorf("Unexpected error: %v", err) + err := rb.BackupCommands(commands, []string{}, tempDir) + if err == nil { + t.Error("Expected error when all commands fail") } - // Check that output file was created + // Check that output file was NOT created (atomic behavior) outputFile := filepath.Join(tempDir, "testhost") _, err = os.ReadFile(outputFile) - if err != nil { - t.Fatalf("Failed to read output file: %v", err) + if err == nil { + t.Error("Expected output file to not exist when all commands fail") } - // File should be created (it will be empty if all commands fail) - // This test just verifies the file creation works + // This test verifies that atomic file behavior works correctly } func TestRouterBackupConnectionState(t *testing.T) { @@ -281,3 +285,38 @@ func TestIPv6AddressFormatting(t *testing.T) { t.Error("Expected IPv4 address to use tcp4 network type") } } + +func TestFilterOutput(t *testing.T) { + // Test with no exclude patterns + input := "line1\nline2\nline3" + result := filterOutput(input, []string{}) + if result != input { + t.Errorf("Expected no filtering with empty patterns, got '%s'", result) + } + + // Test with matching pattern + input = "# 2025-07-06 21:30:45 by RouterOS\nconfig line 1\nconfig line 2" + excludePatterns := []string{"^# ....-..-.. ..:..:.. by RouterOS"} + expected := "config line 1\nconfig line 2" + result = filterOutput(input, excludePatterns) + if result != expected { + t.Errorf("Expected '%s', got '%s'", expected, result) + } + + // Test with multiple patterns + input = "line1\nDEBUG: debug info\nline2\nINFO: info message\nline3" + excludePatterns = []string{"^DEBUG:", "^INFO:"} + expected = "line1\nline2\nline3" + result = filterOutput(input, excludePatterns) + if result != expected { + t.Errorf("Expected '%s', got '%s'", expected, result) + } + + // Test with no matches + input = "line1\nline2\nline3" + excludePatterns = []string{"nomatch"} + result = filterOutput(input, excludePatterns) + if result != input { + t.Errorf("Expected no filtering when patterns don't match, got '%s'", result) + } +}