diff --git a/bucket-tool b/bucket-tool index 8fa7d9f..bc9b662 100644 --- a/bucket-tool +++ b/bucket-tool @@ -1,5 +1,10 @@ #!/usr/bin/ruby +# BEGIN: Requires + +require 'digest' + +# END: Requires # BEGIN: Helper Functions @@ -14,17 +19,25 @@ Description: search the filebucket and restore from it. Actions: - search : Search for bucket entries matching a portion of the filepath + 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-hash : Restore previous state of file stored in bucket (by-hash) - restore-file : Restore previous state of file stored in bucket (by-filepath/filename) + 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 @@ -63,6 +76,18 @@ def log (message, level = 0) 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): " @@ -79,6 +104,54 @@ def get_verification (prompt = "Do you want to continue?") 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 @@ -98,11 +171,19 @@ 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 agent --configprint clientbucketdir `.strip() +$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 @@ -151,6 +232,25 @@ case ARGV[i] 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] == "" @@ -164,6 +264,7 @@ case $CONFIG[:action] usage exit end + end ## END: Checks @@ -178,12 +279,8 @@ class BucketEntry def initialize (entry_dir) @entry_dir = entry_dir @hash = File.basename(entry_dir) - @filepaths = Array.new File.open("#{entry_dir}/paths") do |file| - file.read().split(/\n/).each do |path| - log "BucketEntry[#{@hash}] adding #{path} to @filepaths" - @filepaths.push(path) - end + @filepaths = file.read.split(/\n/) end @mtime = File.mtime(entry_dir) log "BucketEntry was created from #{entry_dir}" @@ -191,10 +288,10 @@ class BucketEntry def path_include? (path_string) log "BucketEntry[#{hash}] was called with #{path_string}" - @filepaths.each.any? {|path| path.include? path_string} + @filepaths.any?{|path| path.include? path_string} end - def infostring + def long_info "Entry [#{@hash}]: Paths: #{@filepaths.join(',')} MTIME: #{@mtime} @@ -202,10 +299,25 @@ class BucketEntry " 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| @@ -224,6 +336,15 @@ class Bucket 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| @@ -234,6 +355,18 @@ class Bucket 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 @@ -243,12 +376,10 @@ end # BEGIN: Work Functions def search_entries_paths (bucket) - log "user entered" - puts bucket.entries.each_value.select {|entry| entry.path_include? $CONFIG[:search_term]}.map{|entry| entry.inline_info}.sort.join("\n") + puts bucket.select{|entry| entry.path_include? $CONFIG[:search_term]}.sort_by(&:mtime).map(&:info) end def get_content_of_entry_hash (bucket) - log "user entered" if bucket.entries.has_key? $CONFIG[:search_term] puts bucket.entries[$CONFIG[:search_term]].content else @@ -258,40 +389,84 @@ def get_content_of_entry_hash (bucket) end def list_all_entries (bucket) - puts bucket.entries.each_value.each.map{|entry| entry.inline_info}.sort.join("\n") + puts bucket.filenames.map{|filename| bucket.select{|entry| entry.path_include? filename}.sort_by(&:mtime).map(&:info)} end def list_entry_files (bucket) - filenames = Array.new - bucket.entries.each_value do |entry| - entry.filepaths.each do |path| - if not filenames.include? path - filenames.push(path) + 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 - puts filenames.sort.join("\n") + + 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) - if bucket.entries.has_key? $CONFIG[:search_term] - entry = bucket.entries[$CONFIG[:search_term]] - if entry.filepaths.count == 1 - filepath = entry.filepaths[0] - if filepath[0] != '/' - filepath = "/#{filepath}" - 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 - else + 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 "There were no entries corresponding to #{$CONFIG[:search_term]}" + puts "Ok not overwriting." end end