Backup of devices without CLI with Unimus

Backup of devices without CLI with Unimus

A guide how to backup devices which don't expose their configs over CLI using Unimus' API and a bit of scripting.

Today we will have a look at how you can handle devices which don't output their configs over CLI, which is not supported by Unimus out-of-the-box. However, a bit of scripting and Unimus' API enables you to navigate around this adversity.

Intro

Configuration backup is an essential practice in any network environment. Possessing recent configuration files safeguards against data loss or system failure and lets you restore your network to a working state much faster than redeploying everything manually from scratch. Believe me this has saved money (and people's jobs) in the past.

Back on a serious note, as a full-featured Network Configuration Management solution Unimus performs these backup duties for you. Over time, Unimus also builds a versioned configuration history of your network from each device backup and notifies you of any and all changes in your network. With this superior visibility Unimus gives you a whole new level of change management adding on to other user favorite features like configuration auditing and change automation.

How Unimus backs up your devices and when devices make it complicated

Unimus gathers backups from managed devices via CLI as any user would. It logs into the device using Telnet or SSH (please don't use Telnet however) and then retrieves the configuration of the device. And therein lies a potential issue. Not all systems have a CLI, such as MikroTik's SwOS (learn how you can get around it in this article on our blog), and some that have a CLI do not support CLI-based configuration backup. We'll call these the Unbackupables.

To illustrate an example, let's have a look at FortiAuthenticator by FortiNet. As a network element which provides centralized authentication services its configuration consists of both a textual configuration, available over the CLI, and a configuration database, which is managed through a web GUI. The config DB includes users, groups, the FortiToken device list, certificates, and many other config elements. All of them can be neatly packed into a binary backup file. FortiAuthenticator allows you to set up auto-backup. This is done by specifying an FTP server address, FTP directory, backup frequency and backup time.

FortiAuthenticator screenshot of Auto-backup menu
FortiAuthenticator auto-backup config GUI

Another example where CLI doesn't support a config dump is PMP 450 access points by Cambium. Complete configuration can be downloaded via web interface and it will be stored in a text file (.cfg). The backup file can then be passed along to an FTP server using device specific CLI commands.

Cambium PMP web GUI for downloading config file
Cambium PMP 450 configuration file download via GUI

We now have knowledge about particularities of backups in some devices and, as was cleverly foreshadowed above, we can have binary/text backup files pushed to an FTP server (TFTP, SCP or SFTP also works). We can also push backups into Unimus using its API. Let's use all this and a few lines of code to backup even the Unbackupable.

Setup

In real-world deployments, topologies can and will be complex. However with our setup it all boils down to three discrete components - the network device, an FTP server and Unimus, plus a script to tie it all together. Then we automate the process via scheduling. The following section captures our test environment in more detail:

Simple diagram of config push to Unimus

1) A cisco router on the left, will represent our Unbackupable - a network device Unimus cannot directly pull a config backup from. It needs to have the backup delivered to an FTP server. Just a note - we are using a Cisco device just for illustration as Unimus supports all Cisco devices natively, without needing to use an FTP server.

2) An FTP (or TFTP, SCP, SFTP...) server runs on either Linux or Windows Server. The only difference being what script version we use - Shell or Powershell. The script will push backups to Unimus (using the API) from files on the FTP server.

3) Unimus. We assume you already have your Unimus server up and running. If not, we have a guide for how to deploy Unimus on our wiki.

The Script. Simply put, it creates backups on Unimus via API. It is described in detail later.


For our setup to work, device backups need to somehow find themselves on the FTP server. There are two scenarios how this can be achieved:

1) Device itself is capable of a scheduled backup push to an FTP server

Similar to the FortiAuthenticator example, mentioned in the Intro, a device can be set up to push its config backup to an FTP server. Likewise, on Cisco IOS, Embedded Event Manager (EEM) feature enables you to create EEM Applet to execute action "copy config to FTP" when event "scheduled time" is triggered. The following image illustrates this logic:

Diagram 1 of config push to Unimus
(1) Cisco EEM prompts backup push to FTP (2) Server scheduler executes the script (3) The script grabs backups from FTP and (4) pushes them to Unimus.

