#!/usr/bin/env ruby $:.unshift File.join(File.expand_path(File.dirname(__FILE__)).untaint, '/..') # require 'io/console' # Ruby 1.9.3 or later require 'time' require 'thread' require 'optparse' require 'netutils' # PATH = '/home/conf/net/certs' CERT = "#{WEBAUTH_HOST}.cer" KEY = "#{WEBAUTH_HOST}.key" INT = 'nii-odca3sha2.cer' CERTFILES = [ CERT, KEY, INT ] # HTML = '/home/conf/net/html/wired' HTMLFILES = [ 'favicon.ico', 'login.html', 'loginNG.html', 'loginOK.html', 'loginProcess.html', 'logout.html', 'logoutNG.html', 'logoutOK.html', 'webauth.msg' ] HTMLDIR = 'html' # a directory on a switch for HTML files # def usage(errmsg = nil) progname = File.basename($0) STDERR.print "ERROR: #{errmsg}\n\n" if errmsg STDERR.print "\ Usage: #{progname} -h #{progname} [-c] [-d] [-f] [-w] [-r] -a #{progname} [-c] [-d] [-f] [-w] [-r] ... Description: deploy files to Alaxala switches, such as certificate files (i.e., certificate itself, private key, intermediate CA certificate), HTML files for Web authentication. If an option, ``-a,'' is given, deploy to all switches using LLDP or CDP. All files should be placed into the FTP server, #{FTP_SERVER}:#{PATH} (for certificate files), #{FTP_SERVER}:#{HTML} (for HTML files), in advance. Arguments: switch IP address: an IP address of a switch. Options: -h: output this help message. -c: deploy certificate files. -d: dry run (do not actually reboot). -f: force to consider all switches as requiring reboots. -w: deploy HTML files. -r: reload a switch if necessary (only for certificate files). -a: deploy to all switches. Example: output information: #{progname} 192.168.0.1 #{progname} -a installing certificate files: #{progname} -c 192.168.0.1 #{progname} -c -a installing certificate and HTML files: #{progname} -c -w 192.168.0.1 #{progname} -c -w -a reload switches if and only if necessary (only for certificate files): #{progname} -r 192.168.0.1 #{progname} -r -a " exit 1 end ## ROOT_DEPTH = 0 class Vertices class Vertex attr_reader :sw, :depth def initialize(sw) @sw = sw depth_compute end def depth @sw.note end def depth_compute return if ! @sw.note.nil? depth = 0 nparent = @sw.parent loop do parent = nparent if parent.nil? break end nparent = parent.parent if ! parent.note.nil? depth += parent.note + 1 break end end @sw.note = depth end end attr_reader :maxdepth def initialize @vertices = [ Queue.new ] @maxdepth = 0 @mutex = Thread::Mutex.new end def length(depth) @vertices[depth].length end def push(sw) v = Vertex.new(sw) @mutex.synchronize do if @vertices[v.depth].nil? @vertices[v.depth] = Queue.new end end @vertices[v.depth].push(v) if v.depth > @maxdepth @maxdepth = v.depth end return v end def pop(depth) @vertices[depth].pop(true) end end vertices = Vertices.new ## # inject() requires Ruby2.4... take a more operational way. options0 = ARGV.getopts( 'hcdfwra', 'help', 'certificate', 'dry', 'force', 'web', 'reload', 'all') options = {} options0.map { |k, v| options[k.to_sym] = v } usage if options[:h] if options[:a] if ARGV.length != 0 usage('Extra argument specified') end Switch.set_retrieve_all SWITCHES.each do |name, ia| Switch.new(name, Switch::Type::ROUTER, ia, ).note = 0 end elsif ARGV.length > 0 ARGV.each do |ia| Switch.new(nil, Switch::Type::ROUTER, ia).note = 0 end else usage('No IP address is given.') end ## user = nil password = nil if options[:c] || options[:w] print 'Input FTP user name: ' user = STDIN.gets.strip print 'Input FTP password: ' password = STDIN.noecho(&:gets).strip end ## retrieve_start_time = Time.now Switch.retrieve do |sw| if sw.name print "Connecting: ``#{sw.name}'' (#{sw.ia})\n" else print "Connecting: #{sw.ia}\n" end sw.login msg = " Connected: ``#{sw.name}'' (#{sw.ia}) (#{sw.maker_to_s} " + "#{sw.product})" if sw.maker != CLI::Maker::ALAXALA puts "#{msg} SKIP non-Alaxala equipment." next end config = sw.config_get if config !~ /web-authentication system-auth-control/m puts "#{msg} SKIP no Web authentication configured." next end puts msg r = sw.cmd('show web-authentication ssl-crt') # SSL key : default now # SSL certificate : 2018/04/03 20:04:11 # SSL intermediate cert : 2018/04/03 20:04:11 sep = '' installedtime = nil msg = " #{sw.name} (#{sw.ia}): CERT:" r.scan(/SSL ([^:]*) : ([^\n]+)/) do |k, v| k.strip! v.strip! if v =~ /^default/ v = 'none' elsif installedtime === nil installedtime = Time.parse(v) end msg += "#{sep} #{k}: #{v}" sep = ',' end r = sw.cmd('show system') if r =~ /^.*Boot Date[^:]+: ([^\n]+).*$/ v = $1.strip boottime = Time.parse(v) msg += ", boot: #{v}" if boottime < installedtime || options[:c] || options[:f] v = vertices.push(sw) msg += ' (need reboot)' end end puts msg # r = sw.cmd('show web-authentication html-files') sep = '' msg = " #{sw.name} (#{sw.ia}): HTML:" r.scan(/^ +([0-9\/]+ [0-9:]+) + ([0-9,]+) ([^\n]+)/) do |date, size, f| date.strip! size.strip! f.strip! msg += "#{sep} #{f} #{size} (#{date})" sep = ',' end puts msg next if ! options[:c] && ! options[:w] oprompt = sw.prompt sw.cmd("ftp #{FTP_SERVER}", 'Name: ') sw.cmd(user, 'Password:') r = sw.cmd(password, "ftp> |#{oprompt}") if r =~ /530 Login incorrect./ raise('Invalid FTP password') end r = sw.cmd('passive') r += sw.cmd('bin') if options[:c] r += sw.cmd("cd #{PATH}") CERTFILES.each { |f| r += sw.cmd("get #{f}") } if r =~ /Failed/ raise('cannot get certificate files') end end htmlfiles = nil if options[:w] r = sw.cmd("cd #{HTML}") r += sw.cmd('mget *', 'mget.*\? ?') r += sw.cmd('a', 'ftp> ') if r =~ /Failed/ raise('cannot get html files') end htmlfiles = r.scan(/nnection for ([^ ]+) /).collect { |a| a[0] } HTMLFILES.each do |f| next if htmlfiles.include?(f) raise("missing HTML file: #{f}") end end sw.cmd('exit', oprompt) if options[:c] sw.cmd('set web-authentication ssl-crt', '[^:]+: ') sw.cmd(KEY) sw.cmd(CERT) sw.cmd(INT, '[^:]+:') sw.cmd('y', oprompt) CERTFILES.each { |f| sw.cmd("del ramdisk #{f}") } end if options[:w] # # here create a directory and move files in order to avoid that # unnecessary files are loaded into HTML files area. # sw.cmd("mkdir ramdisk #{HTMLDIR}") htmlfiles.each do |f| # they do not have ``move''...sign... sw.cmd("copy ramdisk #{f} ramdisk #{HTMLDIR}") sw.cmd("del ramdisk #{f}") end sw.cmd("set web-authentication html-files ramdisk #{HTMLDIR}", '[^:]+: ') # Do you wish to install new html-files? (y/n): y sw.cmd('y', oprompt) # okay remove our files. htmlfiles.each do |f| sw.cmd("del ramdisk #{HTMLDIR}/#{f}") end sw.cmd("rmdir ramdisk #{HTMLDIR}") end print " Done: ``#{sw.name}'' (#{sw.ia}) " + "(#{sw.maker_to_s} #{sw.product})\n" end Switch.warn retrieve_end_time = Time.now exit if ! options[:r] reboot_times = [ Time.now ] reboot_counts = [] thread_concurrency = 64 for rdepth in 0 .. vertices.maxdepth do depth = vertices.maxdepth - rdepth reboot_counts[depth] = vertices.length(depth) puts "examine: #{depth}-th depth (#{vertices.length(depth)} switches)" threads = [] for i in 1..thread_concurrency do threads <<= Thread.new do begin while v = vertices.pop(depth) do sw = v.sw if ! options[:d] sw.configure sw.cmd('save') sw.unconfigure # # note that pager configuration # or other configurations barks # without an option, ``-f.'' # begin sw.cmd('reload -f') rescue end end puts " Reboot: ``#{sw.name}'' " + "(#{sw.ia}) done" end rescue end end end threads.each do |thread| thread.join end reboot_times.push(Time.now) end puts "Retrieve time : #{retrieve_start_time.iso8601(3)}" + " (#{retrieve_end_time - retrieve_start_time})" puts " Reboot time: #{reboot_times.first.iso8601(3)}" + " (#{reboot_times.last - reboot_times.first})" for depth in 0 .. vertices.maxdepth do printf "% 10d-th depth: % 4d switches: " + "#{reboot_times[depth + 1] - reboot_times[depth]}\n", depth, reboot_counts[depth] end