Version 4 of freezeconfig

Updated 2007-11-23 23:15:40 by MJ

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 seperate 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.

enter categories here