#!/usr/bin/ruby # BEGIN: Requires require 'digest' # END: Requires # BEGIN: Helper Functions def usage puts "#{__FILE__} [ACTION ] [] Description: This utlity is meant to be used to interact with & manage the filebucket on P4 nodes due to the utility for that `puppet filebucket -l ` being nonfunctional. This implements the same functionality (minus the puppet tie-in) and will allow the user to search the filebucket and restore from it. Actions: search : Search for bucket entries matching a portion of the filepath list : List all Bucket entries list-files : List all files/paths that have been backed up to the bucket get : Get the content of a specific entry (by hash) restore : Restore previous state of file stored in bucket. Value can be hash or filename/filepath Global Flags: -d | --debug : Set debug flag -h | --help : This help message Info Format Flags: -i | --inline : Set the info format to inline (MTIME : HASH : FILENAME) -l | --long : Set the info format to long : Entry [HASH]: : Paths: path1,path2,...,pathn : MTIME: YYYY-MM-DD HH:MM:SS -#### : -c | --csv : Set the info format to csv ( MTIME,HASH,FILENAME1[;FILENAMEn] ) Author: Name: Tristan Ancelet Email: tristan.ancelet@acumera.com Phone (Work) #: +1 (337) 965-1855 " end def log (message, level = 0) if $DEBUG == true or $CONFIG[:log_file] != "" if message == "" puts "log was called without providing a message" exit end case level when 0 level="INFO" when 1 level="WARN" when 2 level="CRIT" else level="UNDF" end datestamp=Time.now log_message="#{datestamp} : #{$HOSTNAME} : #{level} : #{caller[0]} : #{message}" if $CONFIG[:log_file] != "" File.open($CONFIG[:log_file],'a') do |file| file.write("#{log_message}\n") end else puts log_message end end end def which(cmd) #https://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| exts.each do |ext| exe = File.join(path, "#{cmd}#{ext}") return exe if File.executable?(exe) && !File.directory?(exe) end end nil end def get_verification (prompt = "Do you want to continue?") while true puts "#{prompt} (y/n): " answer = STDIN.gets answer = answer.strip() case answer when 'y','yes' return true when 'n', 'no' return false else puts "#{answer} is not an acceptible answer" end end end def get_selections (reference_array, prompt = "Which of the following do you want to select? ", options = { :multiple => false, }, &procedure) ## Making clone of array since the selections were passed by reference selections = reference_array.clone def put_prompt (selections, prompt) puts prompt selections.each_with_index do |value,index| puts "#{index} : #{value}" end end if options[:multiple] == true output = Array.new else output = "" end put_prompt selections, prompt while true choice = Integer(STDIN.gets.strip()) if choice.is_a? Integer if choice >= 0 and choice < selections.count if options[:multiple] == true output.push(selections[choice]) selections.delete_at(choice) if get_verification "Are you done selecting?" break end put_prompt selections, prompt else output = selections[choice] break end else puts "#{choice} is not between the values of 0 and #{selections.count}. Please try again." end else puts "#{choice} is not a valid option. Please try again." end end if procedure.respond_to? "call" output = procedure.call(output) end output end # END: Helper Functions # BEGIN: Variables if not (ARGV & ['-h', '--help']).empty? usage exit end if ENV["USER"] != 'root' puts "This script should only be run by root (permissions issues). Please rerun it as root or prepend \"sudo\"" exit end $DEBUG=false puppet_exe = which "puppet" if puppet_exe == nil puts "The puppet utility was not found in $PATH. This utility will not be able to function" exit end $CONFIG = Hash.new $CONFIG[:bucket_dir]=` #{puppet_exe} agent --configprint clientbucketdir `.strip() $CONFIG[:action]="" $CONFIG[:search_term]="" $CONFIG[:log_file]="" $CONFIG[:info_format]="inline" File.open('/etc/hostname') do |file| $HOSTNAME=file.read().strip() end FLAG_REGEX=/\-+\S+/ # END: Variables # BEGIN: Handle CLI Args if ARGV.count == 0 puts "No arguments were provided" usage exit end if not (ARGV & ['-d', '--debug']).empty? $DEBUG=true end i=0 case ARGV[i] when 'search', 'get', 'restore' $CONFIG[:action]=ARGV[i] log "$CONFIG[:action] was set to #{ARGV[i]}" log "user provided search action ARGV[i.next] == #{ARGV[i.next]}" if ARGV[i.next] != "" and not ARGV[i.next] =~ FLAG_REGEX $CONFIG[:search_term]=ARGV[i.next] log "search_term was set to #{ARGV[i.next]}" i+=2 else puts "Flag[#{ARGV[i]}] : Argument[#{ARGV[i.next]}] : Either the argument was not provided or it was a flag" usage exit end when 'list', 'list-files' $CONFIG[:action] = ARGV[i] log "$CONFIG[:action] was set to #{ARGV[i]}" i+=1 else puts "#{ARGV[i]} is not a valid action. Please make sure you use a valid action as the first argument of the script" usage exit end while i < ARGV.count case ARGV[i] when '-c', '--csv' $CONFIG[:info_format]='csv' log "$CONFIG[:info_format] was set to #{$CONFIG[:info_format]}" i+=1 when '-l', '--long' $CONFIG[:info_format]='long' log "$CONFIG[:info_format] was set to #{$CONFIG[:info_format]}" i+=1 when '-i', '--inline' $CONFIG[:info_format]='inline' log "$CONFIG[:info_format] was set to #{$CONFIG[:info_format]}" i+=1 else i+=1 end end ## BEGIN: Checks if $CONFIG[:action] == "" puts "Action was not provided" end case $CONFIG[:action] when 'search', 'get' if $CONFIG[:search_term] == "" puts "Search Term was not provided" usage exit end end ## END: Checks # END: Handle CLI Args # BEGIN: Classes class BucketEntry attr_reader :hash, :filepaths, :mtime def initialize (entry_dir) @entry_dir = entry_dir @hash = File.basename(entry_dir) File.open("#{entry_dir}/paths") do |file| @filepaths = file.read.split(/\n/) end @mtime = File.mtime(entry_dir) log "BucketEntry was created from #{entry_dir}" end def path_include? (path_string) log "BucketEntry[#{hash}] was called with #{path_string}" @filepaths.any?{|path| path.include? path_string} end def long_info "Entry [#{@hash}]: Paths: #{@filepaths.join(',')} MTIME: #{@mtime} " end def csv_info [@mtime,@hash,@filepaths.join(';')].join(',') end def inline_info "#{@mtime} : #{@hash} : #{@filepaths.join(',')}" end def info case $CONFIG[:info_format] when 'long' long_info when 'inline' inline_info when 'csv' csv_info end end def content log "BucketEntry[#{@hash}] getting contents" File.open("#{@entry_dir}/contents",'r') do |file| file.read() end end end class Bucket attr_reader :bucketdir, :entries def initialize (clientbucketdir) log "Bucket is being created from #{clientbucketdir}" @bucketdir = clientbucketdir @entries = Hash.new load_bucket end def select(&proc) @entries.each_value.select &proc end def any?(&proc) @entries.each_value.any? &proc end def load_bucket log "Bucket[#{@bucketdir}] is loading entries" Dir["#{@bucketdir}/**/paths"].each.map{|path| File.dirname(path)}.each do |directory| log "\"#{directory}\" was grabbed from bucket directory. Making new BucketEntry" entry = BucketEntry.new(directory) @entries[entry.hash]=entry log "BucketEntry[#{entry.hash}] was added to @entries Size=#{@entries.count()}" end log "Bucket[#{@bucketdir}] was loaded" end def filenames filenames = Array.new @entries.each_value do |entry| entry.filepaths.each do |path| if not filenames.include? path filenames.push(path) end end end filenames end end # END: Classes # BEGIN: Work Functions def search_entries_paths (bucket) puts bucket.select{|entry| entry.path_include? $CONFIG[:search_term]}.sort_by(&:mtime).map(&:info) end def get_content_of_entry_hash (bucket) if bucket.entries.has_key? $CONFIG[:search_term] puts bucket.entries[$CONFIG[:search_term]].content else puts "There were no entries corresponding to #{$CONFIG[:search_term]}" exit end end def list_all_entries (bucket) puts bucket.filenames.map{|filename| bucket.select{|entry| entry.path_include? filename}.sort_by(&:mtime).map(&:info)} end def list_entry_files (bucket) puts bucket.filenames.sort.join("\n") end def get_entry_by_file (bucket, filenames) entry = nil if filenames.count == 1 filename = filenames[0] else filename = get_selections filenames, "Your filename matched multiple files. Please select one to restore" end entries = bucket.select{|entry| entry.path_include? filename} if entries.count == 1 entry = entries.first else while true mtimes = entries.map{|entry| entry.mtime} entry_mtime = get_selections(mtimes , "Which timestamp to you want to revert the file to?") entry = entries.lazy.select{|entry| entry.mtime == entry_mtime}.first if get_verification "Do you want to see the contents of #{filename} at this time?" puts entry.content if get_verification "Is this the entry you want to overwrite #{filename} with?" break end else break end end end if filename[0] != '/' filename = "/#{filename}" end return entry, filename end def get_entry_by_hash (bucket) if bucket.entries.has_key? $CONFIG[:search_term] entry = bucket.entries[$CONFIG[:search_term]] filepath = "" if entry.filepaths.count == 1 filepath = entry.filepaths[0] else filepath = get_selections entry.filepaths, "What filepath do you wish to restore to?" end if filepath[0] != '/' filepath = "/#{filepath}" end return entry, filepath else puts "There were no entries corresponding to #{$CONFIG[:search_term]}" end end def restore_entry (bucket) entry = nil if bucket.any?{ |entry| entry.path_include? $CONFIG[:search_term] } filenames = bucket.filenames.select {|filename| filename.include? $CONFIG[:search_term]} entry, filepath = get_entry_by_file bucket, filenames else entry, filepath = get_entry_by_hash bucket end if get_verification "Are you sure you want to overwrite #{filepath}?" File.open(filepath,'w') do |file| file.write(entry.content) end else puts "Ok not overwriting." end end # END: Work Functions # BEGIN: Work if __FILE__ == $0 bucket = Bucket.new($CONFIG[:bucket_dir]) case $CONFIG[:action] when 'search' search_entries_paths bucket when 'get' get_content_of_entry_hash bucket when 'list' list_all_entries bucket when 'list-files' list_entry_files bucket when 'restore' restore_entry bucket end end # END: Work