#!/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!"