This is the simpler scenario. Only two jobs need to be scheduled. First one on Cisco router via EEM applet that pushes running configuration to an FTP server at a scheduled time. Example below:

event manager applet backup-config-daily
 event timer cron name daily-time cron-entry "30 3 * * *"
 action 1.0 cli command "enable"
 action 2.0 cli command "copy running-config ftp://username:password@ftp-server-ip/router.ip.address/config-$(date \"+%Y%m%d%H%M%S\").cfg"
Cisco IOS EEM applet for 'config backup to FTP' everyday at 3:30am

Second job is scheduled on the host running FTP server via Cron or Task Scheduler, depending on the environment used.

2) The other option - device has to be periodically instructed to push its backup to an FTP server

Since the device itself is incapable of automatically backing itself up to the FTP server, an outside agent (Unimus) will send commands to the device instructing it to create a config backup and copy it to an FTP server according to a schedule.

Diagram 2 of config push to Unimus
(1) Unimus prompts device to push backup to FTP and (2) device pushes backup to FTP. (3) Server scheduler executes the script (4) Script grabs backups from FTP and (5) pushes them to Unimus.

Although this diagram looks more complicated, again, we only need to schedule two jobs. First one runs on Unimus. It is a Mass Config Push preset following a daily schedule:

Unimus job preset GUI
Unimus Mass Config Push preset for 'config backup to FTP' everyday at 3:30am
tclsh
regexp {\S+ +\S+ +(\w+) +(\d+) +(\d+)} [exec show clock] match month day year
exec copy run ftp://my.ftp.server/router.ip.address/config-$day-$month-$year
Command set from the push job preset

In both scenarios the second job is the execution of script on host running FTP server that pushes backups to Unimus. We will schedule it in Cron or Task Scheduler, depending on the environment.

Pushing the backups from the FTP server to Unimus

Once config backups are copied to the FTP server, be it in binary or text form, our script can push them to Unimus. Backups of the devices will be stored on the FTP server following this specific structure for convenience:

Example directory structure
/ftp_rootdir/<device_ip_address>/config_backup

Each device has a separate directory using its management IP address as name, where all configuration backup versions are stored:

Example config backup files
e.g: /home/ftp_data/10.31.8.3/Backup_10.31.8.3_2023-07-14_10-04.txt

On the host with the FTP server we will be running a script (Linux shell or Powershell) that uploads the config backups to Unimus. Here are the steps it takes:

  1. Comb through each subdirectory under defined root directory extracting the directory name (in form of IP address)
  2. Look up device on Unimus; (optional) work on specified Zone; (also optional) create a new device if none is found
  3. Encode each backup file found in a subdirectory to base64 string digestible by Unimus
  4. Create device backup on Unimus via API from oldest to newest and delete it from ftp directory

Shell script

The full ready-to-use script can be found on our GitHub. If you are using the Powershell script, just skip to the Powershell section. We will focus on the Bash script in this section.

You can just download and use it. If you are curious in its internal functioning, feel free to read on!

Working mainly with APIs we will be using curl and jq packages. Install as necessary.

To start let's define mandatory variables for convenient parametric approach. This is the only part of the script we need to adjust for our environment to make it work. Apart from Unimus hostname/IP address, generated API token and http headers the script only needs to know the FTP root directory where the configs are saved.

# Mandatory parameters
UNIMUS_ADDRESS="<http(s)://unimus.server.address(:port)>"
TOKEN="<api token>"
# FTP root directory
FTP_FOLDER="/home/user/ftp_data/"
Mandatory variables

Next there are some optional parameters that are enabled by uncommenting the variables. From Unimus version 2.4.0-Beta3 we can specify a Zone for the script to work on. We can enable curl insecure option if self-signed certificate is used. Also we have an option to create new devices in Unimus if specific ones have not yet been added.

