Compare commits

...

10 Commits

Author SHA1 Message Date
Pim van Pelt
d87967c977 Properly name IPng Networks 2025-07-06 11:35:42 +00:00
Pim van Pelt
7e91564f37 Simplify Makefile 2025-07-06 11:20:22 +00:00
Pim van Pelt
097206060d Add some README 2025-07-06 11:16:14 +00:00
Pim van Pelt
3ff7aeb61c Release v1.0.1, clean up debian build 2025-07-06 11:09:12 +00:00
Pim van Pelt
a8a627a95e add --version 2025-07-06 11:00:51 +00:00
Pim van Pelt
d15f03ac10 Add AP2 license 2025-07-06 10:57:53 +00:00
Pim van Pelt
d60a5a944a Add sync-version 2025-07-06 10:55:18 +00:00
Pim van Pelt
254db32eed make fmt 2025-07-06 10:47:29 +00:00
Pim van Pelt
f3422dc19a Add simple tests 2025-07-06 10:46:46 +00:00
Pim van Pelt
48923ebad5 Ouput files without .txt suffix 2025-07-06 10:43:52 +00:00
13 changed files with 995 additions and 31 deletions

2
.gitignore vendored
View File

@@ -4,8 +4,8 @@ ipng-router-backup
debian/.debhelper/
debian/.gocache/
debian/go/
debian/files
debian/ipng-router-backup/
debian/*.substvars
debian/debhelper-build-stamp
debian/*.debhelper

202
LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -10,9 +10,17 @@ GO_FILES=$(SOURCE_DIR)/main.go
.PHONY: all
all: build
# Sync version from debian/changelog to Go source
.PHONY: sync-version
sync-version:
@echo "Syncing version from debian/changelog..."
@VERSION=$$(head -1 debian/changelog | sed 's/.*(\([^)]*\)).*/\1/'); \
sed -i "s/const Version = \".*\"/const Version = \"$$VERSION\"/" $(SOURCE_DIR)/main.go; \
echo "Version synced: $$VERSION"
# Build the Go binary
.PHONY: build
build:
build: sync-version
@echo "Building $(BINARY_NAME)..."
cd $(SOURCE_DIR) && go build -o ../$(BUILD_DIR)/$(BINARY_NAME) main.go
@echo "Build complete: $(BINARY_NAME)"
@@ -21,14 +29,15 @@ build:
.PHONY: clean
clean:
@echo "Cleaning build artifacts..."
rm -rf debian/.debhelper debian/.gocache debian/go debian/$(BINARY_NAME) debian/files debian/*.substvars debian/debhelper-build-stamp
rm -f ../$(BINARY_NAME)_*.deb ../$(BINARY_NAME)_*.changes ../$(BINARY_NAME)_*.buildinfo
rm -f $(BUILD_DIR)/$(BINARY_NAME)
@echo "Clean complete"
# Run tests
.PHONY: test
test:
@echo "Running tests..."
cd $(SOURCE_DIR) && go test ./...
cd $(SOURCE_DIR) && go test -v .
# Format Go code
.PHONY: fmt
@@ -38,16 +47,9 @@ fmt:
# Build Debian package
.PHONY: pkg-deb
pkg-deb:
pkg-deb: sync-version build
fakeroot dpkg-buildpackage -us -uc -b
# Clean package artifacts
.PHONY: clean-pkg
clean-pkg:
@echo "Cleaning package artifacts..."
@rm -f *.deb
@echo "Package cleanup complete"
# Show help
.PHONY: help
help:
@@ -56,6 +58,6 @@ help:
@echo " clean - Remove build artifacts"
@echo " test - Run tests"
@echo " fmt - Format Go code"
@echo " sync-version - Sync version from debian/changelog to Go source"
@echo " pkg-deb - Create Debian package"
@echo " clean-pkg - Remove package artifacts"
@echo " help - Show this help message"

81
README.md Normal file
View File

@@ -0,0 +1,81 @@
# IPng Networks Router Backup
SSH-based network device configuration backup tool with support for multiple device types and flexible authentication methods.
## Features
- **Multi-device backup**: Configure multiple routers in YAML
- **Device type templates**: Reusable command sets per device type
- **Flexible authentication**: SSH agent, key files, or password
- **Selective execution**: Backup specific devices with `--host` flags
- **Professional CLI**: Standard flags, version info, and help
## Quick Start
### Installation
```bash
# Install Debian package
sudo dpkg -i ipng-router-backup_X.Y.Z_amd64.deb
# Or build from source
make build
```
### Basic Usage
1. **Create configuration file** (`config.yaml`):
```yaml
types:
srlinux:
commands:
- show version
- show platform linecard
- info flat from running
devices:
asw100:
user: admin
type: srlinux
asw120:
user: admin
type: srlinux
```
2. **Run backup**:
```bash
# Backup all devices
ipng-router-backup --config config.yaml --output-dir /backup
# Backup specific devices
ipng-router-backup --config config.yaml --host asw100 --output-dir /backup
```
3. **Check output**:
```bash
ls /backup/
# asw100 asw120
cat /backup/asw100
# ## COMMAND: show version
# Hostname : asw100
# Software Version : v25.3.2
# ...
```
## Authentication
The tool automatically tries authentication methods in this order:
1. **SSH Agent** (if `SSH_AUTH_SOCK` is set)
2. **SSH Key File** (`--key-file` or default locations)
3. **Password** (`--password` flag)
## Documentation
- **[Detailed Documentation](docs/DETAILS.md)** - Complete feature guide, configuration reference, and examples
- **[Manual Page](docs/router_backup.1)** - Unix manual page
- **[Changelog](debian/changelog)** - Version history and changes

11
debian/changelog vendored
View File

@@ -1,3 +1,14 @@
ipng-router-backup (1.0.1) stable; urgency=low
* Add version information to help output
* Add --version/-v flag support
* Add --host flag for selective device processing
* Add version sync between Debian package and binary
* Remove .txt suffix from output files
* Rename binary to ipng-router-backup
-- Pim van Pelt <pim@ipng.ch> Sun, 06 Jul 2025 10:45:00 +0100
ipng-router-backup (1.0.0) stable; urgency=low
* Initial release

View File

@@ -1 +0,0 @@
ipng-router-backup

2
debian/files vendored
View File

@@ -1,2 +0,0 @@
ipng-router-backup_1.0.0_amd64.buildinfo net optional
ipng-router-backup_1.0.0_amd64.deb net optional

2
debian/postinst vendored
View File

@@ -1,3 +1,3 @@
#!/bin/bash
echo 'IPNG Router Backup installed successfully.'
echo 'IPng Networks Router Backup installed successfully.'
echo 'Example config at /etc/ipng-router-backup/config.yaml.example'

358
docs/DETAILS.md Normal file
View File

@@ -0,0 +1,358 @@
# IPng Networks Router Backup - Detailed Documentation
## Overview
IPng Networks Router Backup is a SSH-based network device configuration backup tool written in Go. It connects to multiple network devices defined in a YAML configuration file, executes commands via SSH, and saves the output to local files.
## Key Features
- **Multi-device support**: Backup multiple routers in a single run
- **Device type templates**: Define command sets per device type
- **Flexible authentication**: SSH agent, key files, or password authentication
- **Selective execution**: Target specific devices with `--host` flags
- **Automatic file organization**: Output files named by hostname
- **Command identification**: Each command output prefixed with command name
- **Version synchronization**: Automatic version sync between package and binary
## Configuration File Format
The tool uses a YAML configuration file with two main sections: `types` and `devices`.
### Complete Example
```yaml
types:
srlinux:
commands:
- show version
- show platform linecard
- show platform fan-tray
- show platform power-supply
- info flat from running
eos:
commands:
- show version
- show inventory
- show env power
- show running-config
centec:
commands:
- show version | exc uptime
- show boot images
- show transceiver
- show running-config
devices:
asw100:
user: admin
type: srlinux
asw120:
user: netops
type: srlinux
core-01:
user: admin
type: eos
edge-router:
user: operator
commands:
- show version
- show ip route summary
```
### Configuration Fields
#### Types Section
**`types`**: Define reusable command sets for different device types.
- **`<type-name>`**: Arbitrary name for the device type (e.g., `srlinux`, `eos`)
- **`commands`**: Array of CLI commands to execute on devices of this type
#### Devices Section
**`devices`**: Define individual network devices to backup.
- **`<hostname>`**: Device hostname or IP address
- **`user`** (required): SSH username for authentication
- **`type`** (optional): Reference to a type definition for commands
- **`commands`** (optional): Direct command list (overrides type commands)
### Configuration Validation
- Each device must have a `user` field
- Each device must have either a `type` field (referencing a valid type) or a `commands` field
- Type references must exist in the `types` section
- Commands can be specified either via type reference or directly per device
## Command Line Flags
### Required Flags
- **`--config`**: Path to YAML configuration file
### Optional Flags
- **`--output-dir`**: Output directory for backup files (default: `/tmp`)
- **`--host`**: Specific hostname(s) to process (can be repeated)
- **`--password`**: SSH password for authentication
- **`--key-file`**: Path to SSH private key file
- **`--port`**: SSH port number (default: `22`)
- **`--help`**: Show help information
- **`--version`**: Show version information
### Flag Examples
```bash
# Basic usage - all devices
ipng-router-backup --config /etc/network-backup/config.yaml
# Custom output directory
ipng-router-backup --config config.yaml --output-dir /backup/network
# Specific devices only
ipng-router-backup --config config.yaml --host asw100 --host core-01
# Multiple specific devices
ipng-router-backup --config config.yaml --host asw100 --host asw120 --host core-01
# Custom SSH port
ipng-router-backup --config config.yaml --port 2222
# Using password authentication
ipng-router-backup --config config.yaml --password mypassword
# Using specific SSH key
ipng-router-backup --config config.yaml --key-file ~/.ssh/network_key
```
## SSH Authentication Methods
The tool supports multiple SSH authentication methods in the following priority order:
### 1. SSH Agent (Highest Priority)
Automatically used when the `SSH_AUTH_SOCK` environment variable is set.
```bash
# Start SSH agent and add keys
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa
# Run backup (will use SSH agent automatically)
ipng-router-backup --config config.yaml
```
**Advantages:**
- Most secure (keys remain in memory)
- No password prompts
- Works with hardware security modules
- Single sign-on experience
### 2. SSH Key File
Specify a private key file with `--key-file` or use default locations.
```bash
# Explicit key file
ipng-router-backup --config config.yaml --key-file ~/.ssh/network_key
# Tool automatically checks these default locations:
# ~/.ssh/id_rsa
# ~/.ssh/id_ed25519
# ~/.ssh/id_ecdsa
```
**Key File Requirements:**
- Must be in OpenSSH format
- Proper permissions (600 recommended)
- Corresponding public key must be on target devices
### 3. Password Authentication (Lowest Priority)
Use `--password` flag for password-based authentication.
```bash
# Command line password (not recommended for scripts)
ipng-router-backup --config config.yaml --password mypassword
# Interactive password prompt (when no other auth available)
ipng-router-backup --config config.yaml
# Output: "No SSH key found. Enter SSH password: "
```
**Security Considerations:**
- Passwords visible in process lists
- Not suitable for automation
- Consider using key-based authentication instead
## Output Format
### File Naming
Output files are named after the device hostname:
- Device `asw100` → File `asw100`
- Device `192.168.1.1` → File `192.168.1.1`
### File Content Structure
Each output file contains all command outputs with headers:
```
## COMMAND: show version
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Hostname : asw100
Chassis Type : 7220 IXR-D4
Software Version : v25.3.2
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
## COMMAND: show platform linecard
+-------------+----+-------------+-------------------+---------------------------------+
| Module Type | ID | Admin State | Operational State | Model |
+=============+====+=============+===================+=================================+
| linecard | 1 | N/A | up | imm28-100g-qsfp28+8-400g-qsfpdd |
+-------------+----+-------------+-------------------+---------------------------------+
```
### File Behavior
- **New runs**: Files are truncated and recreated
- **Multiple commands**: All outputs concatenated in single file
- **Command identification**: Each command prefixed with `## COMMAND: <command>`
## Usage Examples
### Basic Backup All Devices
```bash
ipng-router-backup --config /etc/backup/network.yaml --output-dir /backup/$(date +%Y%m%d)
```
### Backup Specific Device Types
Create a config with only the devices you want, or use `--host`:
```bash
# Backup only SR Linux devices
ipng-router-backup --config network.yaml --host asw100 --host asw120 --host asw121
```
### Scheduled Backup with SSH Agent
```bash
#!/bin/bash
# /etc/cron.daily/network-backup
# Start SSH agent
eval "$(ssh-agent -s)"
ssh-add /root/.ssh/network_backup_key
# Run backup
BACKUP_DIR="/backup/network/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
ipng-router-backup \
--config /etc/network-backup/config.yaml \
--output-dir "$BACKUP_DIR"
# Kill SSH agent
ssh-agent -k
```
### Emergency Single Device Backup
```bash
# Quick backup of single device with password
ipng-router-backup \
--config emergency.yaml \
--host core-router-01 \
--password emergency123 \
--output-dir /tmp/emergency-backup
```
## Error Handling
### Common Issues and Solutions
**Device Connection Failures:**
- Check SSH connectivity: `ssh user@hostname`
- Verify authentication method
- Check firewall rules and network connectivity
**Configuration Errors:**
- Validate YAML syntax: `yamllint config.yaml`
- Check that all referenced types exist
- Ensure all devices have required fields
**Permission Issues:**
- Verify SSH key permissions (600)
- Check output directory write permissions
- Ensure user has SSH access to target devices
### Exit Codes
- `0`: Success
- `1`: Configuration error, authentication failure, or connection issues
## Advanced Usage
### Integration with Git
```bash
#!/bin/bash
# Backup and commit to git repository
BACKUP_DIR="/backup/network-configs"
cd "$BACKUP_DIR"
# Run backup
ipng-router-backup --config config.yaml --output-dir .
# Commit changes
git add .
git commit -m "Network backup $(date '+%Y-%m-%d %H:%M:%S')"
git push origin main
```
### Custom Command Sets per Environment
```yaml
types:
production-srlinux:
commands:
- show version
- show system information
- info flat from running
lab-srlinux:
commands:
- show version
- show interface brief
devices:
prod-asw100:
user: readonly
type: production-srlinux
lab-asw100:
user: admin
type: lab-srlinux
```
### Monitoring and Alerting
```bash
#!/bin/bash
# Backup with monitoring
if ipng-router-backup --config config.yaml --output-dir /backup; then
echo "Backup completed successfully" | logger
else
echo "Backup failed!" | logger
# Send alert email
echo "Network backup failed at $(date)" | mail -s "Backup Alert" admin@company.com
fi
```

View File

@@ -1,4 +1,4 @@
.TH IPNG-ROUTER-BACKUP 1 "July 2025" "ipng-router-backup 1.0.0" "User Commands"
.TH IPNG-ROUTER-BACKUP 1 "July 2025" "ipng-router-backup" "User Commands"
.SH NAME
ipng-router-backup \- SSH Router Backup Tool
.SH SYNOPSIS

View File

@@ -1,3 +1,5 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package main
import (
@@ -15,6 +17,8 @@ import (
"gopkg.in/yaml.v2"
)
const Version = "1.0.1"
// Config structures
type Config struct {
Types map[string]DeviceType `yaml:"types"`
@@ -129,7 +133,7 @@ func (rb *RouterBackup) BackupCommands(commands []string, outputDir string) erro
return fmt.Errorf("failed to create directory %s: %v", outputDir, err)
}
filename := fmt.Sprintf("%s.txt", rb.hostname)
filename := rb.hostname
filepath := filepath.Join(outputDir, filename)
// Truncate file at start
@@ -227,7 +231,10 @@ func main() {
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)
// Load configuration
config, err := loadConfig(configPath)
if err != nil {

287
src/main_test.go Normal file
View File

@@ -0,0 +1,287 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package main
import (
"os"
"path/filepath"
"testing"
)
func TestNewRouterBackup(t *testing.T) {
rb := NewRouterBackup("test-host", "test-user", "test-pass", "/test/key", 2222)
if rb.hostname != "test-host" {
t.Errorf("Expected hostname 'test-host', got '%s'", rb.hostname)
}
if rb.username != "test-user" {
t.Errorf("Expected username 'test-user', got '%s'", rb.username)
}
if rb.password != "test-pass" {
t.Errorf("Expected password 'test-pass', got '%s'", rb.password)
}
if rb.keyFile != "/test/key" {
t.Errorf("Expected keyFile '/test/key', got '%s'", rb.keyFile)
}
if rb.port != 2222 {
t.Errorf("Expected port 2222, got %d", rb.port)
}
}
func TestFindDefaultSSHKey(t *testing.T) {
// Create a temporary directory to simulate home directory
tempDir := t.TempDir()
sshDir := filepath.Join(tempDir, ".ssh")
err := os.MkdirAll(sshDir, 0755)
if err != nil {
t.Fatalf("Failed to create .ssh directory: %v", err)
}
// Create a fake SSH key
keyPath := filepath.Join(sshDir, "id_rsa")
err = os.WriteFile(keyPath, []byte("fake-key"), 0600)
if err != nil {
t.Fatalf("Failed to create fake SSH key: %v", err)
}
// Temporarily change HOME environment variable
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", tempDir)
result := findDefaultSSHKey()
if result != keyPath {
t.Errorf("Expected SSH key path '%s', got '%s'", keyPath, result)
}
}
func TestFindDefaultSSHKeyNotFound(t *testing.T) {
// Create a temporary directory with no SSH keys
tempDir := t.TempDir()
// Temporarily change HOME environment variable
originalHome := os.Getenv("HOME")
defer os.Setenv("HOME", originalHome)
os.Setenv("HOME", tempDir)
result := findDefaultSSHKey()
if result != "" {
t.Errorf("Expected empty string when no SSH key found, got '%s'", result)
}
}
func TestLoadConfig(t *testing.T) {
// Create a temporary config file
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "test-config.yaml")
configContent := `types:
test-type:
commands:
- show version
- show status
devices:
test-device:
user: testuser
type: test-type
direct-device:
user: directuser
commands:
- direct command
`
err := os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to create test config file: %v", err)
}
config, err := loadConfig(configPath)
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}
// Test types section
if len(config.Types) != 1 {
t.Errorf("Expected 1 type, got %d", len(config.Types))
}
testType, exists := config.Types["test-type"]
if !exists {
t.Error("Expected 'test-type' to exist in types")
}
if len(testType.Commands) != 2 {
t.Errorf("Expected 2 commands in test-type, got %d", len(testType.Commands))
}
// Test devices section
if len(config.Devices) != 2 {
t.Errorf("Expected 2 devices, got %d", len(config.Devices))
}
testDevice, exists := config.Devices["test-device"]
if !exists {
t.Error("Expected 'test-device' to exist in devices")
}
if testDevice.User != "testuser" {
t.Errorf("Expected user 'testuser', got '%s'", testDevice.User)
}
if testDevice.Type != "test-type" {
t.Errorf("Expected type 'test-type', got '%s'", testDevice.Type)
}
}
func TestLoadConfigInvalidFile(t *testing.T) {
_, err := loadConfig("/nonexistent/config.yaml")
if err == nil {
t.Error("Expected error when loading nonexistent config file")
}
}
func TestLoadConfigInvalidYAML(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "invalid-config.yaml")
// Create invalid YAML content
invalidYAML := `types:
test-type:
commands:
- show version
invalid: [unclosed
`
err := os.WriteFile(configPath, []byte(invalidYAML), 0644)
if err != nil {
t.Fatalf("Failed to create invalid config file: %v", err)
}
_, err = loadConfig(configPath)
if err == nil {
t.Error("Expected error when loading invalid YAML")
}
}
func TestBackupCommandsDirectoryCreation(t *testing.T) {
rb := NewRouterBackup("test-host", "test-user", "", "", 22)
tempDir := t.TempDir()
outputDir := filepath.Join(tempDir, "new-directory")
// Test with empty commands to avoid SSH connection
err := rb.BackupCommands([]string{}, outputDir)
if err != nil {
t.Fatalf("BackupCommands failed: %v", err)
}
// Check if directory was created
if _, err := os.Stat(outputDir); os.IsNotExist(err) {
t.Error("Expected output directory to be created")
}
}
func TestBackupCommandsFileCreation(t *testing.T) {
rb := NewRouterBackup("test-host", "test-user", "", "", 22)
tempDir := t.TempDir()
expectedFilePath := filepath.Join(tempDir, "test-host")
// Test with empty commands to avoid SSH connection
err := rb.BackupCommands([]string{}, tempDir)
if err != nil {
t.Fatalf("BackupCommands failed: %v", err)
}
// Check if file was created
if _, err := os.Stat(expectedFilePath); os.IsNotExist(err) {
t.Error("Expected output file to be created")
}
}
// Benchmark tests
func BenchmarkNewRouterBackup(b *testing.B) {
for i := 0; i < b.N; i++ {
NewRouterBackup("bench-host", "bench-user", "bench-pass", "/bench/key", 22)
}
}
func BenchmarkLoadConfig(b *testing.B) {
// Create a temporary config file
tempDir := b.TempDir()
configPath := filepath.Join(tempDir, "bench-config.yaml")
configContent := `types:
srlinux:
commands:
- show version
- show platform linecard
devices:
device1:
user: user1
type: srlinux
device2:
user: user2
type: srlinux
`
err := os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
b.Fatalf("Failed to create benchmark config file: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := loadConfig(configPath)
if err != nil {
b.Fatalf("Failed to load config: %v", err)
}
}
}
// Example test to demonstrate usage
func ExampleNewRouterBackup() {
rb := NewRouterBackup("example-host", "admin", "", "/home/user/.ssh/id_rsa", 22)
_ = rb // Use the router backup instance
}
// Table-driven test for multiple scenarios
func TestRouterBackupCreation(t *testing.T) {
tests := []struct {
name string
hostname string
username string
password string
keyFile string
port int
}{
{"Basic", "host1", "user1", "pass1", "/key1", 22},
{"Custom Port", "host2", "user2", "pass2", "/key2", 2222},
{"No Password", "host3", "user3", "", "/key3", 22},
{"No Key", "host4", "user4", "pass4", "", 22},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rb := NewRouterBackup(tt.hostname, tt.username, tt.password, tt.keyFile, tt.port)
if rb.hostname != tt.hostname {
t.Errorf("Expected hostname '%s', got '%s'", tt.hostname, rb.hostname)
}
if rb.username != tt.username {
t.Errorf("Expected username '%s', got '%s'", tt.username, rb.username)
}
if rb.password != tt.password {
t.Errorf("Expected password '%s', got '%s'", tt.password, rb.password)
}
if rb.keyFile != tt.keyFile {
t.Errorf("Expected keyFile '%s', got '%s'", tt.keyFile, rb.keyFile)
}
if rb.port != tt.port {
t.Errorf("Expected port %d, got %d", tt.port, rb.port)
}
})
}
}

19
src/version_test.go Normal file
View File

@@ -0,0 +1,19 @@
// Copyright 2025, IPng Networks GmbH, Pim van Pelt <pim@ipng.ch>
package main
import (
"testing"
)
func TestVersion(t *testing.T) {
if Version == "" {
t.Error("Version constant should not be empty")
}
// Test that version follows semantic versioning pattern
// This is a basic check - could be more sophisticated
if len(Version) < 5 { // minimum "1.0.0" format
t.Errorf("Version '%s' seems too short for semantic versioning", Version)
}
}