|
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] |