# Optional parameters
# Specifies the Zone where devices will be searched for by address/hostname
# CASE SENSITIVE; leave commented to use the Default (0) zone
#ZONE="0"
# Insecure mode
# If you are using self-signed certificates you might want to set this to true
SELF_SIGNED_CERT=false
# Variable for enabling creation of new devices in Unimus; set to true to enable
CREATE_DEVICES=false
# Specify description of new devices created in Unimus by the script
CREATED_DESC="The Unbackupable"
Optional variables

Our backup solution is based on API calls to Unimus. Create new device and Get device by address are APIs from APIv2. Both functions createNewDevice and getDeviceId require device IP address as input ($1). GetDeviceId returns device ID value from the response using | jq .data.id. When working with a specific Zone, createNewDevice sends an additional key-value pair in the body and getDeviceId appends zoneId parameter to the URI. Check out the full API documentation on wiki.

function createNewDevice() {
    if [ -z "$ZONE" ]; then
        curl $insecure -X POST -sSL -H "$HEADERS_ACCEPT" -H "$HEADERS_CONTENT_TYPE" -H "$HEADERS_AUTHORIZATION" -d '{"address": "'"$1"'","description":"'"$CREATED_DESC"'"}'\
        "$UNIMUS_ADDRESS/api/v2/devices" > /dev/null
    else
        curl $insecure -X POST -sSL -H "$HEADERS_ACCEPT" -H "$HEADERS_CONTENT_TYPE" -H "$HEADERS_AUTHORIZATION" -d '{"address": "'"$1"'","description":"'"$CREATED_DESC"'", "zoneId": "'"$ZONE"'"}'\
        "$UNIMUS_ADDRESS/api/v2/devices" > /dev/null
    fi
}
Shell function for 'Create new device' API
function getDeviceId() {
    if [ -z "$ZONE" ]; then
        echo "$(curl $insecure -X GET -sSL -H "$HEADERS_ACCEPT" -H "$HEADERS_AUTHORIZATION" "$UNIMUS_ADDRESS/api/v2/devices/findByAddress/$1" | jq .data.id)"
    else
        echo "$(curl $insecure -X GET -sSL -H "$HEADERS_ACCEPT" -H "$HEADERS_AUTHORIZATION" "$UNIMUS_ADDRESS/api/v2/devices/findByAddress/$1?zoneId=$ZONE" | jq .data.id)"
    fi
}
Shell function for 'Get device by address' API

Create new backup API call is done by the createBackup function. It needs device ID ($1) and a JSON ($2) with base64 string of backup file and TEXT/BINARY string as input arguments.

function createBackup() {
    curl $insecure -X POST -sSL -H "$HEADERS_ACCEPT" -H "$HEADERS_CONTENT_TYPE" -H "$HEADERS_AUTHORIZATION" -d "@$2" "$UNIMUS_ADDRESS/api/v2/devices/$1/backups" > /dev/null
}
Shell function for Create new backup API

The functions above will be used in the main function - processFiles.

processFiles function does the following:

  • finds sub-directories inside provided ftp folder
  • retrieves device ID, creates new device in Unimus if none is found
  • sorts files inside each sub-directory by modification date from oldest to newest
  • converts each file to base64 string
  • checks if file is binary or text, creates binary/text backup accordingly and deletes the file
