Hacked By AnonymousFox

Current Path : /opt/puppetlabs/pxp-agent/modules/
Upload File :
Current File : //opt/puppetlabs/pxp-agent/modules/pxp-module-puppet

#!/opt/puppetlabs/puppet/bin/ruby
# encoding: UTF-8

require 'json'
require 'yaml'
require 'puppet'

module Pxp
  class ModulePuppet
    module Errors
      InvalidJson = "invalid_json"
      NoPuppetBin = "no_puppet_bin"
      NoLastRunReport = "no_last_run_report"
      InvalidLastRunReport = "invalid_last_run_report"
      Disabled = "agent_disabled"
      Locked = "agent_locked"
      FailedToStart = "agent_failed_to_start"
      NonZeroExit = "agent_exit_non_zero"
    end

    class ProcessingError < StandardError
      attr_reader :error_type

      def initialize(error_type, message = nil)
        super(message)
        @error_type = error_type
      end
    end

    attr_reader :config, :flags

    def self.handle_action(action)
      if action == 'metadata'
        puts metadata.to_json
      else
        result = create_runner($stdin.read.chomp)
        if result.is_a?(self)
          action_results = result.run
        else
          action_results = result
        end

        print action_results.to_json

        unless action_results["error"].nil?
          exit 1
        end
      end
    end

    DEFAULT_EXITCODE = -1

    def get_env_fix_up
      # If running in a C or POSIX locale, ask Puppet to use UTF-8
      base_env = {}
      if Encoding.default_external == Encoding::US_ASCII
        base_env = {"RUBYOPT" => "#{ENV['RUBYOPT']} -EUTF-8"}
      end

      # Prepare an environment fix-up to make up for its cleansing performed
      # by the Puppet::Util::Execution.execute function.
      # This fix-up is meant for running puppet under a non-root user;
      # puppet cannot find the user's HOME directory otherwise.
      @env_fix_up ||= if Puppet.features.microsoft_windows? || Process.euid == 0
        # no environment fix-up is needed on windows or for root
        base_env
      else
        begin
          require 'etc'

          pwentry = Etc.getpwuid(Process.euid)

          {"USER"    => pwentry.name,
           "LOGNAME" => pwentry.name,
           "HOME"    => pwentry.dir}.merge base_env
        rescue => e
          # oh well ..., let's give it a try without the environment fix-up
          myname = File.basename($0)
          $stderr.puts "#{myname}: Could not fix environment for effective UID #{Process.euid}: #{e.message}"
          $stderr.puts "#{myname}: Expect puppet run problems"
          base_env
        end
      end
    end

    def self.last_run_result(exitcode)
      return {"time"             => "unknown",
              "transaction_uuid" => "unknown",
              "environment"      => "unknown",
              "status"           => "unknown",
              "metrics"          => {},
              "exitcode"         => exitcode,
              "version"          => 1}
    end

    def force_unicode(s)
      begin
        # Later comparisons assume UTF-8. Convert to that encoding now.
        s.encode(Encoding::UTF_8)
      rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
        # Found non-native characters, hope it's a UTF-8 string. Since this is Puppet, and
        # incorrect characters probably means we're in a C or POSIX locale, this is usually safe.
        s.force_encoding(Encoding::UTF_8)
      end
    end

    def config_print(*keys)
      command_array = [config["puppet_bin"], "agent", "--configprint", keys.join(',')]
      process_output = Puppet::Util::Execution.execute(command_array,
                                                       {:custom_environment => get_env_fix_up(),
                                                        :override_locale => false})

      result = force_unicode(process_output.to_s)
      if keys.count == 1
        result.chomp
      else
        result.lines.inject({}) do |conf, line|
          key, value = line.chomp.split(' = ', 2)
          if key && value
            conf[key] = value
          end
          conf
        end
      end
    end

    def running?(lockfile)
      return File.exist?(lockfile)
    end

    def disabled?(lockfile)
      return File.exist?(lockfile)
    end

    def make_environment_hash()
      # NB: we're ignoring the `env` array for setting the environment
      return get_env_fix_up()
    end

    def puppet_agent_command
      cmd_array = [config["puppet_bin"], "agent"]
      cmd_array += flags
      return cmd_array
    end

    def self.make_error_result(exitcode, error_type, error_message)
      result = last_run_result(exitcode)
      result["error_type"] = error_type
      result["error"] = error_message
      return result
    end

    def make_error_result(exitcode, error_type, error_message)
      self.class.make_error_result(exitcode, error_type, error_message)
    end

    def parse_report(filename)
      # Read the report and drop Ruby objects first. We can't parse the Ruby objects
      # (because we don't have Puppet loaded) and don't need that data.
      # Psych::Nodes::Node#each iterates over each node in the parsed document tree.
      # YAML.parse_file returns a Psych::Nodes::Document, and #root returns the
      # root-level Node.
      data = YAML.parse_file(filename)
      data.root.each do |o|
        o.tag = nil if o.respond_to?(:tag=)
      end

      data.to_ruby
    end

    def nest_metrics(metrics)
      metrics.fetch('resources', {}).fetch('values', {}).inject({}) do |result, (name,human_name,value)|
        result.merge(name => value)
      end
    end

    def get_result_from_report(last_run_report, exitcode, start_time)
      if !File.exist?(last_run_report)
        return make_error_result(exitcode, Errors::NoLastRunReport,
                                 "#{last_run_report} doesn't exist")
      end

      if start_time && File.mtime(last_run_report) == start_time
        return make_error_result(exitcode, Errors::NoLastRunReport,
                                 "The Puppet run failed in an unexpected way")
      end

      last_run_report_yaml = {}

      begin
        last_run_report_yaml = parse_report(last_run_report)
      rescue => e
        return make_error_result(exitcode, Errors::InvalidLastRunReport,
                                 "#{last_run_report} could not be loaded: #{e}")
      end

      if exitcode == 0
        run_result = self.class.last_run_result(exitcode)
      else
        run_result = make_error_result(exitcode, Errors::NonZeroExit,
                                       "Puppet agent exited with a non 0 exitcode")
      end

      run_result["time"] = last_run_report_yaml['time']
      run_result["transaction_uuid"] = last_run_report_yaml['transaction_uuid']
      run_result["environment"] = last_run_report_yaml['environment']
      run_result["status"] = last_run_report_yaml['status']
      run_result["metrics"] = nest_metrics(last_run_report_yaml['metrics'])

      return run_result
    end

    # Wait for the lockfile to be removed. If it hasn't after 10 minutes, give up.
    def wait_for_lockfile(lockfile, check_interval = 0.1, give_up_after = 10*60)
      number_of_tries = give_up_after / check_interval
      count = 0
      while File.exist?(lockfile) && count < number_of_tries
        sleep check_interval
        count += 1
      end
    end

    # Determine whether the configured puppet bin exists. This method mostly
    # exists for testing.
    def puppet_bin_present?
      File.exist?(config["puppet_bin"])
    end

    def get_start_time(last_run_report)
      File.mtime(last_run_report) if File.exist?(last_run_report)
    end

    def try_run(last_run_report)
      start_time = get_start_time(last_run_report)
      run_result = Puppet::Util::Execution.execute(puppet_agent_command, {:failonfail => false,
                                                                          :custom_environment => make_environment_hash(),
                                                                          :override_locale => false})
      return start_time, (run_result ? run_result.exitstatus : nil)
    end

    def run
      if !puppet_bin_present?
        return make_error_result(DEFAULT_EXITCODE, Errors::NoPuppetBin,
                                 "Puppet executable '#{config["puppet_bin"]}' does not exist")
      end

      puppet_config = config_print('lastrunreport', 'agent_disabled_lockfile', 'agent_catalog_run_lockfile')
      last_run_report = puppet_config['lastrunreport']

      if last_run_report.nil? || last_run_report.empty?
        return make_error_result(DEFAULT_EXITCODE, Errors::NoLastRunReport,
                                 "could not find the location of the last run report")
      end

      # Initially ignore the lockfile. It might be out-dated, so we give Puppet a chance
      # to clean it up and run.
      start_time, exitcode = try_run(last_run_report)
      if exitcode.nil?
        return make_error_result(DEFAULT_EXITCODE, Errors::FailedToStart,
                                 "Failed to start Puppet agent")
      end

      # If the run was successful, don't check for failure modes.
      if exitcode != 0
        if disabled?(puppet_config['agent_disabled_lockfile'] || '')
          return make_error_result(exitcode, Errors::Disabled,
                                   "Puppet agent is disabled")
        end

        # Check for a lockfile. If present, wait until it's removed and try running again.
        # There's a chance that our run finished with a real error rather than because Puppet was
        # already running, but another run started immediately after. Since we have no
        # language-agnostic way to tell, we accept that we might run twice in that case.
        # The run could also finish immediately after we tried, and the lockfile be absent.
        # In that case we'll fail with poor error reporting.
        lockfile = puppet_config['agent_catalog_run_lockfile'] || ''
        if running?(lockfile)
          wait_for_lockfile(lockfile)

          start_time, exitcode = try_run(last_run_report)
          if exitcode.nil?
            return make_error_result(DEFAULT_EXITCODE, Errors::FailedToStart,
                                     "Failed to start Puppet agent")
          end

          if exitcode != 0
            if disabled?(puppet_config['agent_disabled_lockfile'] || '')
              return make_error_result(exitcode, Errors::Disabled,
                                       "Puppet agent is disabled")
            end

            if running?(lockfile)
              return make_error_result(exitcode, Errors::Locked,
                                       "Puppet agent run is already in progress")
            end
          end
        end
      end

      return get_result_from_report(last_run_report, exitcode, start_time)
    end

    # TODO(ale): remove `env` from input before bumping to the next version
    def self.metadata()
      return {
        :description => "PXP Puppet module",
        :actions => [
          { :name        => "run",
            :description => "Start a Puppet run",
            :input       => {
              :type      => "object",
              :properties => {
                :env => {
                  :type => "array",
                },
                :flags => {
                  :type => "array",
                  :items => {
                    :type => "string"
                  }
                },
                :job => {
                  :type => "string"
                }
              },
              :required => [:flags]
            },
            :results => {
              :type => "object",
              :properties => {
                :time => {
                  :type => "string"
                },
                :transaction_uuid => {
                  :type => "string"
                },
                :metrics => {
                  :type => "object"
                },
                :environment => {
                  :type => "string"
                },
                :status => {
                  :type => "string"
                },
                :error_type => {
                  :type => "string"
                },
                :error => {
                  :type => "string"
                },
                :exitcode => {
                  :type => "number"
                },
                :version => {
                  :type => "number"
                }
              },
              :required => [:time, :transaction_uuid, :environment, :status,
                            :exitcode, :version]
            }
          }
        ],
        :configuration => {
          :type => "object",
          :properties => {
            :puppet_bin => {
              :type => "string"
            }
          }
        }
      }
    end

    def self.add_config_defaults(config)
      config = config.dup
      if config["puppet_bin"].nil? || config["puppet_bin"].empty?
        if !Puppet.features.microsoft_windows?
          config["puppet_bin"] = "/opt/puppetlabs/bin/puppet"
        else
          module_path = File.expand_path(File.dirname(__FILE__))
          puppet_bin = File.join(module_path, '..', '..', 'bin', 'puppet.bat')
          config["puppet_bin"] = File.expand_path(puppet_bin)
        end
      end

      config
    end

    DEFAULT_FLAGS = ["--onetime", "--no-daemonize", "--verbose"]

    DEFAULT_FLAGS_NAMES = ["onetime", "daemonize", "verbose"]

    WHITELISTED_FLAGS_NAMES = [
      "color", "configtimeout",
      "debug","disable_warnings",
      "environment", "evaltrace",
      "filetimeout",
      "graph",
      "http_connect_timeout", "http_debug", "http_keepalive_timeout", "http_read_timeout",
      "log_level",
      "noop",
      "ordering",
      "pluginsync",
      "show_diff", "skip_tags", "splay", "strict_environment_mode",
      "tags", "trace",
      "use_cached_catalog", "usecacheonfailure",
      "waitforcert"]

    # All flags and valid arguments to them should be caught by this.
    #  It was constructed for the following argument types:
    #  "timeout": /\A\d+[mhdy]?\Z/,
    #  "environment": /\A[a-z0-9_]+\Z/,
    #  "tag": /\A[a-z0-9_][a-z0-9_:\.\-]*\Z/,
    #  "ordering": /\Atitle-hash|manifest|random\Z/
    VALID_FLAG_REGEX = /\A[a-zA-Z0-9_:,\.\-]+\Z/

    # This asserts that the flag has a valid prefix
    def self.get_flag_name(flag)
      if flag.start_with?("--no-")
        flag[5..-1]
      elsif flag.start_with?("--")
        flag[2..-1]
      else
        raise "Assertion error: we're here by mistake"
      end
    end

    def self.process_flags(action_input)
      flags = action_input['flags'] || []

      flags.each do |flag|
        flag = flag.strip
        unless flag =~ VALID_FLAG_REGEX
          raise ProcessingError.new(Errors::InvalidJson, "The json received on STDIN contained characters not present in valid flags: #{flag}")
        end

        if flag.start_with?("--")
          flag_name = get_flag_name(flag)

          if DEFAULT_FLAGS_NAMES.include?(flag_name)
            unless DEFAULT_FLAGS.include?(flag)
              raise ProcessingError.new(Errors::InvalidJson, "The json received on STDIN overrides a default setting with: #{flag}")
            end
            next
          end

          unless WHITELISTED_FLAGS_NAMES.include?(flag_name)
            raise ProcessingError.new(Errors::InvalidJson, "The json received on STDIN included a non-permitted flag: #{flag}")
          end
        end
      end

      flags |= DEFAULT_FLAGS

      if action_input.has_key?("job")
        flags += ["--job-id", action_input["job"]]
      end

      flags
    end

    def self.create_runner(input)
      begin
        args = JSON.parse(input)
      rescue
        return make_error_result(DEFAULT_EXITCODE, Errors::InvalidJson,
                                 "Invalid json received on STDIN: #{input}")
      end
      unless args.is_a?(Hash)
        return make_error_result(DEFAULT_EXITCODE, Errors::InvalidJson,
                                 "The json received on STDIN was not a hash: #{args.to_s}")
      end

      output_files = args["output_files"]
      if output_files
        begin
          $stdout.reopen(File.open(output_files["stdout"], 'w', 0640))
          $stderr.reopen(File.open(output_files["stderr"], 'w', 0640))
        rescue => e
          print make_error_result(DEFAULT_EXITCODE, Errors::InvalidJson,
                                  "Could not open output files: #{e.message}").to_json
          exit 5 # this exit code is reserved for problems with opening
                 # of the output_files
        end

        at_exit do
          status = if $!.nil?
            0
          elsif $!.is_a?(SystemExit)
            $!.status
          else
            1
          end

          # flush the stdout/stderr before writing the exitcode
          # file to avoid pxp-agent reading incomplete output
          $stdout.fsync
          $stderr.fsync
          begin
            File.open(output_files["exitcode"], 'w', 0640) do |f|
              f.puts(status)
            end
          rescue => e
            print make_error_result(DEFAULT_EXITCODE, Errors::InvalidJson,
                                    "Could not open exit code file: #{e.message}").to_json
            exit 5 # this exit code is reserved for problems with opening
                   # of the output_files
          end
        end
      end

      begin
        config = add_config_defaults(args["configuration"] || {})

        action_input = args["input"]
        unless action_input.is_a?(Hash)
          raise ProcessingError.new(Errors::InvalidJson, "The json received on STDIN did not contain a valid 'input' key: #{args.to_s}")
        end

        flags = process_flags(action_input)

        new(config, flags)
      rescue ProcessingError => e
        return make_error_result(DEFAULT_EXITCODE, e.error_type, e.message)
      end
    end

    def initialize(config, flags)
      @config = config
      @flags = flags
    end

  end
end

if __FILE__ == $0
  action = ARGV.shift || 'metadata'
  Pxp::ModulePuppet.handle_action(action)
end

Hacked By AnonymousFox1.0, Coded By AnonymousFox