require "puppet" require "puppet_pal" require "tmpdir" module MCollective module Util class BoltSupport class PlanRunner def self.init_puppet TaskResults.include_iterable Puppet::Util::Log.newdestination(:console) end init_puppet attr_reader :modulepath # @param plan [String] the name of the plan to use # @param tmpdir [String] the path to an already existing temporary directory # @param modulepath [String,nil] a : seperated list of locations to look for modules, uses puppet basemodulepath if nil # @param loglevel [debug, info, warn, err] def initialize(plan, tmpdir, modulepath, loglevel) @plan = plan @loglevel = loglevel @tmpdir = tmpdir raise("A temporary directory could not be created") unless @tmpdir raise("A temporary directory could not be created") unless File.directory?(@tmpdir) @modulepath = modulepath unless @modulepath initialize_settings @modulepath = Puppet.settings[:basemodulepath] end @modulepath = @modulepath.split(":") Puppet[:log_level] = @loglevel end # Determines if the requested plan exist # # @return [Boolean] def exist? with_script_compiler do |compiler| return !!compiler.plan_signature(@plan) end end # Initialize Puppet to use the configured tmp dir def puppet_cli_options Puppet::Settings::REQUIRED_APP_SETTINGS.map do |setting| "--%s %s" % [setting, @tmpdir] end end # Retrieves the signature of a plan - its parameters and types # # NOTE: at present it's not possible to extract description or default values # # @return [Hash] def plan_signature with_script_compiler do |compiler| sig = compiler.plan_signature(@plan) raise("Cannot find playbook %s in %s" % [@plan, @modulepath.join(":")]) unless sig sig.params_type.elements.map do |elem| [elem.name, { "type" => elem.value_type.to_s, "required" => !elem.key_type.is_a?(Puppet::Pops::Types::POptionalType) }] end end end # Yields a PAL script compiler in the temporary environment def with_script_compiler(&block) in_environment do |env| env.with_script_compiler(&block) end end # Facts to use in the environment def facts { "choria" => { "playbook" => @plan } } end def initialize_settings Puppet.initialize_settings(puppet_cli_options) unless Puppet.settings.global_defaults_initialized? end # Sets up a temporary environment def in_environment(&block) initialize_settings Puppet::Pal.in_tmp_environment("choria", :modulepath => @modulepath, :facts => facts, &block) end # Converts a Puppet type into something mcollective understands # # This is inevitably hacky by its nature, there is no way for me to # parse the types. PAL might get some helpers for this but till then # this is going to have to be best efforts. # # When there is a too complex situation users can always put in --input # and some JSON to work around it until something better comes around # # @param type [String] a puppet type # @return [Class, Boolean] The data type, if its an array input or not def puppet_type_to_ruby(type) array = false type = $1 if type =~ /Optional\[(.+)/ if type =~ /Array\[(.+)/ type = $1 array = true end return [Numeric, array] if type =~ /Integer/ return [Numeric, array] if type =~ /Float/ return [Hash, array] if type =~ /Hash/ return [:boolean, array] if type =~ /Boolean/ [String, array] end def run!(params) with_script_compiler do |compiler| compiler.call_function("choria::run_playbook", @plan, params) end end # Adds the CLI options for an application based on the playbook inputs # # @param application [MCollective::Application] # @param set_required [Boolean] def add_cli_options(application, set_required=false) sig = plan_signature return if sig.nil? || sig.empty? sig.each do |name, details| type, array = puppet_type_to_ruby(details["type"]) properties = { :description => "Plan input property (%s)" % details["type"], :arguments => "--%s %s" % [name.downcase, name.upcase], :type => array ? :array : type } properties[:required] = true if details["required"] && set_required properties[:arguments] = "--[no-]%s" % name.downcase if type == :boolean application.class.option(name, properties) end end end end end end