freezeconfig

MJ:

When monitoring customer systems or safeguarding system configurations before doing an upgrade, it is very useful to get the current config files and outputs of certain commands (for instance show running-config on routers). I previously used a script written in Ruby for this, but that isn't easily distributed. Hence, I have ported the automated config file download to Tcl now. The result of local commands will be ported using Expect.

The script is driven by an XML config file that defines a number of hosts and their types. And the actions that have to be performed for each type. For example:

<freezeconfig name="Platform A">
    <host name="host">
        <user name="user" pass="pass" /> 
        <user name="mail" pass="pass" /> 
        <type name="unix" /> 
        <type name="mail" />  
    </host>
    <type name="unix">
        <session user="user">
            <protocol name="ftp">
                <dir filter="hosts">/etc</dir> 
                <dir filter="\.emacs">~</dir> 
            </protocol>
        </session>
    </type>
    <type name="mail">
        <session user="mail">
            <protocol name="ftp">
                <dir filter="sendmail\.cf">/etc</dir> 
            </protocol>
        </session>
    </type>
</freezeconfig>

The script

package require Tcl 8.5
package require tdom
package require vfs::ftp

variable xmlfile
variable hostpattern
variable errors

if {$argc < 1 || $argc > 2} {
    puts stderr "usage: freezeconfig xml-file ?host-pattern?"
    exit 1
}

proc set_params {file {pattern .*}} {
    variable xmlfile
    variable hostpattern
    set xmlfile $file
    set hostpattern $pattern
}

proc parse_file {xmlfile} {
    set f [open $xmlfile]
    set dom [dom parse -channel $f]
    set doc [$dom documentElement]
    close $f
    return $doc
}

proc do_type {hostnode hostname address type} {
    # get the type info
    puts "Executing type $type"
    set typeInfo [$hostnode selectNodes {../type[@name=$type]}]
    if {$typeInfo eq {}} {
        error "type not defined"
    }
    set sessionnodes [$typeInfo selectNodes session]
    foreach sessionnode $sessionnodes {
        set user [$sessionnode getAttribute user {}]
        set currentUserNode [$hostnode selectNodes {user[@name=$user]}]
        if {$currentUserNode eq {}} {
            error "user $user not defined for $hostname"
        }
        puts "Starting session for user: $user..."
        set pass [$currentUserNode getAttribute pass {}]
        foreach protocolnode [$sessionnode selectNodes protocol] {
            do_protocol $protocolnode $hostname $user $pass
        }
    }
}

proc do_protocol {node host user pass} {
    set protocol [$node getAttribute name]
    variable errors
    puts "Protocol $protocol"
    protocol::${protocol}::do $node $host $user $pass
}

namespace eval protocol {
    namespace eval ftp {
    }
}

# different protocol handlers
proc ::protocol::ftp::do {node host user pass} {
    file mkdir $host
    foreach dirNode [$node selectNodes dir] {
        set dir [$dirNode asText]
        file mkdir ./$host/$dir
        set filter [$dirNode getAttribute filter {^.*$}]
        set fd [vfs::ftp::Mount $user:$pass@$host/$dir _ftp]
        puts "copying files from '$dir' matching '$filter'"
        foreach file [glob -tails -directory ./_ftp *] {
            if {[regexp -- $filter $file]} {
            puts "...$file"
            file copy ./_ftp/$file ./$host/$dir
            }
        }
        vfs::ftp::Unmount $fd _ftp
    }
}


# main program

set_params {*}$argv

if {[catch {parse_file $xmlfile} doc]} {
    puts stderr "parsing failed: $doc"
    exit 1
}

set name  [$doc getAttribute name unknown]
set timestamp [clock format [clock seconds] -timezone UTC -format %Y%m%d-%H%M%S]
set line [string repeat - 80]
puts $line
puts $line
puts "Freezing $name ($timestamp)"
puts $line

file mkdir $name
cd $name
file mkdir $timestamp
cd $timestamp

set errors {}
foreach hostnode [$doc selectNodes host] {
    set hostname [$hostnode getAttribute name]
    if {[regexp -- $hostpattern $hostname]} {
        set address [$hostnode getAttribute address $hostname]
        puts "freezing $hostname ($address)..."
        foreach typenode [$hostnode selectNodes type] {
            set type [$typenode getAttribute name]
            if {[catch {do_type $hostnode $hostname $address $type} res]} {
            lappend errors [list $hostname $type $res]
            }
        }
    } else {
        puts "skipping $host..."
    }
}

puts "Freezing failed for:"
puts $line
puts [format "| %-15s| %-10s| %s" Host Type Error]
puts $line
foreach error $errors {
    puts [format "| %-15s| %-10s| %s" {*}$error]
}

set timestamp [clock format [clock seconds] -timezone UTC -format %Y%m%d-%H%M%S]
puts "Freezing $name finished ($timestamp)"

Design remarks

  • The timestamp for the stored data uses UTC: When doing forensics on when a change occured, you want to be able to compare different freezes from different people (who are geographically separated) in time. One way to do this is to use [clock seconds] but this is not easily parsed by humans. Instead a date format that sorts chronologically is used at the UTC timezone.
  • Using variable instead of global: this is done to make a possible conversion to a package in a separate namespace as painless as possible.
  • Using Tcl: the reason for porting from Ruby to Tcl is twofold. I am really out of practice with Ruby and Tcl makes it much easier to distribute the script without requiring a local programming language installation thanks to starpacks.