From: <jh...@us...> - 2012-04-30 02:06:14
|
Revision: 337 http://etch.svn.sourceforge.net/etch/?rev=337&view=rev Author: jheiss Date: 2012-04-30 02:06:05 +0000 (Mon, 30 Apr 2012) Log Message: ----------- Tag 4.0.0 release Modified Paths: -------------- Rakefile tags/release-4.0.0/VERSION Added Paths: ----------- tags/release-4.0.0/ tags/release-4.0.0/client/Gemfile tags/release-4.0.0/client/Gemfile.lock tags/release-4.0.0/client/lib/etch/client.rb tags/release-4.0.0/server/ tags/release-4.0.0/server/app/assets/stylesheets/design.css tags/release-4.0.0/server/app/controllers/dashboard_controller.rb tags/release-4.0.0/server/config/initializers/session_store.rb tags/release-4.0.0/server/config/initializers/wrap_parameters.rb tags/release-4.0.0/server/lib/etch/server.rb tags/release-4.0.0/server/lib/etch.rb tags/release-4.0.0/test/TODO tags/release-4.0.0/test/etchtest.rb tags/release-4.0.0/test/test_auth.rb tags/release-4.0.0/test/test_conf.rb tags/release-4.0.0/test/test_file.rb tags/release-4.0.0/test/test_outputcapture.rb Removed Paths: ------------- tags/release-4.0.0/Gemfile tags/release-4.0.0/Gemfile.lock tags/release-4.0.0/client/lib/etch/client.rb tags/release-4.0.0/server/ tags/release-4.0.0/server/app/assets/stylesheets/design.css tags/release-4.0.0/server/app/controllers/dashboard_controller.rb tags/release-4.0.0/server/config/initializers/session_store.rb tags/release-4.0.0/server/config/initializers/wrap_parameters.rb tags/release-4.0.0/server/lib/etch/server.rb tags/release-4.0.0/server/lib/etch.rb tags/release-4.0.0/test/TODO tags/release-4.0.0/test/etchtest.rb tags/release-4.0.0/test/test_auth.rb tags/release-4.0.0/test/test_conf.rb tags/release-4.0.0/test/test_file.rb tags/release-4.0.0/test/test_outputcapture.rb Modified: Rakefile =================================================================== --- Rakefile 2012-04-30 01:44:32 UTC (rev 336) +++ Rakefile 2012-04-30 02:06:05 UTC (rev 337) @@ -1,4 +1,4 @@ -ETCHVER = '3.20.1' +ETCHVER = '4.0.0' TAGNAME = "release-#{ETCHVER}" TAGDIR = "tags/#{TAGNAME}" DIST = "etch-#{ETCHVER}" Deleted: tags/release-4.0.0/Gemfile =================================================================== --- trunk/Gemfile 2012-04-21 04:22:22 UTC (rev 315) +++ tags/release-4.0.0/Gemfile 2012-04-30 02:06:05 UTC (rev 337) @@ -1,14 +0,0 @@ -source :rubygems -gem 'facter' -gem 'nokogiri' -# Tests will be run with nokogiri by default, but it might be nice to have -# libxml available too if you want to test with it as well. -gem 'libxml-ruby' - -group :server do - gem 'rails', '2.3.14' - gem 'sqlite3' - gem 'will_paginate', '~> 2.3.15' - gem 'searchlogic' -end - Deleted: tags/release-4.0.0/Gemfile.lock =================================================================== --- trunk/Gemfile.lock 2012-04-21 04:22:22 UTC (rev 315) +++ tags/release-4.0.0/Gemfile.lock 2012-04-30 02:06:05 UTC (rev 337) @@ -1,42 +0,0 @@ -GEM - remote: http://rubygems.org/ - specs: - actionmailer (2.3.14) - actionpack (= 2.3.14) - actionpack (2.3.14) - activesupport (= 2.3.14) - rack (~> 1.1.0) - activerecord (2.3.14) - activesupport (= 2.3.14) - activeresource (2.3.14) - activesupport (= 2.3.14) - activesupport (2.3.14) - facter (1.6.5) - libxml-ruby (2.2.2) - nokogiri (1.5.0) - rack (1.1.3) - rails (2.3.14) - actionmailer (= 2.3.14) - actionpack (= 2.3.14) - activerecord (= 2.3.14) - activeresource (= 2.3.14) - activesupport (= 2.3.14) - rake (>= 0.8.3) - rake (0.9.2) - searchlogic (2.5.8) - activerecord (~> 2.3.12) - activerecord (~> 2.3.12) - sqlite3 (1.3.5) - will_paginate (2.3.16) - -PLATFORMS - ruby - -DEPENDENCIES - facter - libxml-ruby - nokogiri - rails (= 2.3.14) - searchlogic - sqlite3 - will_paginate (~> 2.3.15) Modified: tags/release-4.0.0/VERSION =================================================================== --- trunk/VERSION 2012-04-21 04:22:22 UTC (rev 315) +++ tags/release-4.0.0/VERSION 2012-04-30 02:06:05 UTC (rev 337) @@ -1 +1 @@ -trunk +4.0.0 Copied: tags/release-4.0.0/client/Gemfile (from rev 323, trunk/client/Gemfile) =================================================================== --- tags/release-4.0.0/client/Gemfile (rev 0) +++ tags/release-4.0.0/client/Gemfile 2012-04-30 02:06:05 UTC (rev 337) @@ -0,0 +1,2 @@ +source :rubygems +gem 'facter' Copied: tags/release-4.0.0/client/Gemfile.lock (from rev 323, trunk/client/Gemfile.lock) =================================================================== --- tags/release-4.0.0/client/Gemfile.lock (rev 0) +++ tags/release-4.0.0/client/Gemfile.lock 2012-04-30 02:06:05 UTC (rev 337) @@ -0,0 +1,10 @@ +GEM + remote: http://rubygems.org/ + specs: + facter (1.6.5) + +PLATFORMS + ruby + +DEPENDENCIES + facter Deleted: tags/release-4.0.0/client/lib/etch/client.rb =================================================================== --- trunk/client/lib/etch/client.rb 2012-04-21 04:22:22 UTC (rev 315) +++ tags/release-4.0.0/client/lib/etch/client.rb 2012-04-30 02:06:05 UTC (rev 337) @@ -1,2583 +0,0 @@ -############################################################################## -# Etch configuration file management tool library -############################################################################## - -# Ensure we can find etch.rb if run within the development directory structure -# This is roughly equivalent to "../../../server/lib" -serverlibdir = File.join(File.dirname(File.dirname(File.dirname(File.dirname(File.expand_path(__FILE__))))), 'server', 'lib') -if File.exist?(serverlibdir) - $:.unshift(serverlibdir) -end - -# Exclude standard libraries and gems from the warnings induced by -# running ruby with the -w flag. Several of these have warnings under -# ruby 1.9 and there's nothing we can do to fix that. -require 'silently' -Silently.silently do - begin - # Try loading facter w/o gems first so that we don't introduce a - # dependency on gems if it is not needed. - require 'facter' # Facter - rescue LoadError - require 'rubygems' - require 'facter' - end - require 'find' - require 'digest/sha1' # hexdigest - require 'openssl' # OpenSSL - require 'base64' # decode64, encode64 - require 'uri' - require 'net/http' - require 'net/https' - require 'rexml/document' - require 'fileutils' # copy, mkpath, rmtree - require 'fcntl' # Fcntl::O_* - require 'etc' # getpwnam, getgrnam - require 'tempfile' # Tempfile - require 'find' # Find.find - require 'cgi' - require 'timeout' - require 'logger' -end -require 'etch' - -class Etch::Client - VERSION = 'unset' - - CONFIRM_PROCEED = 1 - CONFIRM_SKIP = 2 - CONFIRM_QUIT = 3 - PRIVATE_KEY_PATHS = ["/etc/ssh/ssh_host_rsa_key", "/etc/ssh_host_rsa_key"] - DEFAULT_CONFIGDIR = '/etc' - DEFAULT_VARDIR = '/var/etch' - DEFAULT_DETAILED_RESULTS = ['SERVER'] - - # We need these in relation to the output capturing - ORIG_STDOUT = STDOUT.dup - ORIG_STDERR = STDERR.dup - - attr_reader :exec_once_per_run - - def initialize(options) - @server = options[:server] ? options[:server] : 'https://etch' - @configdir = options[:configdir] ? options[:configdir] : DEFAULT_CONFIGDIR - @vardir = options[:vardir] ? options[:vardir] : DEFAULT_VARDIR - @tag = options[:tag] - @local = options[:local] ? File.expand_path(options[:local]) : nil - @debug = options[:debug] - @dryrun = options[:dryrun] - @listfiles = options[:listfiles] - @interactive = options[:interactive] - @filenameonly = options[:filenameonly] - @fullfile = options[:fullfile] - @key = options[:key] ? options[:key] : get_private_key_path - @disableforce = options[:disableforce] - @lockforce = options[:lockforce] - - @last_response = "" - - @file_system_root = '/' # Not sure if this needs to be more portable - # This option is only intended for use by the test suite - if options[:file_system_root] - @file_system_root = options[:file_system_root] - @vardir = File.join(@file_system_root, @vardir) - @configdir = File.join(@file_system_root, @configdir) - end - - @configfile = File.join(@configdir, 'etch.conf') - @detailed_results = [] - - if File.exist?(@configfile) - IO.foreach(@configfile) do |line| - line.chomp! - next if (line =~ /^\s*$/); # Skip blank lines - next if (line =~ /^\s*#/); # Skip comments - line.strip! # Remove leading/trailing whitespace - key, value = line.split(/\s*=\s*/, 2) - if key == 'server' - # A setting for the server to use which comes from upstream - # (generally from a command line option) takes precedence - # over the config file - if !options[:server] - @server = value - # Warn the user, as this could potentially be confusing - # if they don't realize there's a config file lying - # around - warn "Using server #{@server} from #{@configfile}" if @debug - else - # "command line override" isn't necessarily accurate, we don't - # know why the caller passed us an option to override the config - # file, but most of the time it will be due to a command line - # option and I want the message to be easily understood by users. - # If someone can come up with some better wording without turning - # the message into something as long as this comment that would be - # welcome. - warn "Ignoring 'server' option in #{@configfile} due to command line override" if @debug - end - elsif key == 'local' - if !options[:local] && !options[:server] - @local = value - warn "Using local directory #{@local} from #{@configfile}" if @debug - else - warn "Ignoring 'local' option in #{@configfile} due to command line override" if @debug - end - elsif key == 'key' - if !options[:key] - @key = value - warn "Using key #{@key} from #{@configfile}" if @debug - else - warn "Ignoring 'key' option in #{@configfile} due to command line override" if @debug - end - elsif key == 'path' - ENV['PATH'] = value - elsif key == 'detailed_results' - warn "Adding detailed results destination '#{value}'" if @debug - @detailed_results << value - end - end - end - - if @key && !File.readable?(@key) - @key = nil - end - if !@key - warn "No readable private key found, messages to server will not be signed and may be rejected depending on server configuration" - end - - if @detailed_results.empty? - @detailed_results = DEFAULT_DETAILED_RESULTS - end - - @origbase = File.join(@vardir, 'orig') - @historybase = File.join(@vardir, 'history') - @lockbase = File.join(@vardir, 'locks') - @requestbase = File.join(@vardir, 'requests') - - @facts = Facter.to_hash - if @facts['operatingsystemrelease'] - # Some versions of Facter have a bug that leaves extraneous - # whitespace on this fact. Work around that with strip. I.e. on - # CentOS you'll get '5 ' or '5.2 '. - @facts['operatingsystemrelease'].strip! - end - - if @local - logger = Logger.new(STDOUT) - dlogger = Logger.new(STDOUT) - if @debug - dlogger.level = Logger::DEBUG - else - dlogger.level = Logger::INFO - end - blankrequest = {} - @facts.each_pair { |key, value| blankrequest[key] = value.to_s } - blankrequest['fqdn'] = @facts['fqdn'] - @facts = blankrequest - @etch = Etch.new(logger, dlogger) - else - # Make sure the server URL ends in a / so that we can append paths - # to it using URI.join - if @server !~ %r{/$} - @server << '/' - end - @filesuri = URI.join(@server, 'files') - @resultsuri = URI.join(@server, 'results') - - @blankrequest = {} - # If the user specified a non-standard key then override the - # sshrsakey fact so that authentication works - if @key - @facts['sshrsakey'] = IO.read(@key+'.pub').chomp.split[1] - end - @facts.each_pair { |key, value| @blankrequest["facts[#{key}]"] = value.to_s } - @blankrequest['fqdn'] = @facts['fqdn'] - if @debug - @blankrequest['debug'] = '1' - end - if @tag - @blankrequest['tag'] = @tag - end - end - - @locked_files = {} - @first_update = {} - @already_processed = {} - @exec_already_processed = {} - @exec_once_per_run = {} - @results = [] - # See start/stop_output_capture for these - @output_pipes = [] - - @lchown_supported = nil - @lchmod_supported = nil - end - - def process_until_done(files, commands) - # Our overall status. Will be reported to the server and used as the - # return value for this method. Command-line clients should use it as - # their exit value. Zero indicates no errors. - status = 0 - message = '' - - # A variable to collect filenames if operating in @listfiles mode - files_to_list = {} - - # Prep http instance - http = nil - if !@local - puts "Connecting to #{@filesuri}" if (@debug) - http = Net::HTTP.new(@filesuri.host, @filesuri.port) - if @filesuri.scheme == "https" - # Eliminate the OpenSSL "using default DH parameters" warning - if File.exist?(File.join(@configdir, 'etch', 'dhparams')) - dh = OpenSSL::PKey::DH.new(IO.read(File.join(@configdir, 'etch', 'dhparams'))) - Net::HTTP.ssl_context_accessor(:tmp_dh_callback) - http.tmp_dh_callback = proc { dh } - end - http.use_ssl = true - if File.exist?(File.join(@configdir, 'etch', 'ca.pem')) - http.ca_file = File.join(@configdir, 'etch', 'ca.pem') - http.verify_mode = OpenSSL::SSL::VERIFY_PEER - elsif File.directory?(File.join(@configdir, 'etch', 'ca')) - http.ca_path = File.join(@configdir, 'etch', 'ca') - http.verify_mode = OpenSSL::SSL::VERIFY_PEER - end - end - http.start - end - - # catch/throw for expected/non-error events that end processing - # begin/raise for error events that end processing - catch :stop_processing do - begin - enabled, message = check_for_disable_etch_file - if !enabled - # 200 is the arbitrarily picked exit value indicating - # that etch is disabled - status = 200 - throw :stop_processing - end - remove_stale_lock_files - - # Assemble the initial request - request = nil - if @local - request = {} - if files && !files.empty? - request[:files] = {} - files.each do |file| - request[:files][file] = {:orig => save_orig(file)} - local_requests = get_local_requests(file) - if local_requests - request[:files][file][:local_requests] = local_requests - end - end - end - if commands && !commands.empty? - request[:commands] = {} - commands.each do |command| - request[:commands][command] = {} - end - end - else - request = get_blank_request - if (files && !files.empty?) || (commands && !commands.empty?) - if files - files.each do |file| - request["files[#{CGI.escape(file)}][sha1sum]"] = - get_orig_sum(file) - local_requests = get_local_requests(file) - if local_requests - request["files[#{CGI.escape(file)}][local_requests]"] = - local_requests - end - end - end - if commands - commands.each do |command| - request["commands[#{CGI.escape(command)}]"] = '1' - end - end - else - request['files[GENERATEALL]'] = '1' - end - end - - # - # Loop back and forth with the server sending requests for files and - # responding to the server's requests for original contents or sums - # it needs - # - - Signal.trap('EXIT') do - STDOUT.reopen(ORIG_STDOUT) - STDERR.reopen(ORIG_STDERR) - unlock_all_files - end - - # It usually takes a few back and forth exchanges with the server to - # exchange all needed data and get a complete set of configuration. - # The number of iterations is capped at 10 to prevent any unplanned - # infinite loops. The limit of 10 was chosen somewhat arbitrarily but - # seems fine in practice. - 10.times do - # - # Send request to server - # - - responsedata = {} - if @local - results = @etch.generate(@local, @facts, request) - # FIXME: Etch#generate returns parsed XML using whatever XML - # library it happens to use. In order to avoid re-parsing - # the XML we'd have to use the XML abstraction code from Etch - # everwhere here. - # Until then re-parse the XML using REXML. - #responsedata[:configs] = results[:configs] - responsedata[:configs] = {} - results[:configs].each {|f,c| responsedata[:configs][f] = REXML::Document.new(c.to_s) } - responsedata[:need_sums] = {} - responsedata[:need_origs] = results[:need_orig] - #responsedata[:allcommands] = results[:allcommands] - responsedata[:allcommands] = {} - results[:allcommands].each {|cn,c| responsedata[:allcommands][cn] = REXML::Document.new(c.to_s) } - responsedata[:retrycommands] = results[:retrycommands] - else - puts "Sending request to server #{@filesuri}: #{request.inspect}" if (@debug) - post = Net::HTTP::Post.new(@filesuri.path) - post.set_form_data(request) - sign_post!(post, @key) - response = http.request(post) - if !response.kind_of?(Net::HTTPSuccess) - $stderr.puts response.body - # error! raises an exception - response.error! - end - puts "Response from server:\n'#{response.body}'" if (@debug) - if !response.body.nil? && !response.body.empty? - response_xml = REXML::Document.new(response.body) - responsedata[:configs] = {} - response_xml.elements.each('/files/configs/config') do |config| - file = config.attributes['filename'] - # We have to make a new document so that XPath paths are - # referenced relative to the configuration for this - # specific file. - #responsedata[:configs][file] = REXML::Document.new(response_xml.elements["/files/configs/config[@filename='#{file}']"].to_s) - responsedata[:configs][file] = REXML::Document.new(config.to_s) - end - responsedata[:need_sums] = {} - response_xml.elements.each('/files/need_sums/need_sum') do |ns| - responsedata[:need_sums][ns.text] = true - end - responsedata[:need_origs] = {} - response_xml.elements.each('/files/need_origs/need_orig') do |no| - responsedata[:need_origs][no.text] = true - end - responsedata[:allcommands] = {} - response_xml.elements.each('/files/allcommands/commands') do |command| - commandname = command.attributes['commandname'] - # We have to make a new document so that XPath paths are - # referenced relative to the configuration for this - # specific file. - #responsedata[:allcommands][commandname] = REXML::Document.new(response_xml.root.elements["/files/allcommands/commands[@commandname='#{commandname}']"].to_s) - responsedata[:allcommands][commandname] = REXML::Document.new(command.to_s) - end - responsedata[:retrycommands] = {} - response_xml.elements.each('/files/retrycommands/retrycommand') do |rc| - responsedata[:retrycommands][rc.text] = true - end - else - puts " Response is empty" if (@debug) - break - end - end - - # - # Process the response from the server - # - - # Prep a clean request hash - if @local - request = {} - if !responsedata[:need_origs].empty? - request[:files] = {} - end - if !responsedata[:retrycommands].empty? - request[:commands] = {} - end - else - request = get_blank_request - end - - # With generateall we expect to make at least two round trips - # to the server. - # 1) Send GENERATEALL request, get back a list of need_sums - # 2) Send sums, possibly get back some need_origs - # 3) Send origs, get back generated files - need_to_loop = false - reset_already_processed - # Process configs first, as they may contain setup entries that are - # needed to create the original files. - responsedata[:configs].each_key do |file| - puts "Processing config for #{file}" if (@debug) - if !@listfiles - continue_processing = process_file(file, responsedata) - if !continue_processing - throw :stop_processing - end - else - files_to_list[file] = true - end - end - responsedata[:need_sums].each_key do |need_sum| - puts "Processing request for sum of #{need_sum}" if (@debug) - if @local - # If this happens we screwed something up, the local mode - # code never requests sums. - raise "No support for sums in local mode" - else - request["files[#{CGI.escape(need_sum)}][sha1sum]"] = - get_orig_sum(need_sum) - end - local_requests = get_local_requests(need_sum) - if local_requests - if @local - request[:files][need_sum][:local_requests] = local_requests - else - request["files[#{CGI.escape(need_sum)}][local_requests]"] = - local_requests - end - end - need_to_loop = true - end - responsedata[:need_origs].each_key do |need_orig| - puts "Processing request for contents of #{need_orig}" if (@debug) - if @local - request[:files][need_orig] = {:orig => save_orig(need_orig)} - else - request["files[#{CGI.escape(need_orig)}][contents]"] = - Base64.encode64(get_orig_contents(need_orig)) - request["files[#{CGI.escape(need_orig)}][sha1sum]"] = - get_orig_sum(need_orig) - end - local_requests = get_local_requests(need_orig) - if local_requests - if @local - request[:files][need_orig][:local_requests] = local_requests - else - request["files[#{CGI.escape(need_orig)}][local_requests]"] = - local_requests - end - end - need_to_loop = true - end - responsedata[:allcommands].each_key do |commandname| - puts "Processing commands #{commandname}" if (@debug) - continue_processing = process_commands(commandname, responsedata) - if !continue_processing - throw :stop_processing - end - end - responsedata[:retrycommands].each_key do |commandname| - puts "Processing request to retry command #{commandname}" if (@debug) - if @local - request[:commands][commandname] = true - else - request["commands[#{CGI.escape(commandname)}]"] = '1' - end - need_to_loop = true - end - - if !need_to_loop - break - end - end - - puts "Processing 'exec once per run' commands" if (!exec_once_per_run.empty?) - exec_once_per_run.keys.each do |exec| - process_exec('post', exec) - end - rescue Exception => e - status = 1 - $stderr.puts e.message - message << e.message - $stderr.puts e.backtrace.join("\n") if @debug - message << e.backtrace.join("\n") if @debug - end # begin/rescue - end # catch - - if @listfiles - puts "Files under management:" - files_to_list.keys.sort.each {|file| puts file} - end - - # Send results to server - if !@dryrun && !@local - rails_results = [] - # A few of the fields here are numbers or booleans and need a - # to_s to make them compatible with CGI.escape, which expects a - # string. - rails_results << "fqdn=#{CGI.escape(@facts['fqdn'])}" - rails_results << "status=#{CGI.escape(status.to_s)}" - rails_results << "message=#{CGI.escape(message)}" - if @detailed_results.include?('SERVER') - @results.each do |result| - # Strangely enough this works. Even though the key is not unique to - # each result the Rails parameter parsing code keeps track of keys it - # has seen, and if it sees a duplicate it starts a new hash. - rails_results << "results[][file]=#{CGI.escape(result['file'])}" - rails_results << "results[][success]=#{CGI.escape(result['success'].to_s)}" - rails_results << "results[][message]=#{CGI.escape(result['message'])}" - end - end - puts "Sending results to server #{@resultsuri}" if (@debug) - resultspost = Net::HTTP::Post.new(@resultsuri.path) - # We have to bypass Net::HTTP's set_form_data method in this case - # because it expects a hash and we can't provide the results in the - # format we want in a hash because we'd have duplicate keys (see above). - results_as_string = rails_results.join('&') - resultspost.body = results_as_string - resultspost.content_type = 'application/x-www-form-urlencoded' - sign_post!(resultspost, @key) - response = http.request(resultspost) - case response - when Net::HTTPSuccess - puts "Response from server:\n'#{response.body}'" if (@debug) - else - $stderr.puts "Error submitting results:" - $stderr.puts response.body - end - end - - if !@dryrun - @detailed_results.each do |detail_dest| - # If any of the destinations look like a file (start with a /) then we - # log to that file - if detail_dest =~ %r{^/} - FileUtils.mkpath(File.dirname(detail_dest)) - File.open(detail_dest, 'a') do |file| - # Add a header for the overall status of the run - file.puts "Etch run at #{Time.now}" - file.puts "Status: #{status}" - if !message.empty? - file.puts "Message:\n#{message}\n" - end - # Then the detailed results - @results.each do |result| - file.puts "File #{result['file']}, result #{result['success']}:\n" - file.puts result['message'] - end - end - end - end - end - - status - end - - def check_for_disable_etch_file - disable_etch = File.join(@vardir, 'disable_etch') - message = '' - if File.exist?(disable_etch) - if !@disableforce - message = "Etch disabled:\n" - message << IO.read(disable_etch) - puts message - return false, message - else - puts "Ignoring disable_etch file" - end - end - return true, message - end - - def get_blank_request - @blankrequest.dup - end - - # Raises an exception if any fatal error is encountered - # Returns a boolean, true unless the user indicated in interactive mode - # that further processing should be halted - def process_file(file, responsedata) - continue_processing = true - save_results = true - exception = nil - - # We may not have configuration for this file, if it does not apply - # to this host. The server takes care of detecting any errors that - # might involve, so here we can just silently return. - config = responsedata[:configs][file] - if !config - puts "No configuration for #{file}, skipping" if (@debug) - return continue_processing - end - - # Skip files we've already processed in response to <depend> - # statements. - if @already_processed.has_key?(file) - puts "Skipping already processed #{file}" if (@debug) - return continue_processing - end - - # Prep the results capturing for this file - result = {} - result['file'] = file - result['success'] = true - result['message'] = '' - - # catch/throw for expected/non-error events that end processing - # begin/raise for error events that end processing - # Within this block you should throw :process_done if you've reached - # a natural stopping point and nothing further needs to be done. You - # should raise an exception if you encounter an error condition. - # Do not 'return' or 'abort'. - catch :process_done do - begin - start_output_capture - - puts "Processing #{file}" if (@debug) - - # The %locked_files hash provides a convenient way to - # detect circular dependancies. It doesn't give us an ordered - # list of dependencies, which might be handy to help the user - # debug the problem, but I don't think it's worth maintaining a - # seperate array just for that purpose. - if @locked_files.has_key?(file) - raise "Circular dependancy detected. " + - "Dependancy list (unsorted) contains:\n " + - @locked_files.keys.join(', ') - end - - # This needs to be after the circular dependency check - lock_file(file) - - # Process any other files that this file depends on - config.elements.each('/config/depend') do |depend| - puts "Processing dependency #{depend.text}" if (@debug) - process_file(depend.text, responsedata) - end - - # Process any commands that this file depends on - config.elements.each('/config/dependcommand') do |dependcommand| - puts "Processing command dependency #{dependcommand.text}" if (@debug) - process_commands(dependcommand.text, responsedata) - end - - # See what type of action the user has requested - - # Check to see if the user has requested that we revert back to the - # original file. - if config.elements['/config/revert'] - origpathbase = File.join(@origbase, file) - origpath = nil - - # Restore the original file if it is around - revert_occurred = false - if File.exist?("#{origpathbase}.ORIG") - origpath = "#{origpathbase}.ORIG" - origdir = File.dirname(origpath) - origbase = File.basename(origpath) - filedir = File.dirname(file) - - # Remove anything we might have written out for this file - remove_file(file) if (!@dryrun) - - puts "Restoring #{origpath} to #{file}" - recursive_copy_and_rename(origdir, origbase, file) if (!@dryrun) - revert_occurred = true - elsif File.exist?("#{origpathbase}.TAR") - origpath = "#{origpathbase}.TAR" - filedir = File.dirname(file) - - # Remove anything we might have written out for this file - remove_file(file) if (!@dryrun) - - puts "Restoring #{file} from #{origpath}" - system("cd #{filedir} && tar xf #{origpath}") if (!@dryrun) - revert_occurred = true - elsif File.exist?("#{origpathbase}.NOORIG") - origpath = "#{origpathbase}.NOORIG" - puts "Original #{file} didn't exist, restoring that state" - - # Remove anything we might have written out for this file - remove_file(file) if (!@dryrun) - revert_occurred = true - end - - # Update the history log - if revert_occurred - save_history(file) - end - - # Now remove the backed-up original so that future runs - # don't do anything - if origpath - remove_file(origpath) if (!@dryrun) - end - - throw :process_done - end - - # Perform any setup commands that the user has requested. - # These are occasionally needed to install software that is - # required to generate the file (think m4 for sendmail.cf) or to - # install a package containing a sample config file which we - # then edit with a script, and thus doing the install in <pre> - # is too late. - if config.elements['/config/setup'] - process_setup(file, config) - end - - if config.elements['/config/file'] # Regular file - newcontents = nil - if config.elements['/config/file/contents'] - newcontents = Base64.decode64(config.elements['/config/file/contents'].text) - end - - permstring = config.elements['/config/file/perms'].text - perms = permstring.oct - owner = config.elements['/config/file/owner'].text - group = config.elements['/config/file/group'].text - uid = lookup_uid(owner) - gid = lookup_gid(group) - - set_file_contents = false - if newcontents - set_file_contents = !compare_file_contents(file, newcontents) - end - set_permissions = nil - set_ownership = nil - # If the file is currently something other than a plain file then - # always set the flags to set the permissions and ownership. - # Checking the permissions/ownership of whatever is there currently - # is useless. - if set_file_contents && (!File.file?(file) || File.symlink?(file)) - set_permissions = true - set_ownership = true - else - set_permissions = !compare_permissions(file, perms) - set_ownership = !compare_ownership(file, uid, gid) - end - - # Proceed if: - # - The new contents are different from the current file - # - The permissions or ownership requested don't match the - # current permissions or ownership - if !set_file_contents && - !set_permissions && - !set_ownership - puts "No change to #{file} necessary" if (@debug) - throw :process_done - else - # Tell the user what we're going to do - if set_file_contents - # If the new contents are different from the current file - # show that to the user in the format they've requested. - # If the requested permissions are not world-readable then - # use the filenameonly format so that we don't disclose - # non-public data, unless we're in interactive mode - if @filenameonly || (permstring.to_i(8) & 0004 == 0 && !@interactive) - puts "Will write out new #{file}" - elsif @fullfile - # Grab the first 8k of the contents - first8k = newcontents.slice(0, 8192) - # Then check it for null characters. If it has any it's - # likely a binary file. - hasnulls = true if (first8k =~ /\0/) - - if !hasnulls - puts "Generated contents for #{file}:" - puts "=============================================" - puts newcontents - puts "=============================================" - else - puts "Will write out new #{file}, but " + - "generated contents are not plain text so " + - "they will not be displayed" - end - else - # Default is to show a diff of the current file and the - # newly generated file. - puts "Will make the following changes to #{file}, diff -c:" - tempfile = Tempfile.new(File.basename(file)) - tempfile.write(newcontents) - tempfile.close - puts "=============================================" - if File.file?(file) && !File.symlink?(file) - system("diff -c #{file} #{tempfile.path}") - else - # Either the file doesn't currently exist, - # or is something other than a normal file - # that we'll be replacing with a file. In - # either case diffing against /dev/null will - # produce the most logical output. - system("diff -c /dev/null #{tempfile.path}") - end - puts "=============================================" - tempfile.delete - end - end - if set_permissions - puts "Will set permissions on #{file} to #{permstring}" - end - if set_ownership - puts "Will set ownership of #{file} to #{uid}:#{gid}" - end - - # If the user requested interactive mode ask them for - # confirmation to proceed. - if @interactive - case get_user_confirmation() - when CONFIRM_PROCEED - # No need to do anything - when CONFIRM_SKIP - save_results = false - throw :process_done - when CONFIRM_QUIT - unlock_all_files - continue_processing = false - save_results = false - throw :process_done - else - raise "Unexpected result from get_user_confirmation()" - end - end - - # Perform any pre-action commands that the user has requested - if config.elements['/config/pre'] - process_pre(file, config) - end - - # If the original "file" is a directory and the user hasn't - # specifically told us we can overwrite it then raise an exception. - # - # The test is here, rather than a bit earlier where you might - # expect it, because the pre section may be used to address - # originals which are directories. So we don't check until - # after any pre commands are run. - if File.directory?(file) && !File.symlink?(file) && - !config.elements['/config/file/overwrite_directory'] - raise "Can't proceed, original of #{file} is a directory,\n" + - " consider the overwrite_directory flag if appropriate." - end - - # Give save_orig a definitive answer on whether or not to save the - # contents of an original directory. - origpath = save_orig(file, true) - # Update the history log - save_history(file) - - # Make sure the directory tree for this file exists - filedir = File.dirname(file) - if !File.directory?(filedir) - puts "Making directory tree #{filedir}" - FileUtils.mkpath(filedir) if (!@dryrun) - end - - # Make a backup in case we need to roll back. We have no use - # for a backup if there are no test commands defined (since we - # only use the backup to roll back if the test fails), so don't - # bother to create a backup unless there is a test command defined. - backup = nil - if config.elements['/config/test_before_post'] || - config.elements['/config/test'] - backup = make_backup(file) - puts "Created backup #{backup}" - end - - # If the new contents are different from the current file, - # replace the file. - if set_file_contents - if !@dryrun - # Write out the new contents into a temporary file - filebase = File.basename(file) - filedir = File.dirname(file) - newfile = Tempfile.new(filebase, filedir) - - # Set the proper permissions on the file before putting - # data into it. - newfile.chmod(perms) - begin - newfile.chown(uid, gid) - rescue Errno::EPERM - raise if Process.euid == 0 - end - - puts "Writing new contents of #{file} to #{newfile.path}" if (@debug) - newfile.write(newcontents) - newfile.close - - # If the current file is not a plain file, remove it. - # Plain files are left alone so that the replacement is - # atomic. - if File.symlink?(file) || (File.exist?(file) && ! File.file?(file)) - puts "Current #{file} is not a plain file, removing it" if (@debug) - remove_file(file) - end - - # Move the new file into place - File.rename(newfile.path, file) - - # Check the permissions and ownership now to ensure they - # end up set properly - set_permissions = !compare_permissions(file, perms) - set_ownership = !compare_ownership(file, uid, gid) - end - end - - # Ensure the permissions are set properly - if set_permissions - File.chmod(perms, file) if (!@dryrun) - end - - # Ensure the ownership is set properly - if set_ownership - begin - File.chown(uid, gid, file) if (!@dryrun) - rescue Errno::EPERM - raise if Process.euid == 0 - end - end - - # Perform any test_before_post commands that the user has requested - if config.elements['/config/test_before_post'] - if !process_test_before_post(file, config) - restore_backup(file, backup) - raise "test_before_post failed" - end - end - - # Perform any post-action commands that the user has requested - if config.elements['/config/post'] - process_post(file, config) - end - - # Perform any test commands that the user has requested - if config.elements['/config/test'] - if !process_test(file, config) - restore_backup(file, backup) - - # Re-run any post commands - if config.elements['/config/post'] - process_post(file, config) - end - end - end - - # Clean up the backup, we don't need it anymore - if config.elements['/config/test_before_post'] || - config.elements['/config/test'] - puts "Removing backup #{backup}" - remove_file(backup) if (!@dryrun) - end - - # Update the history log again - save_history(file) - - throw :process_done - end - end - - if config.elements['/config/link'] # Symbolic link - - dest = config.elements['/config/link/dest'].text - - set_link_destination = !compare_link_destination(file, dest) - absdest = File.expand_path(dest, File.dirname(file)) - - permstring = config.elements['/config/link/perms'].text - perms = permstring.oct - owner = config.elements['/config/link/owner'].text - group = config.elements['/config/link/group'].text - uid = lookup_uid(owner) - gid = lookup_gid(group) - - # lchown and lchmod are not supported on many platforms. The server - # always includes ownership and permissions settings with any link - # (pulling them from defaults.xml if the user didn't specify them in - # the config.xml file.) As such link management would always fail - # on systems which don't support lchown/lchmod, which seems like bad - # behavior. So instead we check to see if they are implemented, and - # if not just ignore ownership/permissions settings. I suppose the - # ideal would be for the server to tell the client whether the - # ownership/permissions were specifically requested (in config.xml) - # rather than just defaults, and then for the client to always try to - # manage ownership/permissions if the settings are not defaults (and - # fail in the event that they aren't implemented.) - if @lchown_supported.nil? - lchowntestlink = Tempfile.new('etchlchowntest').path - lchowntestfile = Tempfile.new('etchlchowntest').path - File.delete(lchowntestlink) - File.symlink(lchowntestfile, lchowntestlink) - begin - File.lchown(0, 0, lchowntestfile) - @lchown_supported = true - rescue NotImplementedError - @lchown_supported = false - rescue Errno::EPERM - raise if Process.euid == 0 - end - File.delete(lchowntestlink) - end - if @lchmod_supported.nil? - lchmodtestlink = Tempfile.new('etchlchmodtest').path - lchmodtestfile = Tempfile.new('etchlchmodtest').path - File.delete(lchmodtestlink) - File.symlink(lchmodtestfile, lchmodtestlink) - begin - File.lchmod(0644, lchmodtestfile) - @lchmod_supported = true - rescue NotImplementedError - @lchmod_supported = false - end - File.delete(lchmodtestlink) - end - - set_permissions = false - if @lchmod_supported - # If the file is currently something other than a link then - # always set the flags to set the permissions and ownership. - # Checking the permissions/ownership of whatever is there currently - # is useless. - if set_link_destination && !File.symlink?(file) - set_permissions = true - else - set_permissions = !compare_permissions(file, perms) - end - end - set_ownership = false - if @lchown_supported - if set_link_destination && !File.symlink?(file) - set_ownership = true - else - set_ownership = !compare_ownership(file, uid, gid) - end - end - - # Proceed if: - # - The new link destination differs from the current one - # - The permissions or ownership requested don't match the - # current permissions or ownership - if !set_link_destination && - !set_permissions && - !set_ownership - puts "No change to #{file} necessary" if (@debug) - throw :process_done - # Check that the link destination exists, and refuse to create - # the link unless it does exist or the user told us to go ahead - # anyway. - # - # Note that the destination may be a relative path, and the - # target directory may not exist yet, so we have to convert the - # destination to an absolute path and test that for existence. - # expand_path should handle paths that are already absolute - # properly. - elsif ! File.exist?(absdest) && ! File.symlink?(absdest) && - ! config.elements['/config/link/allow_nonexistent_dest'] - puts "Destination #{dest} for link #{file} does not exist," + - " consider the allow_nonexistent_dest flag if appropriate." - throw :process_done - else - # Tell the user what we're going to do - if set_link_destination - puts "Linking #{file} -> #{dest}" - end - if set_permissions - puts "Will set permissions on #{file} to #{permstring}" - end - if set_ownership - puts "Will set ownership of #{file} to #{uid}:#{gid}" - end - - # If the user requested interactive mode ask them for - # confirmation to proceed. - if @interactive - case get_user_confirmation() - when CONFIRM_PROCEED - # No need to do anything - when CONFIRM_SKIP - save_results = false - throw :process_done - when CONFIRM_QUIT - unlock_all_files - continue_processing = false - save_results = false - throw :process_done - else - raise "Unexpected result from get_user_confirmation()" - end - end - - # Perform any pre-action commands that the user has requested - if config.elements['/config/pre'] - process_pre(file, config) - end - - # If the original "file" is a directory and the user hasn't - # specifically told us we can overwrite it then raise an exception. - # - # The test is here, rather than a bit earlier where you might - # expect it, because the pre section may be used to address - # originals which are directories. So we don't check until - # after any pre commands are run. - if File.directory?(file) && !File.symlink?(file) && - !config.elements['/config/link/overwrite_directory'] - raise "Can't proceed, original of #{file} is a directory,\n" + - " consider the overwrite_directory flag if appropriate." - end - - # Give save_orig a definitive answer on whether or not to save the - # contents of an original directory. - origpath = save_orig(file, true) - # Update the history log - save_history(file) - - # Make sure the directory tree for this link exists - filedir = File.dirname(file) - if !File.directory?(filedir) - puts "Making directory tree #{filedir}" - FileUtils.mkpath(filedir) if (!@dryrun) - end - - # Make a backup in case we need to roll back. We have no use - # for a backup if there are no test commands defined (since we - # only use the backup to roll back if the test fails), so don't - # bother to create a backup unless there is a test command defined. - backup = nil - if config.elements['/config/test_before_post'] || - config.elements['/config/test'] - backup = make_backup(file) - puts "Created backup #{backup}" - end - - # Create the link - if set_link_destination - remove_file(file) if (!@dryrun) - File.symlink(dest, file) if (!@dryrun) - - # Check the permissions and ownership now to ensure they - # end up set properly - if @lchmod_supported - set_permissions = !compare_permissions(file, perms) - end - if @lchown_supported - set_ownership = !compare_ownership(file, uid, gid) - end - end - - # Ensure the permissions are set properly - if set_permissions - # Note: lchmod - File.lchmod(perms, file) if (!@dryrun) - end - - # Ensure the ownership is set properly - if set_ownership - begin - # Note: lchown - File.lchown(uid, gid, file) if (!@dryrun) - rescue Errno::EPERM - raise if Process.euid == 0 - end - end - - # Perform any test_before_post commands that the user has requested - if config.elements['/config/test_before_post'] - if !process_test_before_post(file, config) - restore_backup(file, backup) - raise "test_before_post failed" - end - end - - # Perform any post-action commands that the user has requested - if config.elements['/config/post'] - process_post(file, config) - end - - # Perform any test commands that the user has requested - if config.elements['/config/test'] - if !process_test(file, config) - restore_backup(file, backup) - - # Re-run any post commands - if config.elements['/config/post'] - process_post(file, config) - end - end - end - - # Clean up the backup, we don't need it anymore - if config.elements['/config/test_before_post'] || - config.elements['/config/test'] - puts "Removing backup #{backup}" - remove_file(backup) if (!@dryrun) - end - - # Update the history log again - save_history(file) - - throw :process_done - end - end - - if config.elements['/config/directory'] # Directory - - # A little safety check - create = config.elements['/config/directory/create'] - raise "No create element found in directory section" if !create - - permstring = config.elements['/config/directory/perms'].text - perms = permstring.oct - owner = config.elements['/config/directory/owner'].text - group = config.elements['/config/directory/group'].text - uid = lookup_uid(owner) - gid = lookup_gid(group) - - set_directory = !File.directory?(file) || File.symlink?(file) - set_permissions = nil - set_ownership = nil - # If the file is currently something other than a directory then - # always set the flags to set the permissions and ownership. - # Checking the permissions/ownership of whatever is there currently - # is useless. - if set_directory - set_permissions = true - set_ownership = true - else - set_permissions = !compare_permissions(file, perms) - set_ownership = !compare_ownership(file, uid, gid) - end - - # Proceed if: - # - The current file is not a directory - # - The permissions or ownership requested don't match the - # current permissions or ownership - if !set_directory && - !set_permissions && - !set_ownership - puts "No change to #{file} necessary" if (@debug) - throw :process_done - else - # Tell the user what we're going to do - if set_directory - puts "Making directory #{file}" - end - if set_permissions - puts "Will set permissions on #{file} to #{permstring}" - end - if set_ownership - puts "Will set ownership of #{file} to #{uid}:#{gid}" - end - - # If the user requested interactive mode ask them for - # confirmation to proceed. - if @interactive - case get_user_confirmation() - when CONFIRM_PROCEED - # No need to do anything - when CONFIRM_SKIP - save_results = false - throw :process_done - when CONFIRM_QUIT - unlock_all_files - continue_processing = false - save_results = false - throw :process_done - else - raise "Unexpected result from get_user_confirmation()" - end - end - - # Perform any pre-action commands that the user has requested - if config.elements['/config/pre'] - process_pre(file, config) - end - - # Give save_orig a definitive answer on whether or not to save the - # contents of an original directory. - origpath = save_orig(file, false) - # Update the history log - save_history(file) - - # Make sure the directory tree for this directory exists - filedir = File.dirname(file) - if !File.directory?(filedir) - puts "Making directory tree #{filedir}" - FileUtils.mkpath(filedir) if (!@dryrun) - end - - # Make a backup in case we need to roll back. We have no use - # for a backup if there are no test commands defined (since we - # only use the backup to roll back if the test fails), so don't - # bother to create a backup unless there is a test command defined. - backup = nil - if config.elements['/config/test_before_post'] || - config.elements['/config/test'] - backup = make_backup(file) - puts "Created backup #{backup}" - end - - # Create the directory - if set_directory - remove_file(file) if (!@dryrun) - Dir.mkdir(file) if (!@dryrun) - - # Check the permissions and ownership now to ensure they - # end up set properly - set_permissions = !compare_permissions(file, perms) - set_ownership = !compare_ownership(file, uid, gid) - end - - # Ensure the permissions are set properly - if set_permissions - File.chmod(perms, file) if (!@dryrun) - end - - # Ensure the ownership is set properly - if set_ownership - begin - File.chown(uid, gid, file) if (!@dryrun) - rescue Errno::EPERM - raise if Process.euid == 0 - end - end - - # Perform any test_before_post commands that the user has requested - if config.elements['/config/test_before_post'] - if !process_test_before_post(file, config) - restore_backup(file, backup) - raise "test_before_post failed" - end - end - - # Perform any post-action commands that the user has requested - if config.elements['/config/post'] - process_post(file, config) - end - - # Perform any test commands that the user has requested - if config.elements['/config/test'] - if !process_test(file, config) - restore_backup(file, backup) - - # Re-run any post commands - if config.elements['/config/post'] - process_post(file, config) - end - end - end - - # Clean up the backup, we don't need it anymore - if config.elements['/config/test_before_post'] || - config.elements['/config/test'] - puts "Removing backup #{backup}" - remove_file(backup) if (!@dryrun) -... [truncated message content] |