function processFiles() {
    # Set script directory for the script
    script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
    cd $script_dir

    # Creating a log file
    log="$script_dir/unbackupables.log"
    printf 'Log File - ' >> $log
    date +"%F %H:%M:%S" >> $log

    # Insecure curl switch
    if $SELF_SIGNED_CERT; then
        insecure="-k"
    fi

    # Perform Unimus health check
    status=$(healthCheck)
    errorCheck "$?" 'Status check failed'

    if [ $status == 'OK' ]; then
        [ -n "$ZONE" ] && zoneCheck
        echoGreen 'Checks OK. Script starting...'
        ftp_directory="$1"
        # Begin sweeping through the specified FTP directory
        for subdir in "$ftp_directory"/*; do
            if [ -d "$subdir" ]; then
                # Interpret directory names as device addresses/host names in Unimus
                address=$(basename "$subdir")
                # Check if device already exists in Unimus
                id=$(getDeviceId "$address")
                if [ $id = "null" ]; then
                    if $CREATE_DEVICES; then
                        createNewDevice "$address" && id=$(getDeviceId "$address") && echoGreen "New device added. Address: $address, id: $id"
                    fi
                fi
                if [ $id = "null" ] || [ -z "$id" ]; then
                    echoYellow "Device $address not found on Unimus. Consider enabling creating devices. Continuing with next device."
                else
                    for file in $(ls -t "$subdir"); do
                        if [ -f "$subdir/$file" ]; then
                            isTextFile=$(file -b "$subdir/$file")
                            if [[ $isTextFile == *"text"* ]]; then
                                bkp_type="TEXT"
                            else
                                bkp_type="BINARY"
                            fi
                            encoded_backup=$(base64 -w 0 "$subdir/$file")
                            temp_json_file=$(mktemp)

                            cat <<-EOF > "$temp_json_file"
                            {
                            "backup": "$encoded_backup",
                            "type": "$bkp_type"
                            }
EOF
                            # Use jq to process the JSON from the temporary file
                            jq '.' "$temp_json_file" > output.json
                            createBackup "$id" "output.json" && echoGreen "Pushed $bkp_type backup for device $address from file $file"
                            # Clean up the temporary files & backup file
                            rm "$temp_json_file" output.json "$subdir/$file"
                        fi
                    done
                fi
            fi
        done
    else
        if [ -z $status ]; then
            echoRed 'Unable to connect to unimus server'
            exit 2
        else
            echoRed "Unimus server status: $status"
        fi
    fi
    echoGreen 'Script finished'
}
Main shell function

Function is called supplying ftp root folder defined in the beginning:

processFiles $FTP_FOLDER

As we mentioned earlier, you can get the script in its wholesomeness on GitHub.


Powershell script

Corresponding code walkthrough for Windows environments using Powershell can be found in this section. Grab the complete script from GitHub or continue perusing if you're interested in how it works under the hood!

The only part that we need to update for a specific setup is the mandatory variables. Required are Unimus hostname/IP address, API token generated in Unimus and the location of FTP root directory.

# Mandatory parameters
$UNIMUS_ADDRESS = "<http(s)://unimus.server.address(:port)>"
$TOKEN = "<api token>"
# FTP root directory
$FTP_FOLDER = "/ftp_data"
Mandatory variables

Optionally we can specify the Zone we want to work on. This works from 2.4.0-Beta3. Next we can enable skipping certification check for when you are using self-signed certificate on Unimus. The script handles this in a separate fashion based on the version of Powershell present on the system. Also, variable $CREATE_DEVICES controls adding devices to Unimus if they're not present already.

# Optional parameters
# Specifies the Zone where devices will be searched for by address/hostname
# CASE SENSITIVE; leave commented to use the Default (0) zone
#$ZONE="0"
# Insecure mode
# If you are using self-signed certificates you might want to set this to true
$INSECURE = $false
# Variable for enabling creation of new devices in Unimus; set to true to enable
$CREATE_DEVICES = $false
# Specify description of new devices created in Unimus by the script
$CREATED_DESC = "Unbackupable"
Optional variables

Create new device and Get device by address API endpoints are accessed using functions Create-NewDevice and Get-DeviceId. Device IP address ($address) is used as a parameter for both functions. Get-DeviceId returns .data.id value (device ID) from API response. Get-DeviceId is also handling response exceptions by returning null when a device doesn't exist on Unimus. When working on non-default Zone Create-NewDevice adds zoneId key-value pair to its data payload and Get-DeviceId appends zoneId to the URI. Full API documentation can be found on wiki.

function Create-NewDevice {
    param(
        [string]$address
    )

    $body = @{
        address = $address
        description = $CREATED_DESC
    }

    if ($ZONE) {
        $body["zoneId"] = $ZONE
    }

    $body = $body | ConvertTo-Json

    $headers = @{
        "Accept" = "application/json"
        "Content-Type" = "application/json"
        "Authorization" = "Bearer $TOKEN"
    }

    if ($INSECURE -and $psMajorVersion -ge 6) {
        Invoke-RestMethod -SkipCertificateCheck -Uri "$UNIMUS_ADDRESS/api/v2/devices" -Method POST -Headers $headers -Body $body | Out-Null
    } else {
        Invoke-RestMethod -Uri "$UNIMUS_ADDRESS/api/v2/devices" -Method POST -Headers $headers -Body $body | Out-Null
    }
}
Powershell function for Create new device API
function Get-DeviceId {
    param(
        [string]$address
    )

    $headers = @{
        "Accept" = "application/json"
        "Authorization" = "Bearer $TOKEN"
    }

    if ($ZONE) {
        $uri="api/v2/devices/findByAddress/" + $address + "?zoneId=" + $ZONE
    } else {
        $uri="api/v2/devices/findByAddress/" + $address
    }

    try {
        if ($INSECURE -and $psMajorVersion -ge 6) {
            $response = Invoke-RestMethod -SkipCertificateCheck -Uri "$UNIMUS_ADDRESS/$uri" -Method GET -Headers $headers
        } else {
            $response = Invoke-RestMethod -Uri "$UNIMUS_ADDRESS/$uri" -Method GET -Headers $headers
        }
        return $response.data.id
    }
    catch {
        if ($_.Exception.Response.StatusCode -eq 404) {
            return "null"
        }
    }
}
Powershell function for Get device by address API

Create new backup API is called through Create-Backup function. It is using device ID($id), base64 string of the backup file ($encodedBackup) and TEXT/BINARY ($type) as input.

function Create-Backup {
    param(
        [string]$id,
        [string]$encodedBackup,
        [string]$type
    )

    $body = @{
        backup = $encodedBackup
        type = $type
    } | ConvertTo-Json

    $headers = @{
        "Accept" = "application/json"
        "Content-Type" = "application/json"
        "Authorization" = "Bearer $TOKEN"
    }
    if ($INSECURE -and $psMajorVersion -ge 6) {
        Invoke-RestMethod -SkipCertificateCheck -Uri "$UNIMUS_ADDRESS/api/v2/devices/$id/backups" -Method POST -Headers $headers -Body $body | Out-Null
    } else {
        Invoke-RestMethod -Uri "$UNIMUS_ADDRESS/api/v2/devices/$id/backups" -Method POST -Headers $headers -Body $body | Out-Null
    }
}
Powershell function for Create new backup API

The main function Process-Files executes the following logic:

  • finds subdirectories inside provided ftp folder
  • retrieves device ID, creates new device in Unimus if none is found
  • sorts files inside each subdirectory by modification date from oldest to newest
  • converts each file to base64 string
  • checks if file is binary or text, creates binary/text backup accordingly and deletes the file
function Process-Files {
    param(
        [string]$directory
    )

    $log = Join-Path $PSScriptRoot "unbackupablesPS.log"
    $logMessage = "Log File - " + (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
    Add-Content -Path $log -Value $logMessage
    #Health check
    $status = Health-Check

    if ($status -eq 'OK') {
        if ($ZONE) {
            Zone-Check
        }

        Print-Green "Checks OK. Script starting..."
        $ftpSubdirs = Get-ChildItem -Path $directory -Directory

        foreach ($subdir in $ftpSubdirs) {
            $address = $subdir.Name
            # Check if device already exists in Unimus
            $id = "null"; $id = Get-DeviceId $address

            if ($id -eq "null" -and $CREATE_DEVICES) {
                Create-NewDevice $address
                $id = Get-DeviceId $address
                Print-Green ("New device added. Address: " + $address + ", id: " + $id)
            }

            if ($id -eq "null" -or $id -eq $null) {
                Print-Yellow ("Device " + $address + " not found on Unimus. Consider enabling creating devices. Continuing with next device.")
            } else {
                $files = Get-ChildItem -Path $subdir.FullName | Sort-Object -Property LastWriteTime -Descending

                foreach ($file in $files) {
                    if ($file.GetType() -eq [System.IO.FileInfo]) {
                        $content = [System.IO.File]::ReadAllBytes($file.Fullname)
                        $encodedBackup = [System.Convert]::ToBase64String($content)

                        if ($content -contains 0) {
                            $bkp_type = "BINARY"
                        } else {
                            $bkp_type = "TEXT"
                        }
                        Create-Backup $id $encodedBackup $bkp_type
                        Print-Green ("Pushed " + $bkp_type + " backup for device " + $address + " from file " + $($file.Name))
                        Remove-Item $file.FullName
                    }
                }
            }
        }
    } else {
        Print-Red "Unimus server status: $status"
    }
    Print-Green "Script finished."
}
Main powershell function

To run, call the main function via:

Process-Files -directory $FTPFOLDER

That's all she wrote! Here's a GitHub link with the full version of the script.

Scheduling with Cron

This section takes care of the automation part of the whole config push process. On Linux we can run our script at a predefined time using Cron. Add user-specific job via crontab -e and insert the following line:

0 4 * * * /path/to/your_Unbackupables_script.sh
Crontab entry to run the script at 4:00am every day

Task Scheduler

All you gamer folk working in Windows Server environment can use Task Scheduler to run the Powershell version of the script. These are the steps needed:

1) Under Actions hit 'Create task' and provide a name and description.

Task scheduler menu for creating a task

2) Switch to Triggers tab, create a 'New' trigger and specify the start time and frequency of your choice.

Task scheduler menu for adding new trigger

3) In Actions tab hit 'New'. Select 'Start a program' as action, browse for PowerShell executable and add an argument by supplying the path to the Powershell script.

Task scheduler menu for creating an action

After confirming we can see our new task 'push backups to Unimus' in Task Scheduler Library. It is now ready to automatically run according to our specified schedule.

Task scheduler menu for running tasks

Backup push aftermath in Unimus

After the script runs we should see new backups created in Unimus:

Unimus GUI displaying text backups
API created TEXT backups
Unimus GUI displaying binary backups
API created BINARY backups

Note that new device backups may show almost identical timestamps. This could happen when the script makes multiple API calls due to a collection of config files accumulated inside a single device folder on the FTP server. Likely scenario is an initial script execution when we start with a considerable backup history that we want pushed to Unimus from FTP. Or when the script is not executed periodically and configs on FTP stack up. If your backup push script runs with (slightly after) your 'copy backup to FTP' schedule, this should not happen. Even if it does the backups on Unimus would be created with preserved chronology thanks to the script's internal logic.

T-shooting

Things sometimes work out on the first try. More often tho, they don't. If your case is "they don't", keep on reading.

There are a few possible sources of headache - FTP directory structure, Unimus API, working Zone selection and using self-signed certificate being the likely candidates. Let's have a closer look at each.

Directory structure nerve wreckers

Successful implementation of the backup handling in our scripts heavily relies on directory structure of your FTP server. Or on how robust the script is (we did our best). In our setup each managed device has its own folder named after device IP address or host name. Each folder in turn contains backup file versions for a given device. Make sure your setup keeps this structure.

API mishaps

There should not be any but if issues with API calls arise one can check the following.

Every API request includes Authorization header that supplies 'API token', in order to authorize the request, following this scheme:

Authorization: Bearer <token>

To create a token log in to your Unimus instance and navigate to User management > API tokens. Make sure you copy the whole string to the mandatory variable 'TOKEN' value. Otherwise you might get the following error:

ERROR: Unimus server status: null

The script is designed to push backups to a specific Zone in Unimus, default one if no Zone is specified. If the specified Zone does not exist you will get an error:

ERROR: Error. Zone <ZoneID> not found!

Just make sure to specify an existing Zone ID. Zone ID is case sensitive.

Self-signed cert predicaments

If you encounter errors with the script it pays to double-check optional parameters. Self-signed certificate on Unimus might give you SSL related errors if issued by a certificate authority unknown/untrusted to the system you're executing the script from. Enabling 'certification check skip' in the script is an option to consider.

Final words

We hope you will find this tutorial helpful for integrating configuration backups to Unimus from devices that do not support backups from CLI. We appreciate any feedback, questions or possible improvements and have a thread on our Forum for just that purpose.

←→