#!/opt/puppetlabs/puppet/bin/ruby # # Purpose # ------- # # This script is meant to be called by the %preun and %postttrans sections of # the various SIMP Puppet module RPMs. # # The purpose of the script is to provide helper methods that correctly # scaffold the system in such a way that all SIMP Puppet Modules can be # installed via RPM to a single location and subsequently can be copied into # the standard SIMP installation location based on the version of Puppet that # is installed. # # Care is taken that, should the target directory be managed via 'git', this # script will do nothing to harm the managed installation in any way. This # ensures that the SIMP modules have maximum compatibility with the widest # possible range of Puppet module best practices for management while ensuring # that the RPM installations and upgrades can proceed in a seamless fashion # over time. # # Should the 'simp' environment not be found, the script will simply exit # without copying any files. # # Configuration # ------------- # # A configuration file may be placed at /etc/simp/adapter_config.yaml. The # file must consist of proper YAML as demonstrated in the example below which # lists the default options. # # Any configuration options that are not understood will be ignored. # # ```yaml # --- # # Target directory # # May be set to a fully qualified path or 'auto' # # If set to 'auto', the directory will be determined from puppet itself # # target_directory : 'auto' # # # Copy the RPM data to the target directory # # copy_rpm_data : false # # ``` # require 'facter' require 'fileutils' require 'yaml' require 'optparse' require 'ostruct' require 'find' # Make sure we can find the Puppet executables ENV['PATH'] += ':/opt/puppetlabs/bin' class SimpRpmHelper def initialize @program_name = File.basename(__FILE__) # A list of modules that should never be touched once installed @safe_modules = ['site'] end def debug(msg) # SIMP RPMs do not enable debug when they call this script. So, if # you want to debug an RPM problem with this script, comment out # the line below. return unless @options.debug msg.split("\n").each do |line| puts ">>>#{@program_name} DEBUG: #{line}" end end def info(msg) # When these messages get written out in an RPM upgrade, name of program # is helpful to end user puts "#{@program_name}: #{msg}" end # Get the Puppet configuration parameters currently in use def get_puppet_config system_config = %x{puppet config --section master print} config_hash = Hash.new system_config.each_line do |line| k,v = line.split('=') config_hash[k.strip] = v.strip end return config_hash end # Determine whether the passed path is under management by git or svn def is_managed?(path) # Short circuit if the directory is not present return false unless File.directory?(path) git = Facter::Core::Execution.which('git') svn = Facter::Core::Execution.which('svn') Dir.chdir(path) do if git %x{#{git} ls-files . --error-unmatch &> /dev/null} return true if $?.success? end if svn %x{#{svn} info &> /dev/null} return true if $?.success? end end return false end def parse_options(args) @options = OpenStruct.new @options.config_file = '/etc/simp/adapter_config.yaml' @options.preserve = false all_opts = OptionParser.new do |opts| opts.banner = "Usage: #{@program_name} [options]" opts.separator '' opts.on( '--rpm_dir PATH', 'The directory into which the RPM source material is installed' ) do |arg| @options.rpm_dir = arg.strip @options.module_name = File.basename(@options.rpm_dir) end opts.on( '--rpm_section SECTION', 'The section of the RPM from which the script is being called.', " Must be one of 'pre', 'preun', 'postun', 'posttrans'" ) do |arg| @options.rpm_section = arg.strip end opts.on( '--rpm_status STATUS', 'The status code passed to the RPM section' ) do |arg| @options.rpm_status = arg.strip end opts.on( '-f CONFIG_FILE', '--config CONFIG_FILE', 'The configuration file to use.', " Default: #{@options.config_file}" ) do |arg| @options.config_file = arg.strip end opts.on( '-p', '--preserve', "Preserve material in 'target_dir' that is not in 'rpm_dir'" ) do |arg| @options.preserve = true end opts.on( '-e', '--enforce', 'If set, enforce the copy, regardless of the setting in the config file', ' Default: false' ) do |arg| @options.copy_rpm_data = true end opts.on( '-t DIR', '--target_dir DIR', "The subdirectory of #{simp_target_dir('')}", 'into which to copy the materials.', " Default: #{simp_target_dir.gsub(/#{simp_target_dir('')}/,'')}" ) do |arg| @options.target_dir = simp_target_dir(arg.strip) end opts.on( '-v', '--verbose', 'Print out debug info when processing.' ) do @options.debug = true end opts.on( '-h', '--help', 'Help Message' ) do puts opts @options.help_requested = true end end begin all_opts.parse!(args) rescue OptionParser::ParseError => e msg = "Error: #{e}\n\n#{all_opts}" raise(msg) end validate_options(all_opts.to_s) end # Process the config, validate the entries and do some munging # Sets @options hash. def process_config # Defaults config = { 'target_directory' => 'auto', 'copy_rpm_data' => false } if File.exist?(@options.config_file) begin system_config = YAML.load_file(@options.config_file) if system_config config.merge!(system_config) end rescue raise("Error: Config file '#{@options.config_file}' could not be processed") end end if @options.copy_rpm_data.nil? @options.copy_rpm_data = (config['copy_rpm_data'].to_s == 'true') end if @options.target_dir.nil? && config['target_directory'] if config['target_directory'] == 'auto' @options.target_dir = simp_target_dir else unless config['target_directory'][0].chr == '/' raise("Error: 'target_directory' in '#{@options.config_file}' must be an absolute path") end @options.target_dir = config['target_directory'].strip end end end def puppet_codedir # Figure out where the Puppet code should go # Puppet 4+ code_dir = puppet_config['codedir'] if !code_dir || code_dir.empty? code_dir = puppet_config['confdir'] end return code_dir end def puppet_config unless @puppet_config @puppet_config = get_puppet_config end @puppet_config end def puppet_group puppet_config['group'] end # Return the target installation directory def simp_target_dir(subdir=File.join('simp','modules')) install_target = puppet_codedir if install_target.empty? raise('Error: Could not find a Puppet code directory for installation') end install_target = File.join(install_target,'environments', subdir) return install_target end # Input Validation def validate_options(usage) return if @options.help_requested unless @options.rpm_dir raise("Error: 'rpm_dir' is required\n#{usage}") end unless @options.rpm_status raise("Error: 'rpm_status' is required\n#{usage}") end unless @options.rpm_section raise("Error: 'rpm_section' is required\n#{usage}") end # We allow 'post' for backward compatibility with SIMP RPMs that use # this, but copying over files in the 'post' during an upgrade is # problematic. If the old package has files that are not in the new # package, these files will not be removed in the destination directory. # This is because during %post, the old package files have not yet # been removed from the source directory by RPM. So, the 'rsync' # operation copies over the OBE files from the old package. valid_rpm_sections = ['pre','post','preun','postun', 'posttrans'] unless valid_rpm_sections.include?(@options.rpm_section) raise("Error: 'rpm_section' must be one of '#{valid_rpm_sections.join("', '")}'\n#{usage}") end if (@options.rpm_section == 'posttrans') || (@options.rpm_section == 'preun') || (@options.rpm_section == 'post') unless File.directory?(@options.rpm_dir) raise("Error: Could not find 'rpm_dir': '#{@options.rpm_dir}'") end end unless @options.rpm_status =~ /^\d+$/ raise("Error: 'rpm_status' must be an integer\n#{usage}") end end def handle_install debug("Processing install, upgrade, or downgrade of #{@options.module_name}") if @safe_modules.include?(@options.module_name) # Make sure that we preserve anything in the safe modules on installation @options.preserve = true if @options.rpm_status == '2' # Short circuit on upgrading safe modules, just don't touch them! target_module_dir = File.join(@options.target_dir, @options.module_name) if File.directory?(target_module_dir) debug("Skipping upgrade of 'safe' module directory #{target_module_dir}") return end end end raise('Error: Could not determine puppet group') if puppet_group.empty? rsync = Facter::Core::Execution.which('rsync') raise("Error: Could not find 'rsync' command!") unless rsync # Create the directories, with the proper mode, all the way down dir_paths = @options.target_dir.split(File::SEPARATOR).reject(&:empty?) top_dir = File::SEPARATOR + dir_paths.shift unless File.directory?(top_dir) FileUtils.mkdir(top_dir, :mode => 0750) FileUtils.chown('root', puppet_group, top_dir) end orig_dir = Dir.pwd Dir.chdir(top_dir) dir_paths.each do |dir| unless File.directory?(dir) FileUtils.mkdir(dir, :mode => 0750) FileUtils.chown('root', puppet_group, dir) end Dir.chdir(dir) end Dir.chdir(orig_dir) cmd = %(#{rsync} -a --force) if @options.preserve cmd += %( --ignore-existing) else cmd += %( --delete) end cmd += %( --verbose) if @options.debug cmd += %( #{@options.rpm_dir} #{@options.target_dir}) cmd += %( 2>&1) info("Copying '#{@options.module_name}' files into '#{@options.target_dir}'") debug("Executing: #{cmd}") output = %x{#{cmd}} debug("Output:\n#{output}") unless $?.success? raise(%(Error: Copy of '#{@options.module_name}' into '#{@options.target_dir}' using '#{cmd}' failed with the following error:\n #{output.gsub("\n","\n ")})) end FileUtils.chown_R(nil, "#{puppet_group}", @options.target_dir) end def handle_uninstall debug("Processing uninstall of #{@options.module_name}") # Play it safe, this needs to have at least 'environments/simp' in it! if @options.target_dir.split(File::SEPARATOR).reject(&:empty?).size < 3 raise("Error: Not removing directory '#{@options.target_dir}' for safety") end if @safe_modules.include?(@options.module_name) target_module_dir = File.join(@options.target_dir, @options.module_name) debug("Skipping removal of 'safe' module directory #{target_module_dir}") return end info("Removing '#{@options.module_name}' files from '#{@options.target_dir}'") # Find out what we have ref_list = [] Dir.chdir(@options.rpm_dir) do Find.find('.').each do |file| if File.symlink?(file) ref_list << file Find.prune end ref_list << file end end # Delete from the bottom up to clear out the directories first # before removing them ref_list.reverse! ref_list.map{|x| x.sub!(/^./, @options.module_name)} # Only delete items that are in the reference repo Dir.chdir(@options.target_dir) do ref_list.each do |to_rm| if File.symlink?(to_rm) debug("Removing symlink #{to_rm}") FileUtils.rm_f(to_rm) elsif File.directory?(to_rm) && (Dir.entries(to_rm).delete_if {|dir| dir == '.' || dir == '..'}.size == 0) debug("Removing directory #{to_rm}") FileUtils.rmdir(to_rm) elsif File.exist?(to_rm) debug("Removing file #{to_rm}") FileUtils.rm_f(to_rm) end end end end def run(args) parse_options(args) return 0 if @options.help_requested process_config debug("Running with config=#{@options.to_s}") # If the target directory is managed, we're done target_module_dir = File.join(@options.target_dir, @options.module_name) unless is_managed?(target_module_dir) || !@options.copy_rpm_data debug("Processing unmanaged target directory #{target_module_dir}") if (@options.rpm_section == 'posttrans') || (@options.rpm_section == 'post') # A regular installation, upgrade or downgrade # This *should* happen in the RPM %posttrans, but we allow this to # occur in the %post for backward compatibility with SIMP RPMs that # erroneously try to affect a copy in the %post. (Copying over the # files in the RPM %post during an upgrade/downgrade is problematic. # If the old package has files that are not in the new package, # these files will not yet have been removed in the source # directory, and thus end up in the target directory.) handle_install elsif @options.rpm_section == 'preun' && @options.rpm_status == '0' # A regular uninstall # This needs to happen *before* RPM removes the files (%preun with # status 0), since we need to compare with what's on disk to undo # the copy done during the RPM install via handle_install() handle_uninstall end end return 0 rescue RuntimeError => e $stderr.puts(e) return 1 rescue Exception => e $stderr.puts(e) e.backtrace.first(10).each{|l| $stderr.puts l } return 1 end end if __FILE__ == $0 helper = SimpRpmHelper.new exit helper.run(ARGV) end