#!/usr/bin/expect --
# >>>> Backup configuration on a PIX/ASA v7.2, v8.2 or FWSM 3.1
# (May work on other versions)
#
# Copyright (C) 2010-2012, Peter Rathlev (peter.rathlev@stab.rm.dk)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# Things that are considered rather specific to Region Midtjylland are
# marked with the following tag:
# #===RMNET===
# Defaults
set WRITE_MEM 1
set WRITE_MEM_GRACE 7200
set WRITE_NET 1
set FORCE_WRITE_NET 0
set VERBOSE 1
set TARGET ""
set DRY_RUN 0
set CONFIG_READ_METHOD "more"
set SYS_BACKUP 0
set MAX_MATCH 5000000
set MIN_CONFIGURATION_LENGTH 100
set PAGER_LINES 24
set TFTP_ROOT "/tftpboot"
set TFTP_ARCHIVE "$TFTP_ROOT/archive"
# Login credentials
set BACKUP_USER "some_user"
set BACKUP_PWD "some_password"
set ENABLE_PWD "enable_password"
proc display_usage {} {
global WRITE_MEM_GRACE
send_user "\n"
send_user "Options:\n"
send_user "\n"
send_user " -q|--quiet Display only errors\n"
send_user " -d|--debug Display verbose output\n"
send_user " -w|--write-mem Commit configuration to NVRAM (default)\n"
send_user " -W|--no-write-mem Don't commit configuration to NVRAM\n"
send_user " -n|--write-net Make a network backup (default)\n"
send_user " -N|--no-write-net Don't make a network backup\n"
send_user " --sys-backup Change to the \"sys\" context on a multi-context\n"
send_user " firewall and act on this (only from \"admin\" context)\n"
send_user " --force-write-net Make a network backup even if there are users logged\n"
send_user " in and/or a current backup already exists\n"
send_user " --dry-run Don't actually do anything\n"
send_user " --write-mem-grace Don't commit configuration to NVRAM if last change was\n"
send_user " within seconds (default: $WRITE_MEM_GRACE)\n"
send_user "\n"
exit 1
}
set i 0
while { $i < $argc } {
switch -regexp -- [lindex $argv $i] {
"^(-q|--quiet)$" { set VERBOSE 0 }
"^(-d|--debug)$" { incr VERBOSE }
"^(-w|--write-mem)$" { set WRITE_MEM 1 }
"^(-W|--no-write-mem)$" { set WRITE_MEM 0 }
"^(-n|--write-net)$" { set WRITE_NET 1 }
"^(-N|--no-write-net)$" { set WRITE_NET 0 }
"^--sys-backup$" { set SYS_BACKUP 1 }
"^--force-write-net$" { set FORCE_WRITE_NET 1 }
"^--dry-run$" { set DRY_RUN 1 }
"^--write-mem-grace$" { set WRITE_MEM_GRACE [lindex $argv [incr i]] }
"^-" { send_user "Unknown option '[lindex $argv $i]'\n"; display_usage }
"" { set TARGET [string tolower [lindex $argv $i]]; break }
}
incr i
}
if { $TARGET eq "" } {
send_user "Missing target?\n"
exit 1
}
if { $WRITE_MEM == 0 && $WRITE_NET == 0 } {
send_user "Nothing to do\n"
exit 0
}
array set MONTH_NUMBER {
Jan "01"
Feb "02"
Mar "03"
Apr "04"
May "05"
Jun "06"
Jul "07"
Aug "08"
Sep "09"
Oct "10"
Nov "11"
Dec "12"
}
# TIME_RE: HH:MM:SS.mmm TZ
# where TZ is one or more characters
set TIME_RE "(\[0-9\]\[0-9\]):(\[0-9\]\[0-9\]):(\[0-9\]\[0-9\])\.\[0-9\]\[0-9\]\[0-9\] \[a-zA-Z\]+"
# DATE_RE: MON MDAY YYYY
# where MON is a three letter representation of the month (cf. MONTH_NUMBER array)
set DATE_RE "\[a-zA-Z\]\[a-zA-Z\]\[a-zA-Z\] (\[a-zA-Z\]\[a-zA-Z\]\[a-zA-Z\]) (\[0-9\]?\[0-9\]) (\[0-9\]\[0-9\]\[0-9\]\[0-9\])"
set CLOCK_RE "($TIME_RE $DATE_RE)"
proc convert_time {timestring} {
global CLOCK_RE
global MONTH_NUMBER
regexp -nocase $CLOCK_RE $timestring dummy dummy t_hour t_min t_sec t_mon t_mday t_year
set t_mon $MONTH_NUMBER($t_mon)
return [clock scan "$t_year-$t_mon-$t_mday $t_hour:$t_min:$t_sec"]
}
proc w_error {text} {
global TARGET
send_user "$TARGET : ERROR: $text\n"
}
proc w_status {text} {
global VERBOSE
global TARGET
if { $VERBOSE > 0 } {
send_user "$TARGET : $text\n"
}
}
proc w_verbose {text} {
global VERBOSE
global TARGET
if { $VERBOSE > 1 } {
send_user "$TARGET : DEBUG: $text\n"
}
}
proc reset_pager_lines_and_exit {} {
global UNIT_PROMPT
global PAGER_LINES
global TARGET
# Disable paging
w_verbose "Resetting pager lines to $PAGER_LINES"
send -- "terminal pager lines $PAGER_LINES\r"
expect -exact "$UNIT_PROMPT# "
# Exit from host
send -- "exit\r"
expect -- "Connection to $TARGET closed."
}
if { $VERBOSE > 2 } {
log_user 1
} else {
log_user 0
}
set timeout 30
# Connect to target and login
w_status "Connecting..."
spawn "ssh" "$BACKUP_USER@$TARGET"
match_max $MAX_MATCH
expect -exact "$BACKUP_USER@$TARGET's password: "
send -- "$BACKUP_PWD\r"
set RETRIES_LEFT 2
set UNIT_PROMPT ""
expect {
-re "(\[^>\r\n\]+)> " {
# Catch the prompt
set UNIT_PROMPT $expect_out(1,string)
}
-exact "Permission denied" {
incr RETRIES_LEFT -1
if { $RETRIES_LEFT == 0 } {
w_error "Permission denied trying to login"
exit 1
}
sleep 5
}
timeout {
incr RETRIES_LEFT -1
if { $RETRIES_LEFT == 0 } {
w_error "Timeout trying to login"
exit 1
}
sleep 5
exp_continue
}
}
if { $UNIT_PROMPT eq "" } {
w_error "Didn't catch unit prompt, problems logging in?"
exit 1
}
w_verbose "Unit prompt is '$UNIT_PROMPT'"
# Enter enabled mode
send -- "enable\r"
expect {
-re "(\[^\r\n\]+)\[\r\n\]+$UNIT_PROMPT> " { w_error "Can't issue enable command! ('$expect_out(1,string)')"; exit 1 }
-exact "Password: "
}
send -- "$ENABLE_PWD\r"
expect {
-exact "Password: " { w_error "Can't enable! (wrong enable password?)"; exit 1 }
-exact "$UNIT_PROMPT> " { w_error "Can't enable! ('$expect_out(buffer)')"; exit 1 }
-exact "$UNIT_PROMPT# "
}
# Disable paging temporarily
send -- "show pager\r"
expect {
-re "pager lines (\[0-9\]+)" {
set PAGER_LINES $expect_out(1,string)
w_verbose "Pager lines is currently $PAGER_LINES"
exp_continue
}
-exact "$UNIT_PROMPT# "
}
w_verbose "Setting terminal pager lines to 0"
send -- "terminal pager lines 0\r"
expect -exact "$UNIT_PROMPT# "
# Find out if anybody is connected at the moment
w_verbose "Looking for existing SSH sessions"
set SESSION_USER ""
set EXISTING_SESSIONS 0
send -- "show ssh sessions\r"
# Assume " "
expect {
-re "\n\[0-9\]+ +\[0-9.\]+ +2.0 +IN +\[^ \]+ \[^ \]+ +SessionStarted +(\[^ \r\n\]+)" {
set SESSION_USER $expect_out(1,string)
if { $SESSION_USER ne $BACKUP_USER } {
w_status "User \"$SESSION_USER\" already logged in"
incr EXISTING_SESSIONS 1
exp_continue
} else {
exp_continue
}
}
-exact "$UNIT_PROMPT# "
}
if { $EXISTING_SESSIONS > 0 } {
if { $FORCE_WRITE_NET == 1 } {
w_status "$EXISTING_SESSIONS user(s) already logged in; --force-write-net specified, continuing"
} else {
w_status "$EXISTING_SESSIONS user(s) already logged in, exiting (use --force-write-net to override)"
reset_pager_lines_and_exit
exit 255
}
}
# If it's a "sys-backup" (i.e. backup of the system context in a multiple context FWSM)
# we must "changeto system" first
if { $SYS_BACKUP == 1 } {
send -- "changeto system\r"
expect {
-exact "ERROR:" {
w_error "Unable to change to system space (permission problem?)"
reset_pager_lines_and_exit
exit 1
}
-exact "Command not valid in current execution space" {
w_error "Unable to change to system space, must be connected to the admin context"
reset_pager_lines_and_exit
exit 1
}
-exact "Command authorization failed" {
w_error "Unable to change to system space, authorization failed"
reset_pager_lines_and_exit
exit 1
}
-exact "$UNIT_PROMPT# " {
# Still in the same context, so we failed
w_error "Unable to change to system space, unknown problem"
reset_pager_lines_and_exit
exit 1
}
-re ".# " {
# Test if we were really successful
send -- "show version | include <.*>\r"
expect {
-exact "" {
# Still in some context
w_error "Unable to change to system space"
reset_pager_lines_and_exit
exit 1
}
-re "(\[^>\r\n\]+)# " {
w_verbose "Successfully changed to system space"
# Catch new prompt
set UNIT_PROMPT $expect_out(1,string)
w_verbose "New prompt is '$UNIT_PROMPT'"
# Adjust target
set expect_out(buffer) ""
send -- "show hostname\r"
expect -re "show hostname\[\r\n\]*(\[^\r\n\]+)\[\r\n\]*$UNIT_PROMPT# "
set TARGET $expect_out(1,string)
w_verbose "Target is now '$TARGET'"
}
}
}
}
}
# Find out when and by who the unit was last configured
w_verbose "Finding time of last configuration change"
set CONFIGURED_USER ""
set CONFIGURED_WHEN ""
send -- "show version | incl ^Configuration last modified by \r"
expect {
-re "Configuration last modified by (\[a-zA-Z0-9_\]+) at (\[^\r\n\]+)" {
set CONFIGURED_USER $expect_out(1,string)
set CONFIGURED_WHEN $expect_out(2,string)
exp_continue
}
-exact "$UNIT_PROMPT# "
}
if { $CONFIGURED_WHEN ne "" } {
w_verbose "Last configuration change: $CONFIGURED_WHEN by $CONFIGURED_USER"
} else {
w_verbose "No configuration change since reboot"
}
# Find out current time on unit
w_verbose "Finding current unit time"
set UNIT_TIME ""
send -- "show clock\r"
expect {
-re $CLOCK_RE {
set UNIT_TIME $expect_out(1,string)
exp_continue
}
-exact "$UNIT_PROMPT# "
}
w_verbose "Current unit time is $UNIT_TIME"
# Find out current configuration cryptochecksum
w_verbose "Finding current configuration cryptochecksum"
set CRYPTO_CHECKSUM ""
send -- "show checksum\r"
expect {
-re "Cryptochecksum: (........ ........ ........ ........) *\r" {
set CRYPTO_CHECKSUM $expect_out(1,string)
regsub -all { } $CRYPTO_CHECKSUM {} CRYPTO_CHECKSUM
exp_continue
}
-exact "$UNIT_PROMPT# "
}
if { $CRYPTO_CHECKSUM eq "" } {
w_status "Can't find current cryptochecksum!"
reset_pager_lines_and_exit
exit 1
}
w_verbose "Current configuration cryptochecksum: '$CRYPTO_CHECKSUM'"
if { $WRITE_MEM == 1 && $CONFIGURED_WHEN ne "" } {
# Find out time of last commit to non-volatile storage
w_verbose "Finding time of last configuration commit to non-volatile storage"
set WRITTEN_USER ""
set WRITTEN_WHEN ""
send -- "show configuration | incl ^: Written \r"
expect {
-re ": Written by (\[a-zA-Z0-9_\]+) at (\[^\r\n\]+)" {
set WRITTEN_USER $expect_out(1,string)
set WRITTEN_WHEN $expect_out(2,string)
exp_continue
}
-exact "$UNIT_PROMPT# "
}
w_verbose "Last written $WRITTEN_WHEN by $WRITTEN_USER"
# Check to see if unit was configured after last write
if { [convert_time $CONFIGURED_WHEN] > [convert_time $WRITTEN_WHEN] } {
w_status "Configuration has changed since last NVRAM commit"
# If more than WRITE_MEM_GRACE seconds have passed since last configuration, write to memory
if { [expr [convert_time $UNIT_TIME] - [convert_time $CONFIGURED_WHEN]] > $WRITE_MEM_GRACE } {
if { $DRY_RUN == 0 } {
w_status "Issuing 'write memory'..."
send -- "write memory\r"
set WRITE_MEM_SUCCESS 0
set LAST_ERROR ""
expect {
-exact "\[OK\]" { set WRITE_MEM_SUCCESS 1; exp_continue }
-re "ERROR: (\[^\r\n\]+)" { set LAST_ERROR $expect_out(1,string); exp_continue }
-exact "$UNIT_PROMPT# "
}
if { $WRITE_MEM_SUCCESS == 1 } {
w_status "'write memory' was successful!"
} else {
w_error "Error writing to memory! (Error was: '$LAST_ERROR')"
}
} else {
w_status "Dry run, doing nothing"
}
} else {
w_status "Still within grace period for NVRAM commit ($WRITE_MEM_GRACE seconds, [expr $WRITE_MEM_GRACE - [convert_time $UNIT_TIME] + [convert_time $CONFIGURED_WHEN]] left)"
}
} else {
w_status "Configuration has not changed since last commit to non-volatile storage"
}
}
if { $WRITE_NET == 1 } {
# Compare "Cryptochecksum:" from "CURRENT" backup with the running
# where TARGET_UNQUALIFIED is TARGET with anything from first "." and on removed
regexp "\[^.\]+" $TARGET TARGET_UNQUALIFIED
set DO_WRITE_NET 0
if { $FORCE_WRITE_NET == 1 } {
w_status "Forcing network backup (--force-write-net specified)"
set DO_WRITE_NET 1
} else {
#===RMNET===
# This looks for a local copy to see if the current backup is up to date
# Just replace this else-clause with "set DO_WRITE_NET 1" to unconditionally
# perform the backup always.
w_verbose "Finding checksum of last network backup"
set FOUND_LOCAL_COPY [catch {exec "grep" "-m" "1" "^Cryptochecksum: \\?" "$TFTP_ARCHIVE/$TARGET_UNQUALIFIED/CURRENT" "2>/dev/null"} NET_BACKUP_CHECKSUM_LINE]
if { $FOUND_LOCAL_COPY != 0 } {
w_status "No network backup found"
set DO_WRITE_NET 1
} else {
regexp "^Cryptochecksum: ?(........ ?........ ?........ ?........) *" $NET_BACKUP_CHECKSUM_LINE -> NET_BACKUP_CHECKSUM
regsub -all { } $NET_BACKUP_CHECKSUM {} NET_BACKUP_CHECKSUM
w_verbose "Network backup cryptochecksum: '$NET_BACKUP_CHECKSUM'"
if { $CRYPTO_CHECKSUM eq $NET_BACKUP_CHECKSUM } {
w_status "Network backup is up to date"
} else {
w_status "Network backup is not up to date"
set DO_WRITE_NET 1
}
}
}
if { $DO_WRITE_NET == 1 } {
if { $DRY_RUN == 0 } {
# PIX/ASA/FWSM 'write net' doesn't include information about who configured and when
# so we simulate a 'write net' by storing a modified 'show run' in /tftproot
w_status "Backing up configuration..."
set expect_out(buffer) ""
set RETRIES 0
set CONFIGURATION ""
set CONFIGURATION_LENGTH 0
while { $CONFIGURATION_LENGTH == 0 && $RETRIES <= 2 } {
send -- "terminal pager lines 0\r"
expect -exact "$UNIT_PROMPT# "
set CONFIGURATION ""
if { $CONFIG_READ_METHOD eq "more" } {
w_status "... using 'more system:running-config'"
send -- "more system:running-config\r"
expect {
-re "ERROR: % Invalid input detected" {
w_status "Unable to use \"more system:running-config\", falling back to \"show running-config\""
set expect_out(buffer) ""
set CONFIG_READ_METHOD "show"
}
-re "more system:running-config(.*)$UNIT_PROMPT# " {
set CONFIGURATION [string map {"\r\n" "\n"} $expect_out(1,string)]
}
}
}
if { $CONFIG_READ_METHOD eq "show" } {
w_status "... using 'show running-config'"
send -- "show running-config\r"
expect {
-re "show running-config(.*)$UNIT_PROMPT# " {
set CONFIGURATION [string map {"\r\n" "\n"} $expect_out(1,string)]
}
}
}
set CONFIGURATION_LENGTH [string length $CONFIGURATION]
if { [ string match "*Configuration update in progress by another process*" $CONFIGURATION ] == 1 } {
w_status "Configuration update conflict detected, pausing for 15 seconds"
sleep 15
set expect_out(buffer) ""
set CONFIGURATION ""
set CONFIGURATION_LENGTH 0
}
if { $CONFIGURATION_LENGTH == 0 } {
set RETRIES [ expr $RETRIES + 1 ]
}
}
w_status "... done retrieving configuration, $CONFIGURATION_LENGTH characters"
if { $CONFIGURATION_LENGTH >= $MAX_MATCH } {
w_error "Configuration length close to \$MAX_MATCH ($MAX_MATCH), probably too big!"
}
if { $CONFIGURATION_LENGTH <= $MIN_CONFIGURATION_LENGTH } {
w_error "Configuration length ($CONFIGURATION_LENGTH) smaller than minimum ($MIN_CONFIGURATION_LENGTH), try to adjust \$MAX_MATCH or maybe \$MIN_CONFIGURATION_LENGTH"
send_user "Expect buffer:\n$expect_out(buffer)\n"
reset_pager_lines_and_exit
exit 1
}
# Insert standard IOS "Last configuration change"-header
# For this we need the configuration change time without milliseconds
regsub {\.[0-9]+} $CONFIGURED_WHEN "" LM_TIME
set LAST_MODIFIED_HEADER "! Last configuration change at $LM_TIME by $CONFIGURED_USER"
if { $DRY_RUN == 0 } {
#===RMNET===
# This could write to a file or maybe standard out instead
# Store this
set FNAME [exec "mktemp" "$TFTP_ROOT/LOCKED.backup-$BACKUP_USER-$TARGET-XXXXXXXX"]
set FID [open $FNAME "w"]
puts $FID $LAST_MODIFIED_HEADER
puts $FID $CONFIGURATION
close $FID
# Change ownership & permissions
exec {chown} "root:apache" $FNAME
exec {chmod} "0640" $FNAME
# Rename file
regsub {^$TFTP_ROOT/LOCKED\.} $FNAME "$TFTP_ROOT/" NEW_NAME
exec {mv} $FNAME $NEW_NAME
w_status "Configuration backed up successfully!"
}
} else {
w_status "Dry run, doing nothing"
}
}
}
reset_pager_lines_and_exit
w_status "Great success!"