eaiovnaovbqoebvqoeavibavo facter.rb000064400000014261147634041260006350 0ustar00# Facter - Host Fact Detection and Reporting # # Copyright 2011 Puppet Labs Inc # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require 'facter/version' # Functions as a hash of 'facts' about your system system, such as MAC # address, IP address, architecture, etc. # # @example Retrieve a fact # puts Facter['operatingsystem'].value # # @example Retrieve all facts # Facter.to_hash # => { "kernel"=>"Linux", "uptime_days"=>0, "ipaddress"=>"10.0.0.1" } # # @api public module Facter # Most core functionality of facter is implemented in `Facter::Util`. # @api public module Util; end require 'facter/util/fact' require 'facter/util/collection' include Comparable include Enumerable require 'facter/core/logging' extend Facter::Core::Logging # module methods # Accessor for the collection object which holds all the facts # @return [Facter::Util::Collection] the collection of facts # # @api private def self.collection unless defined?(@collection) and @collection @collection = Facter::Util::Collection.new( Facter::Util::Loader.new, Facter::Util::Config.ext_fact_loader) end @collection end # Returns whether the JSON "feature" is available. # # @api private def self.json? begin require 'json' true rescue LoadError false end end # Returns a fact object by name. If you use this, you still have to # call {Facter::Util::Fact#value `value`} on it to retrieve the actual # value. # # @param name [String] the name of the fact # # @return [Facter::Util::Fact, nil] The fact object, or nil if no fact # is found. # # @api public def self.[](name) collection.fact(name) end # (see []) def self.fact(name) collection.fact(name) end # Flushes cached values for all facts. This does not cause code to be # reloaded; it only clears the cached results. # # @return [void] # # @api public def self.flush collection.flush end # Lists all fact names # # @return [Array] array of fact names # # @api public def self.list collection.list end # Gets the value for a fact. Returns `nil` if no such fact exists. # # @param name [String] the fact name # @return [Object, nil] the value of the fact, or nil if no fact is # found # # @api public def self.value(name) collection.value(name) end # Gets a hash mapping fact names to their values # # @return [Hash{String => Object}] the hash of fact names and values # # @api public def self.to_hash collection.load_all collection.to_hash end # Define a new fact or extend an existing fact. # # @param name [Symbol] The name of the fact to define # @param options [Hash] A hash of options to set on the fact # # @return [Facter::Util::Fact] The fact that was defined # # @api public # @see {Facter::Util::Collection#define_fact} def self.define_fact(name, options = {}, &block) collection.define_fact(name, options, &block) end # Adds a {Facter::Util::Resolution resolution} mechanism for a named # fact. This does not distinguish between adding a new fact and adding # a new way to resolve a fact. # # @overload add(name, options = {}, { || ... }) # @param name [String] the fact name # @param options [Hash] optional parameters for the fact - attributes # of {Facter::Util::Fact} and {Facter::Util::Resolution} can be # supplied here # @option options [Integer] :timeout set the # {Facter::Util::Resolution#timeout timeout} for this resolution # @param block [Proc] a block defining a fact resolution # # @return [Facter::Util::Fact] the fact object, which includes any previously # defined resolutions # # @api public def self.add(name, options = {}, &block) collection.add(name, options, &block) end # Iterates over fact names and values # # @yieldparam [String] name the fact name # @yieldparam [String] value the current value of the fact # # @return [void] # # @api public def self.each # Make sure all facts are loaded. collection.load_all collection.each do |*args| yield(*args) end end # Clears all cached values and removes all facts from memory. # # @return [void] # # @api public def self.clear Facter.flush Facter.reset end # Removes all facts from memory. Use this when the fact code has # changed on disk and needs to be reloaded. # # @return [void] # # @api public def self.reset @collection = nil reset_search_path! end # Loads all facts. # # @return [void] # # @api public def self.loadfacts collection.load_all end # Register directories to be searched for facts. The registered directories # must be absolute paths or they will be ignored. # # @param dirs [String] directories to search # # @return [void] # # @api public def self.search(*dirs) @search_path += dirs end # Returns the registered search directories. # # @return [Array] An array of the directories searched # # @api public def self.search_path @search_path.dup end # Reset the Facter search directories. # # @api private # @return [void] def self.reset_search_path! @search_path = [] end reset_search_path! # Registers directories to be searched for external facts. # # @param dirs [Array] directories to search # # @return [void] # # @api public def self.search_external(dirs) Facter::Util::Config.external_facts_dirs += dirs end # Returns the registered search directories. # # @return [Array] An array of the directories searched # # @api public def self.search_external_path Facter::Util::Config.external_facts_dirs.dup end end semver.rb000064400000007541147634041260006410 0ustar00require 'puppet/util/monkey_patches' # We need to subclass Numeric to force range comparisons not to try to iterate over SemVer # and instead use numeric comparisons (eg >, <, >=, <=) # Ruby 1.8 already did this for all ranges, but Ruby 1.9 changed range include behavior class SemVer < Numeric include Comparable VERSION = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z-]*|)$/ SIMPLE_RANGE = /^v?(\d+|[xX])(?:\.(\d+|[xX])(?:\.(\d+|[xX]))?)?$/ def self.valid?(ver) VERSION =~ ver end def self.find_matching(pattern, versions) versions.select { |v| v.matched_by?("#{pattern}") }.sort.last end def self.pre(vstring) vstring =~ /-/ ? vstring : vstring + '-' end def self.[](range) range.gsub(/([><=])\s+/, '\1').split(/\b\s+(?!-)/).map do |r| case r when SemVer::VERSION SemVer.new(pre(r)) .. SemVer.new(r) when SemVer::SIMPLE_RANGE r += ".0" unless SemVer.valid?(r.gsub(/x/i, '0')) SemVer.new(r.gsub(/x/i, '0'))...SemVer.new(r.gsub(/(\d+)\.x/i) { "#{$1.to_i + 1}.0" } + '-') when /\s+-\s+/ a, b = r.split(/\s+-\s+/) SemVer.new(pre(a)) .. SemVer.new(b) when /^~/ ver = r.sub(/~/, '').split('.').map(&:to_i) start = (ver + [0] * (3 - ver.length)).join('.') ver.pop unless ver.length == 1 ver[-1] = ver.last + 1 finish = (ver + [0] * (3 - ver.length)).join('.') SemVer.new(pre(start)) ... SemVer.new(pre(finish)) when /^>=/ ver = r.sub(/^>=/, '') SemVer.new(pre(ver)) .. SemVer::MAX when /^<=/ ver = r.sub(/^<=/, '') SemVer::MIN .. SemVer.new(ver) when /^>/ if r =~ /-/ ver = [r[1..-1]] else ver = r.sub(/^>/, '').split('.').map(&:to_i) ver[2] = ver.last + 1 end SemVer.new(ver.join('.') + '-') .. SemVer::MAX when /^(other) other = SemVer.new("#{other}") unless other.is_a? SemVer return self.major <=> other.major unless self.major == other.major return self.minor <=> other.minor unless self.minor == other.minor return self.tiny <=> other.tiny unless self.tiny == other.tiny return 0 if self.special == other.special return 1 if self.special == '' return -1 if other.special == '' return self.special <=> other.special end def matched_by?(pattern) # For the time being, this is restricted to exact version matches and # simple range patterns. In the future, we should implement some or all of # the comparison operators here: # https://github.com/isaacs/node-semver/blob/d474801/semver.js#L340 case pattern when SIMPLE_RANGE pattern = SIMPLE_RANGE.match(pattern).captures pattern[1] = @minor unless pattern[1] && pattern[1] !~ /x/i pattern[2] = @tiny unless pattern[2] && pattern[2] !~ /x/i [@major, @minor, @tiny] == pattern.map { |x| x.to_i } when VERSION self == SemVer.new(pattern) else false end end def inspect @vstring || "v#{@major}.#{@minor}.#{@tiny}#{@special}" end alias :to_s :inspect MIN = SemVer.new('0.0.0-') MIN.instance_variable_set(:@vstring, 'vMIN') MAX = SemVer.new('8.0.0') MAX.instance_variable_set(:@major, Float::INFINITY) # => Infinity MAX.instance_variable_set(:@vstring, 'vMAX') end hiera.rb000064400000003555147634041260006200 0ustar00require 'yaml' class Hiera require "hiera/error" require "hiera/version" require "hiera/config" require "hiera/util" require "hiera/backend" require "hiera/console_logger" require "hiera/puppet_logger" require "hiera/noop_logger" require "hiera/fallback_logger" require "hiera/filecache" class << self attr_reader :logger # Loggers are pluggable, just provide a class called # Hiera::Foo_logger and respond to :warn and :debug # # See hiera-puppet for an example that uses the Puppet # loging system instead of our own def logger=(logger) require "hiera/#{logger}_logger" @logger = Hiera::FallbackLogger.new( Hiera.const_get("#{logger.capitalize}_logger"), Hiera::Console_logger) rescue Exception => e @logger = Hiera::Console_logger warn("Failed to load #{logger} logger: #{e.class}: #{e}") end def warn(msg); @logger.warn(msg); end def debug(msg); @logger.debug(msg); end end attr_reader :options, :config # If the config option is a string its assumed to be a filename, # else a hash of what would have been in the YAML config file def initialize(options={}) options[:config] ||= File.join(Util.config_dir, 'hiera.yaml') @config = Config.load(options[:config]) Config.load_backends end # Calls the backends to do the actual lookup. # # The scope can be anything that responds to [], if you have input # data like a Puppet Scope that does not you can wrap that data in a # class that has a [] method that fetches the data from your source. # See hiera-puppet for an example of this. # # The order-override will insert as first in the hierarchy a data source # of your choice. def lookup(key, default, scope, order_override=nil, resolution_type=:priority) Backend.lookup(key, default, scope, order_override, resolution_type) end end hiera/noop_logger.rb000064400000000166147634041260010505 0ustar00class Hiera module Noop_logger class << self def warn(msg);end def debug(msg);end end end end hiera/backend.rb000064400000020047147634041260007562 0ustar00require 'hiera/util' require 'hiera/interpolate' begin require 'deep_merge' rescue LoadError end class Hiera module Backend class << self # Data lives in /var/lib/hiera by default. If a backend # supplies a datadir in the config it will be used and # subject to variable expansion based on scope def datadir(backend, scope) backend = backend.to_sym if Config[backend] && Config[backend][:datadir] dir = Config[backend][:datadir] else dir = Hiera::Util.var_dir end if !dir.is_a?(String) raise(Hiera::InvalidConfigurationError, "datadir for #{backend} cannot be an array") end parse_string(dir, scope) end # Finds the path to a datafile based on the Backend#datadir # and extension # # If the file is not found nil is returned def datafile(backend, scope, source, extension) datafile_in(datadir(backend, scope), source, extension) end # @api private def datafile_in(datadir, source, extension) file = File.join(datadir, "#{source}.#{extension}") if File.exist?(file) file else Hiera.debug("Cannot find datafile #{file}, skipping") nil end end # Constructs a list of data sources to search # # If you give it a specific hierarchy it will just use that # else it will use the global configured one, failing that # it will just look in the 'common' data source. # # An override can be supplied that will be pre-pended to the # hierarchy. # # The source names will be subject to variable expansion based # on scope def datasources(scope, override=nil, hierarchy=nil) if hierarchy hierarchy = [hierarchy] elsif Config.include?(:hierarchy) hierarchy = [Config[:hierarchy]].flatten else hierarchy = ["common"] end hierarchy.insert(0, override) if override hierarchy.flatten.map do |source| source = parse_string(source, scope) yield(source) unless source == "" or source =~ /(^\/|\/\/|\/$)/ end end # Constructs a list of data files to search # # If you give it a specific hierarchy it will just use that # else it will use the global configured one, failing that # it will just look in the 'common' data source. # # An override can be supplied that will be pre-pended to the # hierarchy. # # The source names will be subject to variable expansion based # on scope # # Only files that exist will be returned. If the file is missing, then # the block will not receive the file. # # @yield [String, String] the source string and the name of the resulting file # @api public def datasourcefiles(backend, scope, extension, override=nil, hierarchy=nil) datadir = Backend.datadir(backend, scope) Backend.datasources(scope, override, hierarchy) do |source| Hiera.debug("Looking for data source #{source}") file = datafile_in(datadir, source, extension) if file yield source, file end end end # Parse a string like '%{foo}' against a supplied # scope and additional scope. If either scope or # extra_scope includes the variable 'foo', then it will # be replaced else an empty string will be placed. # # If both scope and extra_data has "foo", then the value in scope # will be used. # # @param data [String] The string to perform substitutions on. # This will not be modified, instead a new string will be returned. # @param scope [#[]] The primary source of data for substitutions. # @param extra_data [#[]] The secondary source of data for substitutions. # @return [String] A copy of the data with all instances of %{...} replaced. # # @api public def parse_string(data, scope, extra_data={}) Hiera::Interpolate.interpolate(data, scope, extra_data) end # Parses a answer received from data files # # Ultimately it just pass the data through parse_string but # it makes some effort to handle arrays of strings as well def parse_answer(data, scope, extra_data={}) if data.is_a?(Numeric) or data.is_a?(TrueClass) or data.is_a?(FalseClass) return data elsif data.is_a?(String) return parse_string(data, scope, extra_data) elsif data.is_a?(Hash) answer = {} data.each_pair do |key, val| interpolated_key = parse_string(key, scope, extra_data) answer[interpolated_key] = parse_answer(val, scope, extra_data) end return answer elsif data.is_a?(Array) answer = [] data.each do |item| answer << parse_answer(item, scope, extra_data) end return answer end end def resolve_answer(answer, resolution_type) case resolution_type when :array [answer].flatten.uniq.compact when :hash answer # Hash structure should be preserved else answer end end # Merges two hashes answers with the configured merge behavior. # :merge_behavior: {:native|:deep|:deeper} # # Deep merge options use the Hash utility function provided by [deep_merge](https://github.com/peritor/deep_merge) # # :native => Native Hash.merge # :deep => Use Hash.deep_merge # :deeper => Use Hash.deep_merge! # def merge_answer(left,right) case Config[:merge_behavior] when :deeper,'deeper' left.deep_merge!(right) when :deep,'deep' left.deep_merge(right) else # Native and undefined left.merge(right) end end # Calls out to all configured backends in the order they # were specified. The first one to answer will win. # # This lets you declare multiple backends, a possible # use case might be in Puppet where a Puppet module declares # default data using in-module data while users can override # using JSON/YAML etc. By layering the backends and putting # the Puppet one last you can override module author data # easily. # # Backend instances are cached so if you need to connect to any # databases then do so in your constructor, future calls to your # backend will not create new instances def lookup(key, default, scope, order_override, resolution_type) @backends ||= {} answer = nil Config[:backends].each do |backend| if constants.include?("#{backend.capitalize}_backend") || constants.include?("#{backend.capitalize}_backend".to_sym) @backends[backend] ||= Backend.const_get("#{backend.capitalize}_backend").new new_answer = @backends[backend].lookup(key, scope, order_override, resolution_type) if not new_answer.nil? case resolution_type when :array raise Exception, "Hiera type mismatch: expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String answer ||= [] answer << new_answer when :hash raise Exception, "Hiera type mismatch: expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash answer ||= {} answer = merge_answer(new_answer,answer) else answer = new_answer break end end end end answer = resolve_answer(answer, resolution_type) unless answer.nil? answer = parse_string(default, scope) if answer.nil? and default.is_a?(String) return default if answer.nil? return answer end def clear! @backends = {} end end end end hiera/puppet_function.rb000064400000004246147634041260011420 0ustar00require 'hiera_puppet' # Provides the base class for the puppet functions hiera, hiera_array, hiera_hash, and hiera_include. # The actual function definitions will call init_dispatch and override the merge_type and post_lookup methods. # # @see hiera_array.rb, hiera_include.rb under lib/puppet/functions for sample usage # class Hiera::PuppetFunction < Puppet::Functions::InternalFunction def self.init_dispatch dispatch :hiera_splat do scope_param param 'Tuple[String, Any, Any, 1, 3]', :args end dispatch :hiera_no_default do scope_param param 'String',:key end dispatch :hiera_with_default do scope_param param 'String',:key param 'Any', :default optional_param 'Any', :override end dispatch :hiera_block1 do scope_param param 'String', :key block_param 'Callable[1,1]', :default_block end dispatch :hiera_block2 do scope_param param 'String', :key param 'Any', :override block_param 'Callable[1,1]', :default_block end end def hiera_splat(scope, args) hiera(scope, *args) end def hiera_no_default(scope, key) post_lookup(scope, key, lookup(scope, key, nil, nil)) end def hiera_with_default(scope, key, default, override = nil) undefined = (@@undefined_value ||= Object.new) result = lookup(scope, key, undefined, override) post_lookup(scope, key, result.equal?(undefined) ? default : result) end def hiera_block1(scope, key, &default_block) common(scope, key, nil, default_block) end def hiera_block2(scope, key, override, &default_block) common(scope, key, override, default_block) end def common(scope, key, override, default_block) undefined = (@@undefined_value ||= Object.new) result = lookup(scope, key, undefined, override) post_lookup(scope, key, result.equal?(undefined) ? default_block.call(key) : result) end private :common def lookup(scope, key, default, override) HieraPuppet.lookup(key, default, scope, override, merge_type) end def merge_type :priority end def post_lookup(scope, key, result) result end end hiera/console_logger.rb000064400000000375147634041260011176 0ustar00class Hiera module Console_logger class << self def warn(msg) STDERR.puts("WARN: %s: %s" % [Time.now.to_s, msg]) end def debug(msg) STDERR.puts("DEBUG: %s: %s" % [Time.now.to_s, msg]) end end end end hiera/fallback_logger.rb000064400000002130147634041260011262 0ustar00# Select from a given list of loggers the first one that # it suitable and use that as the actual logger # # @api private class Hiera::FallbackLogger # Chooses the first suitable logger. For all of the loggers that are # unsuitable it will issue a warning using the suitable logger stating that # the unsuitable logger is not being used. # # @param implementations [Array] the implementations to choose from # @raises when there are no suitable loggers def initialize(*implementations) warnings = [] @implementation = implementations.find do |impl| if impl.respond_to?(:suitable?) if impl.suitable? true else warnings << "Not using #{impl.name}. It does not report itself to be suitable." false end else true end end if @implementation.nil? raise "No suitable logging implementation found." end warnings.each { |message| warn(message) } end def warn(message) @implementation.warn(message) end def debug(message) @implementation.debug(message) end end hiera/error.rb000064400000000142147634041260007316 0ustar00class Hiera class Error < StandardError; end class InvalidConfigurationError < Error; end end hiera/config.rb000064400000004732147634041260007443 0ustar00class Hiera::Config class << self ## # load takes a string or hash as input, strings are treated as filenames # hashes are stored as data that would have been in the config file # # Unless specified it will only use YAML as backend with a single # 'common' hierarchy and console logger # # @return [Hash] representing the configuration. e.g. # {:backends => "yaml", :hierarchy => "common"} def load(source) @config = {:backends => "yaml", :hierarchy => "common", :merge_behavior => :native } if source.is_a?(String) if File.exist?(source) config = begin yaml_load_file(source) rescue TypeError => detail case detail.message when /no implicit conversion from nil to integer/ false else raise detail end end @config.merge! config if config else raise "Config file #{source} not found" end elsif source.is_a?(Hash) @config.merge! source end @config[:backends] = [ @config[:backends] ].flatten if @config.include?(:logger) Hiera.logger = @config[:logger].to_s else @config[:logger] = "console" Hiera.logger = "console" end self.validate! @config end def validate! case @config[:merge_behavior] when :deep,'deep',:deeper,'deeper' begin require "deep_merge" rescue LoadError Hiera.warn "Ignoring configured merge_behavior" Hiera.warn "Must have 'deep_merge' gem installed." @config[:merge_behavior] = :native end end end ## # yaml_load_file directly delegates to YAML.load_file and is intended to be # a private, internal method suitable for stubbing and mocking. # # @return [Object] return value of {YAML.load_file} def yaml_load_file(source) YAML.load_file(source) end private :yaml_load_file def load_backends @config[:backends].each do |backend| begin require "hiera/backend/#{backend.downcase}_backend" rescue LoadError => e Hiera.warn "Cannot load backend #{backend}: #{e}" end end end def include?(key) @config.include?(key) end def [](key) @config[key] end end end hiera/util.rb000064400000001516147634041260007150 0ustar00class Hiera module Util module_function def posix? require 'etc' Etc.getpwuid(0) != nil end def microsoft_windows? return false unless file_alt_separator begin require 'win32/dir' true rescue LoadError => err warn "Cannot run on Microsoft Windows without the win32-dir gem: #{err}" false end end def config_dir if microsoft_windows? File.join(common_appdata, 'PuppetLabs', 'hiera', 'etc') else '/etc' end end def var_dir if microsoft_windows? File.join(common_appdata, 'PuppetLabs', 'hiera', 'var') else '/var/lib/hiera' end end def file_alt_separator File::ALT_SEPARATOR end def common_appdata Dir::COMMON_APPDATA end end end hiera/backend/yaml_backend.rb000064400000003700147634041260012170 0ustar00class Hiera module Backend class Yaml_backend def initialize(cache=nil) require 'yaml' Hiera.debug("Hiera YAML backend starting") @cache = cache || Filecache.new end def lookup(key, scope, order_override, resolution_type) answer = nil Hiera.debug("Looking up #{key} in YAML backend") Backend.datasourcefiles(:yaml, scope, "yaml", order_override) do |source, yamlfile| data = @cache.read_file(yamlfile, Hash) do |data| YAML.load(data) || {} end next if data.empty? next unless data.include?(key) # Extra logging that we found the key. This can be outputted # multiple times if the resolution type is array or hash but that # should be expected as the logging will then tell the user ALL the # places where the key is found. Hiera.debug("Found #{key} in #{source}") # for array resolution we just append to the array whatever # we find, we then goes onto the next file and keep adding to # the array # # for priority searches we break after the first found data item new_answer = Backend.parse_answer(data[key], scope) case resolution_type when :array raise Exception, "Hiera type mismatch: expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String answer ||= [] answer << new_answer when :hash raise Exception, "Hiera type mismatch: expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash answer ||= {} answer = Backend.merge_answer(new_answer,answer) else answer = new_answer break end end return answer end private def file_exists?(path) File.exist? path end end end end hiera/backend/puppet_backend.rb000064400000005507147634041260012552 0ustar00require 'hiera/backend' class Hiera module Backend class Puppet_backend def initialize Hiera.debug("Hiera Puppet backend starting") end def hierarchy(scope, override) begin data_class = Config[:puppet][:datasource] || "data" rescue data_class = "data" end calling_class = scope.resource.name.to_s.downcase calling_module = calling_class.split("::").first hierarchy = Config[:hierarchy] || [calling_class, calling_module] hierarchy = [hierarchy].flatten.map do |klass| klass = Backend.parse_string(klass, scope, { "calling_class" => calling_class, "calling_module" => calling_module } ) next if klass == "" [data_class, klass].join("::") end.compact hierarchy << [calling_class, data_class].join("::") unless calling_module == calling_class hierarchy << [calling_module, data_class].join("::") end hierarchy.insert(0, [data_class, override].join("::")) if override hierarchy end def lookup(key, scope, order_override, resolution_type) answer = nil Hiera.debug("Looking up #{key} in Puppet backend") Puppet::Parser::Functions.function(:include) loaded_classes = scope.catalog.classes hierarchy(scope, order_override).each do |klass| Hiera.debug("Looking for data in #{klass}") varname = [klass, key].join("::") temp_answer = nil unless loaded_classes.include?(klass) begin if scope.respond_to?(:function_include) scope.function_include([klass]) else scope.real.function_include([klass]) end temp_answer = scope[varname] Hiera.debug("Found data in class #{klass}") rescue end else temp_answer = scope[varname] end # Note that temp_answer might be define but false. if temp_answer.nil? next else # For array resolution we just append to the array whatever we # find, we then go onto the next file and keep adding to the array. # # For priority searches we break after the first found data item. case resolution_type when :array answer ||= [] answer << Backend.parse_answer(temp_answer, scope) when :hash answer ||= {} answer = Backend.parse_answer(temp_answer, scope).merge answer else answer = Backend.parse_answer(temp_answer, scope) break end end end answer end end end end hiera/backend/json_backend.rb000064400000003305147634041260012200 0ustar00class Hiera module Backend class Json_backend def initialize(cache=nil) require 'json' Hiera.debug("Hiera JSON backend starting") @cache = cache || Filecache.new end def lookup(key, scope, order_override, resolution_type) answer = nil Hiera.debug("Looking up #{key} in JSON backend") Backend.datasources(scope, order_override) do |source| Hiera.debug("Looking for data source #{source}") jsonfile = Backend.datafile(:json, scope, source, "json") || next next unless File.exist?(jsonfile) data = @cache.read_file(jsonfile, Hash) do |data| JSON.parse(data) end next if data.empty? next unless data.include?(key) # for array resolution we just append to the array whatever # we find, we then goes onto the next file and keep adding to # the array # # for priority searches we break after the first found data item new_answer = Backend.parse_answer(data[key], scope) case resolution_type when :array raise Exception, "Hiera type mismatch: expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String answer ||= [] answer << new_answer when :hash raise Exception, "Hiera type mismatch: expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash answer ||= {} answer = Backend.merge_answer(new_answer,answer) else answer = new_answer break end end return answer end end end end hiera/scope.rb000064400000002101147634041260007273 0ustar00class Hiera class Scope CALLING_CLASS = "calling_class" CALLING_MODULE = "calling_module" MODULE_NAME = "module_name" attr_reader :real def initialize(real) @real = real end def [](key) if key == CALLING_CLASS ans = find_hostclass(@real) elsif key == CALLING_MODULE ans = @real.lookupvar(MODULE_NAME) else ans = @real.lookupvar(key) end if ans.nil? or ans == "" nil else ans end end def include?(key) if key == CALLING_CLASS or key == CALLING_MODULE true else @real.lookupvar(key) != "" end end def catalog @real.catalog end def resource @real.resource end def compiler @real.compiler end def find_hostclass(scope) if scope.source and scope.source.type == :hostclass return scope.source.name.downcase elsif scope.parent return find_hostclass(scope.parent) else return nil end end private :find_hostclass end end hiera/interpolate.rb000064400000003630147634041260010520 0ustar00require 'hiera/backend' require 'hiera/recursive_guard' class Hiera::Interpolate class << self INTERPOLATION = /%\{([^\}]*)\}/ METHOD_INTERPOLATION = /%\{(scope|hiera)\(['"]([^"']*)["']\)\}/ def interpolate(data, scope, extra_data) if data.is_a?(String) # Wrapping do_interpolation in a gsub block ensures we process # each interpolation site in isolation using separate recursion guards. data.gsub(INTERPOLATION) do |match| do_interpolation(match, Hiera::RecursiveGuard.new, scope, extra_data) end else data end end def do_interpolation(data, recurse_guard, scope, extra_data) if data.is_a?(String) && (match = data.match(INTERPOLATION)) interpolation_variable = match[1] recurse_guard.check(interpolation_variable) do interpolate_method, key = get_interpolation_method_and_key(data) interpolated_data = send(interpolate_method, data, key, scope, extra_data) do_interpolation(interpolated_data, recurse_guard, scope, extra_data) end else data end end private :do_interpolation def get_interpolation_method_and_key(data) if (match = data.match(METHOD_INTERPOLATION)) case match[1] when 'hiera' then [:hiera_interpolate, match[2]] when 'scope' then [:scope_interpolate, match[2]] end elsif (match = data.match(INTERPOLATION)) [:scope_interpolate, match[1]] end end private :get_interpolation_method_and_key def scope_interpolate(data, key, scope, extra_data) value = scope[key] if value.nil? || value == :undefined value = extra_data[key] end value end private :scope_interpolate def hiera_interpolate(data, key, scope, extra_data) Hiera::Backend.lookup(key, nil, scope, nil, :priority) end private :hiera_interpolate end end hiera/version.rb000064400000006771147634041260007670 0ustar00# The version method and constant are isolated in hiera/version.rb so that a # simple `require 'hiera/version'` allows a rubygems gemspec or bundler # Gemfile to get the hiera version of the gem install. # # The version is programatically settable because we want to allow the # Raketasks and such to set the version based on the output of `git describe` class Hiera VERSION = "1.3.4" ## # version is a public API method intended to always provide a fast and # lightweight way to determine the version of hiera. # # The intent is that software external to hiera be able to determine the # hiera version with no side-effects. The expected use is: # # require 'hiera/version' # version = Hiera.version # # This function has the following ordering precedence. This precedence list # is designed to facilitate automated packaging tasks by simply writing to # the VERSION file in the same directory as this source file. # # 1. If a version has been explicitly assigned using the Hiera.version= # method, return that version. # 2. If there is a VERSION file, read the contents, trim any # trailing whitespace, and return that version string. # 3. Return the value of the Hiera::VERSION constant hard-coded into # the source code. # # If there is no VERSION file, the method must return the version string of # the nearest parent version that is an officially released version. That is # to say, if a branch named 3.1.x contains 25 patches on top of the most # recent official release of 3.1.1, then the version method must return the # string "3.1.1" if no "VERSION" file is present. # # By design the version identifier is _not_ intended to vary during the life # a process. There is no guarantee provided that writing to the VERSION file # while a Hiera process is running will cause the version string to be # updated. On the contrary, the contents of the VERSION are cached to reduce # filesystem accesses. # # The VERSION file is intended to be used by package maintainers who may be # applying patches or otherwise changing the software version in a manner # that warrants a different software version identifier. The VERSION file is # intended to be managed and owned by the release process and packaging # related tasks, and as such should not reside in version control. The # VERSION constant is intended to be version controlled in history. # # Ideally, this behavior will allow package maintainers to precisely specify # the version of the software they're packaging as in the following example: # # $ git describe --match "1.2.*" > lib/hiera/VERSION # $ ruby -r hiera/version -e 'puts Hiera.version' # 1.2.1-9-g9fda440 # # @api public # # @return [String] containing the hiera version, e.g. "1.2.1" def self.version version_file = File.join(File.dirname(__FILE__), 'VERSION') return @hiera_version if @hiera_version if version = read_version_file(version_file) @hiera_version = version end @hiera_version ||= VERSION end def self.version=(version) @hiera_version = version end ## # read_version_file reads the content of the "VERSION" file that lives in the # same directory as this source code file. # # @api private # # @return [String] for example: "1.6.14-6-gea42046" or nil if the VERSION # file does not exist. def self.read_version_file(path) if File.exists?(path) File.read(path).chomp end end private_class_method :read_version_file end hiera/filecache.rb000064400000005146147634041260010101 0ustar00class Hiera class Filecache def initialize @cache = {} end # Reads a file, optionally parse it in some way check the # output type and set a default # # Simply invoking it this way will return the file contents # # data = read("/some/file") # # But as most cases of file reading in hiera involves some kind # of parsing through a serializer there's some help for those # cases: # # data = read("/some/file", Hash, {}) do |data| # JSON.parse(data) # end # # In this case it will read the file, parse it using JSON then # check that the end result is a Hash, if it's not a hash or if # reading/parsing fails it will return {} instead # # Prior to calling this method you should be sure the file exist def read(path, expected_type = Object, default=nil, &block) read_file(path, expected_type, &block) rescue TypeError => detail Hiera.debug("#{detail.message}, setting defaults") @cache[path][:data] = default rescue => detail error = "Reading data from #{path} failed: #{detail.class}: #{detail}" if default.nil? raise detail else Hiera.debug(error) @cache[path][:data] = default end end # Read a file when it changes. If a file is re-read and has not changed since the last time # then the last, processed, contents will be returned. # # The processed data can also be checked against an expected type. If the # type does not match a TypeError is raised. # # No error handling is done inside this method. Any failed reads or errors # in processing will be propagated to the caller def read_file(path, expected_type = Object) if stale?(path) data = File.read(path) @cache[path][:data] = block_given? ? yield(data) : data if !@cache[path][:data].is_a?(expected_type) raise TypeError, "Data retrieved from #{path} is #{data.class} not #{expected_type}" end end @cache[path][:data] end private def stale?(path) meta = path_metadata(path) @cache[path] ||= {:data => nil, :meta => nil} if @cache[path][:meta] == meta return false else @cache[path][:meta] = meta return true end end # This is based on the old caching in the YAML backend and has a # resolution of 1 second, changes made within the same second of # a previous read will be ignored def path_metadata(path) stat = File.stat(path) {:inode => stat.ino, :mtime => stat.mtime, :size => stat.size} end end end hiera/puppet_logger.rb000064400000000436147634041260011047 0ustar00class Hiera module Puppet_logger class << self def suitable? defined?(::Puppet) == "constant" end def warn(msg) Puppet.notice("hiera(): #{msg}") end def debug(msg) Puppet.debug("hiera(): #{msg}") end end end end hiera/recursive_guard.rb000064400000000644147634041260011365 0ustar00# Allow for safe recursive lookup of values during variable interpolation. # # @api private class Hiera::InterpolationLoop < StandardError; end class Hiera::RecursiveGuard def initialize @seen = [] end def check(value, &block) if @seen.include?(value) raise Hiera::InterpolationLoop, "Detected in [#{@seen.join(', ')}]" end @seen.push(value) ret = yield @seen.pop ret end end hiera_puppet.rb000064400000003775147634041260007601 0ustar00require 'hiera' require 'hiera/scope' require 'puppet' module HieraPuppet module_function def lookup(key, default, scope, override, resolution_type) scope = Hiera::Scope.new(scope) answer = hiera.lookup(key, default, scope, override, resolution_type) if answer.nil? raise(Puppet::ParseError, "Could not find data item #{key} in any Hiera data file and no default supplied") end answer end def parse_args(args) # Functions called from Puppet manifests like this: # # hiera("foo", "bar") # # Are invoked internally after combining the positional arguments into a # single array: # # func = function_hiera # func(["foo", "bar"]) # # Functions called from templates preserve the positional arguments: # # scope.function_hiera("foo", "bar") # # Deal with Puppet's special calling mechanism here. if args[0].is_a?(Array) args = args[0] end if args.empty? raise(Puppet::ParseError, "Please supply a parameter to perform a Hiera lookup") end key = args[0] default = args[1] override = args[2] return [key, default, override] end private module_function def hiera @hiera ||= Hiera.new(:config => hiera_config) end def hiera_config config = {} if config_file = hiera_config_file config = Hiera::Config.load(config_file) end config[:logger] = 'puppet' config end def hiera_config_file config_file = nil if Puppet.settings[:hiera_config].is_a?(String) expanded_config_file = File.expand_path(Puppet.settings[:hiera_config]) if Puppet::FileSystem.exist?(expanded_config_file) config_file = expanded_config_file end elsif Puppet.settings[:confdir].is_a?(String) expanded_config_file = File.expand_path(File.join(Puppet.settings[:confdir], '/hiera.yaml')) if Puppet::FileSystem.exist?(expanded_config_file) config_file = expanded_config_file end end config_file end end puppet.rb000064400000023404147634041260006420 0ustar00require 'puppet/version' # see the bottom of the file for further inclusions # Also see the new Vendor support - towards the end # require 'facter' require 'puppet/error' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/settings' require 'puppet/util/feature' require 'puppet/util/suidmanager' require 'puppet/util/run_mode' require 'puppet/external/pson/common' require 'puppet/external/pson/version' require 'puppet/external/pson/pure' #------------------------------------------------------------ # the top-level module # # all this really does is dictate how the whole system behaves, through # preferences for things like debugging # # it's also a place to find top-level commands like 'debug' # The main Puppet class. Everything is contained here. # # @api public module Puppet require 'puppet/file_system' require 'puppet/context' require 'puppet/environments' class << self include Puppet::Util attr_reader :features attr_writer :name end # the hash that determines how our system behaves @@settings = Puppet::Settings.new # Note: It's important that these accessors (`self.settings`, `self.[]`) are # defined before we try to load any "features" (which happens a few lines below), # because the implementation of the features loading may examine the values of # settings. def self.settings @@settings end # Get the value for a setting # # @param [Symbol] param the setting to retrieve # # @api public def self.[](param) if param == :debug return Puppet::Util::Log.level == :debug else return @@settings[param] end end # The services running in this process. @services ||= [] require 'puppet/util/logging' extend Puppet::Util::Logging # The feature collection @features = Puppet::Util::Feature.new('puppet/feature') # Load the base features. require 'puppet/feature/base' # Store a new default value. def self.define_settings(section, hash) @@settings.define_settings(section, hash) end # setting access and stuff def self.[]=(param,value) @@settings[param] = value # Ensure that all environment caches are cleared if we're changing the parser lookup(:environments).clear_all if param == :parser end def self.clear @@settings.clear end def self.debug=(value) if value Puppet::Util::Log.level=(:debug) else Puppet::Util::Log.level=(:notice) end end def self.run_mode # This sucks (the existence of this method); there are a lot of places in our code that branch based the value of # "run mode", but there used to be some really confusing code paths that made it almost impossible to determine # when during the lifecycle of a puppet application run the value would be set properly. A lot of the lifecycle # stuff has been cleaned up now, but it still seems frightening that we rely so heavily on this value. # # I'd like to see about getting rid of the concept of "run_mode" entirely, but there are just too many places in # the code that call this method at the moment... so I've settled for isolating it inside of the Settings class # (rather than using a global variable, as we did previously...). Would be good to revisit this at some point. # # --cprice 2012-03-16 Puppet::Util::RunMode[@@settings.preferred_run_mode] end # Load all of the settings. require 'puppet/defaults' def self.genmanifest if Puppet[:genmanifest] puts Puppet.settings.to_manifest exit(0) end end # Parse the config file for this process. # @deprecated Use {initialize_settings} def self.parse_config() Puppet.deprecation_warning("Puppet.parse_config is deprecated; please use Faces API (which will handle settings and state management for you), or (less desirable) call Puppet.initialize_settings") Puppet.initialize_settings end # Initialize puppet's settings. This is intended only for use by external tools that are not # built off of the Faces API or the Puppet::Util::Application class. It may also be used # to initialize state so that a Face may be used programatically, rather than as a stand-alone # command-line tool. # # @api public # @param args [Array] the command line arguments to use for initialization # @return [void] def self.initialize_settings(args = []) do_initialize_settings_for_run_mode(:user, args) end # Initialize puppet's settings for a specified run_mode. # # @deprecated Use {initialize_settings} def self.initialize_settings_for_run_mode(run_mode) Puppet.deprecation_warning("initialize_settings_for_run_mode may be removed in a future release, as may run_mode itself") do_initialize_settings_for_run_mode(run_mode, []) end # private helper method to provide the implementation details of initializing for a run mode, # but allowing us to control where the deprecation warning is issued def self.do_initialize_settings_for_run_mode(run_mode, args) Puppet.settings.initialize_global_settings(args) run_mode = Puppet::Util::RunMode[run_mode] Puppet.settings.initialize_app_defaults(Puppet::Settings.app_defaults_for_run_mode(run_mode)) Puppet.push_context(Puppet.base_context(Puppet.settings), "Initial context after settings initialization") Puppet::Parser::Functions.reset Puppet::Util::Log.level = Puppet[:log_level] end private_class_method :do_initialize_settings_for_run_mode # Create a new type. Just proxy to the Type class. The mirroring query # code was deprecated in 2008, but this is still in heavy use. I suppose # this can count as a soft deprecation for the next dev. --daniel 2011-04-12 def self.newtype(name, options = {}, &block) Puppet::Type.newtype(name, options, &block) end # Load vendored (setup paths, and load what is needed upfront). # See the Vendor class for how to add additional vendored gems/code require "puppet/vendor" Puppet::Vendor.load_vendored # Set default for YAML.load to unsafe so we don't affect programs # requiring puppet -- in puppet we will call safe explicitly SafeYAML::OPTIONS[:default_mode] = :unsafe # The bindings used for initialization of puppet # @api private def self.base_context(settings) environments = settings[:environmentpath] modulepath = Puppet::Node::Environment.split_path(settings[:basemodulepath]) if environments.empty? loaders = [Puppet::Environments::Legacy.new] else loaders = Puppet::Environments::Directories.from_path(environments, modulepath) # in case the configured environment (used for the default sometimes) # doesn't exist default_environment = Puppet[:environment].to_sym if default_environment == :production loaders << Puppet::Environments::StaticPrivate.new( Puppet::Node::Environment.create(Puppet[:environment].to_sym, [], Puppet::Node::Environment::NO_MANIFEST)) end end { :environments => Puppet::Environments::Cached.new(Puppet::Environments::Combined.new(*loaders)), :http_pool => proc { require 'puppet/network/http' Puppet::Network::HTTP::NoCachePool.new } } end # A simple set of bindings that is just enough to limp along to # initialization where the {base_context} bindings are put in place # @api private def self.bootstrap_context root_environment = Puppet::Node::Environment.create(:'*root*', [], Puppet::Node::Environment::NO_MANIFEST) { :current_environment => root_environment, :root_environment => root_environment } end # @param overrides [Hash] A hash of bindings to be merged with the parent context. # @param description [String] A description of the context. # @api private def self.push_context(overrides, description = "") @context.push(overrides, description) end # Return to the previous context. # @raise [StackUnderflow] if the current context is the root # @api private def self.pop_context @context.pop end # Lookup a binding by name or return a default value provided by a passed block (if given). # @api private def self.lookup(name, &block) @context.lookup(name, &block) end # @param bindings [Hash] A hash of bindings to be merged with the parent context. # @param description [String] A description of the context. # @yield [] A block executed in the context of the temporarily pushed bindings. # @api private def self.override(bindings, description = "", &block) @context.override(bindings, description, &block) ensure lookup(:root_environment).instance_variable_set(:@future_parser, nil) end # @api private def self.mark_context(name) @context.mark(name) end # @api private def self.rollback_context(name) @context.rollback(name) end require 'puppet/node' # The single instance used for normal operation @context = Puppet::Context.new(bootstrap_context) # Is the future parser in effect for the given environment, or in :current_environment if no # environment is given. # def self.future_parser?(in_environment = nil) env = in_environment || Puppet.lookup(:current_environment) { return Puppet[:parser] == 'future' } env.future_parser? end end # This feels weird to me; I would really like for us to get to a state where there is never a "require" statement # anywhere besides the very top of a file. That would not be possible at the moment without a great deal of # effort, but I think we should strive for it and revisit this at some point. --cprice 2012-03-16 require 'puppet/indirector' require 'puppet/type' require 'puppet/resource' require 'puppet/parser' require 'puppet/network' require 'puppet/ssl' require 'puppet/module' require 'puppet/data_binding' require 'puppet/util/storage' require 'puppet/status' require 'puppet/file_bucket/file' puppet/property.rb000064400000066477147634041260010325 0ustar00# The virtual base class for properties, which are the self-contained building # blocks for actually doing work on the system. require 'puppet' require 'puppet/parameter' # The Property class is the implementation of a resource's attributes of _property_ kind. # A Property is a specialized Resource Type Parameter that has both an 'is' (current) state, and # a 'should' (wanted state). However, even if this is conceptually true, the current _is_ value is # obtained by asking the associated provider for the value, and hence it is not actually part of a # property's state, and only available when a provider has been selected and can obtain the value (i.e. when # running on an agent). # # A Property (also in contrast to a parameter) is intended to describe a managed attribute of # some system entity, such as the name or mode of a file. # # The current value _(is)_ is read and written with the methods {#retrieve} and {#set}, and the wanted # value _(should)_ is read and written with the methods {#value} and {#value=} which delegate to # {#should} and {#should=}, i.e. when a property is used like any other parameter, it is the _should_ value # that is operated on. # # All resource type properties in the puppet system are derived from this class. # # The intention is that new parameters are created by using the DSL method {Puppet::Type.newproperty}. # # @abstract # @note Properties of Types are expressed using subclasses of this class. Such a class describes one # named property of a particular Type (as opposed to describing a type of property in general). This # limits the use of one (concrete) property class instance to occur only once for a given type's inheritance # chain. An instance of a Property class is the value holder of one instance of the resource type (e.g. the # mode of a file resource instance). # A Property class may server as the superclass _(parent)_ of another; e.g. a Size property that describes # handling of measurements such as kb, mb, gb. If a type requires two different size measurements it requires # one concrete class per such measure; e.g. MinSize (:parent => Size), and MaxSize (:parent => Size). # # @todo Describe meta-parameter shadowing. This concept can not be understood by just looking at the descriptions # of the methods involved. # # @see Puppet::Type # @see Puppet::Parameter # # @api public # class Puppet::Property < Puppet::Parameter require 'puppet/property/ensure' # Returns the original wanted value(s) _(should)_ unprocessed by munging/unmunging. # The original values are set by {#value=} or {#should=}. # @return (see #should) # attr_reader :shouldorig # The noop mode for this property. # By setting a property's noop mode to `true`, any management of this property is inhibited. Calculation # and reporting still takes place, but if a change of the underlying managed entity's state # should take place it will not be carried out. This noop # setting overrides the overall `Puppet[:noop]` mode as well as the noop mode in the _associated resource_ # attr_writer :noop class << self # @todo Figure out what this is used for. Can not find any logic in the puppet code base that # reads or writes this attribute. # ??? Probably Unused attr_accessor :unmanaged # @return [Symbol] The name of the property as given when the property was created. # attr_reader :name # @!attribute [rw] array_matching # @comment note that $#46; is a period - char code require to not terminate sentence. # The `is` vs. `should` array matching mode; `:first`, or `:all`. # # @comment there are two blank chars after the symbols to cause a break - do not remove these. # * `:first` # This is primarily used for single value properties. When matched against an array of values # a match is true if the `is` value matches any of the values in the `should` array. When the `is` value # is also an array, the matching is performed against the entire array as the `is` value. # * `:all` # : This is primarily used for multi-valued properties. When matched against an array of # `should` values, the size of `is` and `should` must be the same, and all values in `is` must match # a value in `should`. # # @note The semantics of these modes are implemented by the method {#insync?}. That method is the default # implementation and it has a backwards compatible behavior that imposes additional constraints # on what constitutes a positive match. A derived property may override that method. # @return [Symbol] (:first) the mode in which matching is performed # @see #insync? # @dsl type # @api public # def array_matching @array_matching ||= :first end # @comment This is documented as an attribute - see the {array_matching} method. # def array_matching=(value) value = value.intern if value.is_a?(String) raise ArgumentError, "Supported values for Property#array_matching are 'first' and 'all'" unless [:first, :all].include?(value) @array_matching = value end end # Looks up a value's name among valid values, to enable option lookup with result as a key. # @param name [Object] the parameter value to match against valid values (names). # @return {Symbol, Regexp} a value matching predicate # @api private # def self.value_name(name) if value = value_collection.match?(name) value.name end end # Returns the value of the given option (set when a valid value with the given "name" was defined). # @param name [Symbol, Regexp] the valid value predicate as returned by {value_name} # @param option [Symbol] the name of the wanted option # @return [Object] value of the option # @raise [NoMethodError] if the option is not supported # @todo Guessing on result of passing a non supported option (it performs send(option)). # @api private # def self.value_option(name, option) if value = value_collection.value(name) value.send(option) end end # Defines a new valid value for this property. # A valid value is specified as a literal (typically a Symbol), but can also be # specified with a Regexp. # # @param name [Symbol, Regexp] a valid literal value, or a regexp that matches a value # @param options [Hash] a hash with options # @option options [Symbol] :event The event that should be emitted when this value is set. # @todo Option :event original comment says "event should be returned...", is "returned" the correct word # to use? # @option options [Symbol] :call When to call any associated block. The default value is `:instead` which # means that the block should be called instead of the provider. In earlier versions (before 20081031) it # was possible to specify a value of `:before` or `:after` for the purpose of calling # both the block and the provider. Use of these deprecated options will now raise an exception later # in the process when the _is_ value is set (see #set). # @option options [Symbol] :invalidate_refreshes Indicates a change on this property should invalidate and # remove any scheduled refreshes (from notify or subscribe) targeted at the same resource. For example, if # a change in this property takes into account any changes that a scheduled refresh would have performed, # then the scheduled refresh would be deleted. # @option options [Object] any Any other option is treated as a call to a setter having the given # option name (e.g. `:required_features` calls `required_features=` with the option's value as an # argument). # @todo The original documentation states that the option `:method` will set the name of the generated # setter method, but this is not implemented. Is the documentatin or the implementation in error? # (The implementation is in Puppet::Parameter::ValueCollection#new_value). # @todo verify that the use of :before and :after have been deprecated (or rather - never worked, and # was never in use. (This means, that the option :call could be removed since calls are always :instead). # # @dsl type # @api public def self.newvalue(name, options = {}, &block) value = value_collection.newvalue(name, options, &block) define_method(value.method, &value.block) if value.method and value.block value end # Calls the provider setter method for this property with the given value as argument. # @return [Object] what the provider returns when calling a setter for this property's name # @raise [Puppet::Error] when the provider can not handle this property. # @see #set # @api private # def call_provider(value) method = self.class.name.to_s + "=" unless provider.respond_to? method self.fail "The #{provider.class.name} provider can not handle attribute #{self.class.name}" end provider.send(method, value) end # Sets the value of this property to the given value by calling the dynamically created setter method associated with the "valid value" referenced by the given name. # @param name [Symbol, Regexp] a valid value "name" as returned by {value_name} # @param value [Object] the value to set as the value of the property # @raise [Puppet::DevError] if there was no method to call # @raise [Puppet::Error] if there were problems setting the value # @raise [Puppet::ResourceError] if there was a problem setting the value and it was not raised # as a Puppet::Error. The original exception is wrapped and logged. # @todo The check for a valid value option called `:method` does not seem to be fully supported # as it seems that this option is never consulted when the method is dynamically created. Needs to # be investigated. (Bug, or documentation needs to be changed). # @see #set # @api private # def call_valuemethod(name, value) if method = self.class.value_option(name, :method) and self.respond_to?(method) begin self.send(method) rescue Puppet::Error raise rescue => detail error = Puppet::ResourceError.new("Could not set '#{value}' on #{self.class.name}: #{detail}", @resource.line, @resource.file, detail) error.set_backtrace detail.backtrace Puppet.log_exception(detail, error.message) raise error end elsif block = self.class.value_option(name, :block) # FIXME It'd be better here to define a method, so that # the blocks could return values. self.instance_eval(&block) else devfail "Could not find method for value '#{name}'" end end # Formats a message for a property change from the given `current_value` to the given `newvalue`. # @return [String] a message describing the property change. # @note If called with equal values, this is reported as a change. # @raise [Puppet::DevError] if there were issues formatting the message # def change_to_s(current_value, newvalue) begin if current_value == :absent return "defined '#{name}' as #{self.class.format_value_for_display should_to_s(newvalue)}" elsif newvalue == :absent or newvalue == [:absent] return "undefined '#{name}' from #{self.class.format_value_for_display is_to_s(current_value)}" else return "#{name} changed #{self.class.format_value_for_display is_to_s(current_value)} to #{self.class.format_value_for_display should_to_s(newvalue)}" end rescue Puppet::Error, Puppet::DevError raise rescue => detail message = "Could not convert change '#{name}' to string: #{detail}" Puppet.log_exception(detail, message) raise Puppet::DevError, message, detail.backtrace end end # Produces the name of the event to use to describe a change of this property's value. # The produced event name is either the event name configured for this property, or a generic # event based on the name of the property with suffix `_changed`, or if the property is # `:ensure`, the name of the resource type and one of the suffixes `_created`, `_removed`, or `_changed`. # @return [String] the name of the event that describes the change # def event_name value = self.should event_name = self.class.value_option(value, :event) and return event_name name == :ensure or return (name.to_s + "_changed").to_sym return (resource.type.to_s + case value when :present; "_created" when :absent; "_removed" else "_changed" end).to_sym end # Produces an event describing a change of this property. # In addition to the event attributes set by the resource type, this method adds: # # * `:name` - the event_name # * `:desired_value` - a.k.a _should_ or _wanted value_ # * `:property` - reference to this property # * `:source_description` - the _path_ (?? See todo) # * `:invalidate_refreshes` - if scheduled refreshes should be invalidated # # @todo What is the intent of this method? What is the meaning of the :source_description passed in the # options to the created event? # @return [Puppet::Transaction::Event] the created event # @see Puppet::Type#event def event attrs = { :name => event_name, :desired_value => should, :property => self, :source_description => path } if should and value = self.class.value_collection.match?(should) attrs[:invalidate_refreshes] = true if value.invalidate_refreshes end resource.event attrs end # @todo What is this? # What is this used for? attr_reader :shadow # Initializes a Property the same way as a Parameter and handles the special case when a property is shadowing a meta-parameter. # @todo There is some special initialization when a property is not a metaparameter but # Puppet::Type.metaparamclass(for this class's name) is not nil - if that is the case a # setup_shadow is performed for that class. # # @param hash [Hash] options passed to the super initializer {Puppet::Parameter#initialize} # @note New properties of a type should be created via the DSL method {Puppet::Type.newproperty}. # @see Puppet::Parameter#initialize description of Parameter initialize options. # @api private def initialize(hash = {}) super if ! self.metaparam? and klass = Puppet::Type.metaparamclass(self.class.name) setup_shadow(klass) end end # Determines whether the property is in-sync or not in a way that is protected against missing value. # @note If the wanted value _(should)_ is not defined or is set to a non-true value then this is # a state that can not be fixed and the property is reported to be in sync. # @return [Boolean] the protected result of `true` or the result of calling {#insync?}. # # @api private # @note Do not override this method. # def safe_insync?(is) # If there is no @should value, consider the property to be in sync. return true unless @should # Otherwise delegate to the (possibly derived) insync? method. insync?(is) end # Protects against override of the {#safe_insync?} method. # @raise [RuntimeError] if the added method is `:safe_insync?` # @api private # def self.method_added(sym) raise "Puppet::Property#safe_insync? shouldn't be overridden; please override insync? instead" if sym == :safe_insync? end # Checks if the current _(is)_ value is in sync with the wanted _(should)_ value. # The check if the two values are in sync is controlled by the result of {#match_all?} which # specifies a match of `:first` or `:all`). The matching of the _is_ value against the entire _should_ value # or each of the _should_ values (as controlled by {#match_all?} is performed by {#property_matches?}. # # A derived property typically only needs to override the {#property_matches?} method, but may also # override this method if there is a need to have more control over the array matching logic. # # @note The array matching logic in this method contains backwards compatible logic that performs the # comparison in `:all` mode by checking equality and equality of _is_ against _should_ converted to array of String, # and that the lengths are equal, and in `:first` mode by checking if one of the _should_ values # is included in the _is_ values. This means that the _is_ value needs to be carefully arranged to # match the _should_. # @todo The implementation should really do return is.zip(@should).all? {|a, b| property_matches?(a, b) } # instead of using equality check and then check against an array with converted strings. # @param is [Object] The current _(is)_ value to check if it is in sync with the wanted _(should)_ value(s) # @return [Boolean] whether the values are in sync or not. # @raise [Puppet::DevError] if wanted value _(should)_ is not an array. # @api public # def insync?(is) self.devfail "#{self.class.name}'s should is not array" unless @should.is_a?(Array) # an empty array is analogous to no should values return true if @should.empty? # Look for a matching value, either for all the @should values, or any of # them, depending on the configuration of this property. if match_all? then # Emulate Array#== using our own comparison function. # A non-array was not equal to an array, which @should always is. return false unless is.is_a? Array # If they were different lengths, they are not equal. return false unless is.length == @should.length # Finally, are all the elements equal? In order to preserve the # behaviour of previous 2.7.x releases, we need to impose some fun rules # on "equality" here. # # Specifically, we need to implement *this* comparison: the two arrays # are identical if the is values are == the should values, or if the is # values are == the should values, stringified. # # This does mean that property equality is not commutative, and will not # work unless the `is` value is carefully arranged to match the should. return (is == @should or is == @should.map(&:to_s)) # When we stop being idiots about this, and actually have meaningful # semantics, this version is the thing we actually want to do. # # return is.zip(@should).all? {|a, b| property_matches?(a, b) } else return @should.any? {|want| property_matches?(is, want) } end end # Checks if the given current and desired values are equal. # This default implementation performs this check in a backwards compatible way where # the equality of the two values is checked, and then the equality of current with desired # converted to a string. # # A derived implementation may override this method to perform a property specific equality check. # # The intent of this method is to provide an equality check suitable for checking if the property # value is in sync or not. It is typically called from {#insync?}. # def property_matches?(current, desired) # This preserves the older Puppet behaviour of doing raw and string # equality comparisons for all equality. I am not clear this is globally # desirable, but at least it is not a breaking change. --daniel 2011-11-11 current == desired or current == desired.to_s end # Produces a pretty printing string for the given value. # This default implementation simply returns the given argument. A derived implementation # may perform property specific pretty printing when the _is_ and _should_ values are not # already in suitable form. # @return [String] a pretty printing string def is_to_s(currentvalue) currentvalue end # Emits a log message at the log level specified for the associated resource. # The log entry is associated with this property. # @param msg [String] the message to log # @return [void] # def log(msg) Puppet::Util::Log.create( :level => resource[:loglevel], :message => msg, :source => self ) end # @return [Boolean] whether the {array_matching} mode is set to `:all` or not def match_all? self.class.array_matching == :all end # (see Puppet::Parameter#munge) # If this property is a meta-parameter shadow, the shadow's munge is also called. # @todo Incomprehensible ! The concept of "meta-parameter-shadowing" needs to be explained. # def munge(value) self.shadow.munge(value) if self.shadow super end # @return [Symbol] the name of the property as stated when the property was created. # @note A property class (just like a parameter class) describes one specific property and # can only be used once within one type's inheritance chain. def name self.class.name end # @return [Boolean] whether this property is in noop mode or not. # Returns whether this property is in noop mode or not; if a difference between the # _is_ and _should_ values should be acted on or not. # The noop mode is a transitive setting. The mode is checked in this property, then in # the _associated resource_ and finally in Puppet[:noop]. # @todo This logic is different than Parameter#noop in that the resource noop mode overrides # the property's mode - in parameter it is the other way around. Bug or feature? # def noop # This is only here to make testing easier. if @resource.respond_to?(:noop?) @resource.noop? else if defined?(@noop) @noop else Puppet[:noop] end end end # Retrieves the current value _(is)_ of this property from the provider. # This implementation performs this operation by calling a provider method with the # same name as this property (i.e. if the property name is 'gid', a call to the # 'provider.gid' is expected to return the current value. # @return [Object] what the provider returns as the current value of the property # def retrieve provider.send(self.class.name) end # Sets the current _(is)_ value of this property. # The value is set using the provider's setter method for this property ({#call_provider}) if nothing # else has been specified. If the _valid value_ for the given value defines a `:call` option with the # value `:instead`, the # value is set with {#call_valuemethod} which invokes a block specified for the valid value. # # @note In older versions (before 20081031) it was possible to specify the call types `:before` and `:after` # which had the effect that both the provider method and the _valid value_ block were called. # This is no longer supported. # # @param value [Object] the value to set as the value of this property # @return [Object] returns what {#call_valuemethod} or {#call_provider} returns # @raise [Puppet::Error] when the provider setter should be used but there is no provider set in the _associated # resource_ # @raise [Puppet::DevError] when a deprecated call form was specified (e.g. `:before` or `:after`). # @api public # def set(value) # Set a name for looking up associated options like the event. name = self.class.value_name(value) call = self.class.value_option(name, :call) || :none if call == :instead call_valuemethod(name, value) elsif call == :none # They haven't provided a block, and our parent does not have # a provider, so we have no idea how to handle this. self.fail "#{self.class.name} cannot handle values of type #{value.inspect}" unless @resource.provider call_provider(value) else # LAK:NOTE 20081031 This is a change in behaviour -- you could # previously specify :call => [;before|:after], which would call # the setter *in addition to* the block. I'm convinced this # was never used, and it makes things unecessarily complicated. # If you want to specify a block and still call the setter, then # do so in the block. devfail "Cannot use obsolete :call value '#{call}' for property '#{self.class.name}'" end end # Sets up a shadow property for a shadowing meta-parameter. # This construct allows the creation of a property with the # same name as a meta-parameter. The metaparam will only be stored as a shadow. # @param klass [Class] the class of the shadowed meta-parameter # @return [Puppet::Parameter] an instance of the given class (a parameter or property) # def setup_shadow(klass) @shadow = klass.new(:resource => self.resource) end # Returns the wanted _(should)_ value of this property. # If the _array matching mode_ {#match_all?} is true, an array of the wanted values in unmunged format # is returned, else the first value in the array of wanted values in unmunged format is returned. # @return [Array, Object, nil] Array of values if {#match_all?} else a single value, or nil if there are no # wanted values. # @raise [Puppet::DevError] if the wanted value is non nil and not an array # # @note This method will potentially return different values than the original values as they are # converted via munging/unmunging. If the original values are wanted, call {#shouldorig}. # # @see #shouldorig # @api public # def should return nil unless defined?(@should) self.devfail "should for #{self.class.name} on #{resource.name} is not an array" unless @should.is_a?(Array) if match_all? return @should.collect { |val| self.unmunge(val) } else return self.unmunge(@should[0]) end end # Sets the wanted _(should)_ value of this property. # If the given value is not already an Array, it will be wrapped in one before being set. # This method also sets the cached original _should_ values returned by {#shouldorig}. # # @param values [Array, Object] the value(s) to set as the wanted value(s) # @raise [StandardError] when validation of a value fails (see {#validate}). # @api public # def should=(values) values = [values] unless values.is_a?(Array) @shouldorig = values values.each { |val| validate(val) } @should = values.collect { |val| self.munge(val) } end # Formats the given newvalue (following _should_ type conventions) for inclusion in a string describing a change. # @return [String] Returns the given newvalue in string form with space separated entries if it is an array. # @see #change_to_s # def should_to_s(newvalue) [newvalue].flatten.join(" ") end # Synchronizes the current value _(is)_ and the wanted value _(should)_ by calling {#set}. # @raise [Puppet::DevError] if {#should} is nil # @todo The implementation of this method is somewhat inefficient as it computes the should # array twice. def sync devfail "Got a nil value for should" unless should set(should) end # Asserts that the given value is valid. # If the developer uses a 'validate' hook, this method will get overridden. # @raise [Exception] if the value is invalid, or value can not be handled. # @return [void] # @api private # def unsafe_validate(value) super validate_features_per_value(value) end # Asserts that all required provider features are present for the given property value. # @raise [ArgumentError] if a required feature is not present # @return [void] # @api private # def validate_features_per_value(value) if features = self.class.value_option(self.class.value_name(value), :required_features) features = Array(features) needed_features = features.collect { |f| f.to_s }.join(", ") raise ArgumentError, "Provider must have features '#{needed_features}' to set '#{self.class.name}' to '#{value}'" unless provider.satisfies?(features) end end # @return [Object, nil] Returns the wanted _(should)_ value of this property. def value self.should end # (see #should=) def value=(values) self.should = values end end puppet/type.rb000064400000270213147634041260007403 0ustar00require 'puppet' require 'puppet/util/log' require 'puppet/util/metric' require 'puppet/property' require 'puppet/parameter' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/metatype/manager' require 'puppet/util/errors' require 'puppet/util/logging' require 'puppet/util/tagging' # see the bottom of the file for the rest of the inclusions module Puppet # The base class for all Puppet types. # # A type describes: #-- # * **Attributes** - properties, parameters, and meta-parameters are different types of attributes of a type. # * **Properties** - these are the properties of the managed resource (attributes of the entity being managed; like # a file's owner, group and mode). A property describes two states; the 'is' (current state) and the 'should' (wanted # state). # * **Ensurable** - a set of traits that control the lifecycle (create, remove, etc.) of a managed entity. # There is a default set of operations associated with being _ensurable_, but this can be changed. # * **Name/Identity** - one property is the name/identity of a resource, the _namevar_ that uniquely identifies # one instance of a type from all others. # * **Parameters** - additional attributes of the type (that does not directly related to an instance of the managed # resource; if an operation is recursive or not, where to look for things, etc.). A Parameter (in contrast to Property) # has one current value where a Property has two (current-state and wanted-state). # * **Meta-Parameters** - parameters that are available across all types. A meta-parameter typically has # additional semantics; like the `require` meta-parameter. A new type typically does not add new meta-parameters, # but you need to be aware of their existence so you do not inadvertently shadow an existing meta-parameters. # * **Parent** - a type can have a super type (that it inherits from). # * **Validation** - If not just a basic data type, or an enumeration of symbolic values, it is possible to provide # validation logic for a type, properties and parameters. # * **Munging** - munging/unmunging is the process of turning a value in external representation (as used # by a provider) into an internal representation and vice versa. A Type supports adding custom logic for these. # * **Auto Requirements** - a type can specify automatic relationships to resources to ensure that if they are being # managed, they will be processed before this type. # * **Providers** - a provider is an implementation of a type's behavior - the management of a resource in the # system being managed. A provider is often platform specific and is selected at runtime based on # criteria/predicates specified in the configured providers. See {Puppet::Provider} for details. # * **Device Support** - A type has some support for being applied to a device; i.e. something that is managed # by running logic external to the device itself. There are several methods that deals with type # applicability for these special cases such as {apply_to_device}. # # Additional Concepts: # -- # * **Resource-type** - A _resource type_ is a term used to denote the type of a resource; internally a resource # is really an instance of a Ruby class i.e. {Puppet::Resource} which defines its behavior as "resource data". # Conceptually however, a resource is an instance of a subclass of Type (e.g. File), where such a class describes # its interface (what can be said/what is known about a resource of this type), # * **Managed Entity** - This is not a term in general use, but is used here when there is a need to make # a distinction between a resource (a description of what/how something should be managed), and what it is # managing (a file in the file system). The term _managed entity_ is a reference to the "file in the file system" # * **Isomorphism** - the quality of being _isomorphic_ means that two resource instances with the same name # refers to the same managed entity. Or put differently; _an isomorphic name is the identity of a resource_. # As an example, `exec` resources (that executes some command) have the command (i.e. the command line string) as # their name, and these resources are said to be non-isomorphic. # # @note The Type class deals with multiple concerns; some methods provide an internal DSL for convenient definition # of types, other methods deal with various aspects while running; wiring up a resource (expressed in Puppet DSL # or Ruby DSL) with its _resource type_ (i.e. an instance of Type) to enable validation, transformation of values # (munge/unmunge), etc. Lastly, Type is also responsible for dealing with Providers; the concrete implementations # of the behavior that constitutes how a particular Type behaves on a particular type of system (e.g. how # commands are executed on a flavor of Linux, on Windows, etc.). This means that as you are reading through the # documentation of this class, you will be switching between these concepts, as well as switching between # the conceptual level "a resource is an instance of a resource-type" and the actual implementation classes # (Type, Resource, Provider, and various utility and helper classes). # # @api public # # class Type include Puppet::Util include Puppet::Util::Errors include Puppet::Util::Logging include Puppet::Util::Tagging # Comparing type instances. include Comparable # Compares this type against the given _other_ (type) and returns -1, 0, or +1 depending on the order. # @param other [Object] the object to compare against (produces nil, if not kind of Type} # @return [-1, 0, +1, nil] produces -1 if this type is before the given _other_ type, 0 if equals, and 1 if after. # Returns nil, if the given _other_ is not a kind of Type. # @see Comparable # def <=>(other) # We only order against other types, not arbitrary objects. return nil unless other.is_a? Puppet::Type # Our natural order is based on the reference name we use when comparing # against other type instances. self.ref <=> other.ref end # Code related to resource type attributes. class << self include Puppet::Util::ClassGen include Puppet::Util::Warnings # @return [Array] The list of declared properties for the resource type. # The returned lists contains instances if Puppet::Property or its subclasses. attr_reader :properties end # Returns all the attribute names of the type in the appropriate order. # The {key_attributes} come first, then the {provider}, then the {properties}, and finally # the {parameters} and {metaparams}, # all in the order they were specified in the respective files. # @return [Array] all type attribute names in a defined order. # def self.allattrs key_attributes | (parameters & [:provider]) | properties.collect { |property| property.name } | parameters | metaparams end # Returns the class associated with the given attribute name. # @param name [String] the name of the attribute to obtain the class for # @return [Class, nil] the class for the given attribute, or nil if the name does not refer to an existing attribute # def self.attrclass(name) @attrclasses ||= {} # We cache the value, since this method gets called such a huge number # of times (as in, hundreds of thousands in a given run). unless @attrclasses.include?(name) @attrclasses[name] = case self.attrtype(name) when :property; @validproperties[name] when :meta; @@metaparamhash[name] when :param; @paramhash[name] end end @attrclasses[name] end # Returns the attribute type (`:property`, `;param`, `:meta`). # @comment What type of parameter are we dealing with? Cache the results, because # this method gets called so many times. # @return [Symbol] a symbol describing the type of attribute (`:property`, `;param`, `:meta`) # def self.attrtype(attr) @attrtypes ||= {} unless @attrtypes.include?(attr) @attrtypes[attr] = case when @validproperties.include?(attr); :property when @paramhash.include?(attr); :param when @@metaparamhash.include?(attr); :meta end end @attrtypes[attr] end # Provides iteration over meta-parameters. # @yieldparam p [Puppet::Parameter] each meta parameter # @return [void] # def self.eachmetaparam @@metaparams.each { |p| yield p.name } end # Creates a new `ensure` property with configured default values or with configuration by an optional block. # This method is a convenience method for creating a property `ensure` with default accepted values. # If no block is specified, the new `ensure` property will accept the default symbolic # values `:present`, and `:absent` - see {Puppet::Property::Ensure}. # If something else is wanted, pass a block and make calls to {Puppet::Property.newvalue} from this block # to define each possible value. If a block is passed, the defaults are not automatically added to the set of # valid values. # # @note This method will be automatically called without a block if the type implements the methods # specified by {ensurable?}. It is recommended to always call this method and not rely on this automatic # specification to clearly state that the type is ensurable. # # @overload ensurable() # @overload ensurable({|| ... }) # @yield [ ] A block evaluated in scope of the new Parameter # @yieldreturn [void] # @return [void] # @dsl type # @api public # def self.ensurable(&block) if block_given? self.newproperty(:ensure, :parent => Puppet::Property::Ensure, &block) else self.newproperty(:ensure, :parent => Puppet::Property::Ensure) do self.defaultvalues end end end # Returns true if the type implements the default behavior expected by being _ensurable_ "by default". # A type is _ensurable_ by default if it responds to `:exists`, `:create`, and `:destroy`. # If a type implements these methods and have not already specified that it is _ensurable_, it will be # made so with the defaults specified in {ensurable}. # @return [Boolean] whether the type is _ensurable_ or not. # def self.ensurable? # If the class has all three of these methods defined, then it's # ensurable. [:exists?, :create, :destroy].all? { |method| self.public_method_defined?(method) } end # @comment These `apply_to` methods are horrible. They should really be implemented # as part of the usual system of constraints that apply to a type and # provider pair, but were implemented as a separate shadow system. # # @comment We should rip them out in favour of a real constraint pattern around the # target device - whatever that looks like - and not have this additional # magic here. --daniel 2012-03-08 # # Makes this type applicable to `:device`. # @return [Symbol] Returns `:device` # @api private # def self.apply_to_device @apply_to = :device end # Makes this type applicable to `:host`. # @return [Symbol] Returns `:host` # @api private # def self.apply_to_host @apply_to = :host end # Makes this type applicable to `:both` (i.e. `:host` and `:device`). # @return [Symbol] Returns `:both` # @api private # def self.apply_to_all @apply_to = :both end # Makes this type apply to `:host` if not already applied to something else. # @return [Symbol] a `:device`, `:host`, or `:both` enumeration # @api private def self.apply_to @apply_to ||= :host end # Returns true if this type is applicable to the given target. # @param target [Symbol] should be :device, :host or :target, if anything else, :host is enforced # @return [Boolean] true # @api private # def self.can_apply_to(target) [ target == :device ? :device : :host, :both ].include?(apply_to) end # Processes the options for a named parameter. # @param name [String] the name of a parameter # @param options [Hash] a hash of options # @option options [Boolean] :boolean if option set to true, an access method on the form _name_? is added for the param # @return [void] # def self.handle_param_options(name, options) # If it's a boolean parameter, create a method to test the value easily if options[:boolean] define_method(name.to_s + "?") do val = self[name] if val == :true or val == true return true end end end end # Is the given parameter a meta-parameter? # @return [Boolean] true if the given parameter is a meta-parameter. # def self.metaparam?(param) @@metaparamhash.include?(param.intern) end # Returns the meta-parameter class associated with the given meta-parameter name. # Accepts a `nil` name, and return nil. # @param name [String, nil] the name of a meta-parameter # @return [Class,nil] the class for the given meta-parameter, or `nil` if no such meta-parameter exists, (or if # the given meta-parameter name is `nil`. # def self.metaparamclass(name) return nil if name.nil? @@metaparamhash[name.intern] end # Returns all meta-parameter names. # @return [Array] all meta-parameter names # def self.metaparams @@metaparams.collect { |param| param.name } end # Returns the documentation for a given meta-parameter of this type. # @param metaparam [Puppet::Parameter] the meta-parameter to get documentation for. # @return [String] the documentation associated with the given meta-parameter, or nil of no such documentation # exists. # @raise if the given metaparam is not a meta-parameter in this type # def self.metaparamdoc(metaparam) @@metaparamhash[metaparam].doc end # Creates a new meta-parameter. # This creates a new meta-parameter that is added to this and all inheriting types. # @param name [Symbol] the name of the parameter # @param options [Hash] a hash with options. # @option options [Class] :parent (Puppet::Parameter) the super class of this parameter # @option options [Hash{String => Object}] :attributes a hash that is applied to the generated class # by calling setter methods corresponding to this hash's keys/value pairs. This is done before the given # block is evaluated. # @option options [Boolean] :boolean (false) specifies if this is a boolean parameter # @option options [Boolean] :namevar (false) specifies if this parameter is the namevar # @option options [Symbol, Array] :required_features specifies required provider features by name # @return [Class] the created parameter # @yield [ ] a required block that is evaluated in the scope of the new meta-parameter # @api public # @dsl type # @todo Verify that this description is ok # def self.newmetaparam(name, options = {}, &block) @@metaparams ||= [] @@metaparamhash ||= {} name = name.intern param = genclass( name, :parent => options[:parent] || Puppet::Parameter, :prefix => "MetaParam", :hash => @@metaparamhash, :array => @@metaparams, :attributes => options[:attributes], &block ) # Grr. param.required_features = options[:required_features] if options[:required_features] handle_param_options(name, options) param.metaparam = true param end # Returns the list of parameters that comprise the composite key / "uniqueness key". # All parameters that return true from #isnamevar? or is named `:name` are included in the returned result. # @see uniqueness_key # @return [Array] WARNING: this return type is uncertain def self.key_attribute_parameters @key_attribute_parameters ||= ( @parameters.find_all { |param| param.isnamevar? or param.name == :name } ) end # Returns cached {key_attribute_parameters} names. # Key attributes are properties and parameters that comprise a composite key # or "uniqueness key". # @return [Array] cached key_attribute names # def self.key_attributes # This is a cache miss around 0.05 percent of the time. --daniel 2012-07-17 @key_attributes_cache ||= key_attribute_parameters.collect { |p| p.name } end # Returns a mapping from the title string to setting of attribute value(s). # This default implementation provides a mapping of title to the one and only _namevar_ present # in the type's definition. # @note Advanced: some logic requires this mapping to be done differently, using a different # validation/pattern, breaking up the title # into several parts assigning each to an individual attribute, or even use a composite identity where # all namevars are seen as part of the unique identity (such computation is done by the {#uniqueness} method. # These advanced options are rarely used (only one of the built in puppet types use this, and then only # a small part of the available functionality), and the support for these advanced mappings is not # implemented in a straight forward way. For these reasons, this method has been marked as private). # # @raise [Puppet::DevError] if there is no title pattern and there are two or more key attributes # @return [Array>>>, nil] a structure with a regexp and the first key_attribute ??? # @comment This wonderful piece of logic creates a structure used by Resource.parse_title which # has the capability to assign parts of the title to one or more attributes; It looks like an implementation # of a composite identity key (all parts of the key_attributes array are in the key). This can also # be seen in the method uniqueness_key. # The implementation in this method simply assigns the title to the one and only namevar (which is name # or a variable marked as namevar). # If there are multiple namevars (any in addition to :name?) then this method MUST be implemented # as it raises an exception if there is more than 1. Note that in puppet, it is only File that uses this # to create a different pattern for assigning to the :path attribute # This requires further digging. # The entire construct is somewhat strange, since resource checks if the method "title_patterns" is # implemented (it seems it always is) - why take this more expensive regexp mathching route for all # other types? # @api private # def self.title_patterns case key_attributes.length when 0; [] when 1; [ [ /(.*)/m, [ [key_attributes.first] ] ] ] else raise Puppet::DevError,"you must specify title patterns when there are two or more key attributes" end end # Produces a resource's _uniqueness_key_ (or composite key). # This key is an array of all key attributes' values. Each distinct tuple must be unique for each resource type. # @see key_attributes # @return [Object] an object that is a _uniqueness_key_ for this object # def uniqueness_key self.class.key_attributes.sort_by { |attribute_name| attribute_name.to_s }.map{ |attribute_name| self[attribute_name] } end # Creates a new parameter. # @param name [Symbol] the name of the parameter # @param options [Hash] a hash with options. # @option options [Class] :parent (Puppet::Parameter) the super class of this parameter # @option options [Hash{String => Object}] :attributes a hash that is applied to the generated class # by calling setter methods corresponding to this hash's keys/value pairs. This is done before the given # block is evaluated. # @option options [Boolean] :boolean (false) specifies if this is a boolean parameter # @option options [Boolean] :namevar (false) specifies if this parameter is the namevar # @option options [Symbol, Array] :required_features specifies required provider features by name # @return [Class] the created parameter # @yield [ ] a required block that is evaluated in the scope of the new parameter # @api public # @dsl type # def self.newparam(name, options = {}, &block) options[:attributes] ||= {} param = genclass( name, :parent => options[:parent] || Puppet::Parameter, :attributes => options[:attributes], :block => block, :prefix => "Parameter", :array => @parameters, :hash => @paramhash ) handle_param_options(name, options) # Grr. param.required_features = options[:required_features] if options[:required_features] param.isnamevar if options[:namevar] param end # Creates a new property. # @param name [Symbol] the name of the property # @param options [Hash] a hash with options. # @option options [Symbol] :array_matching (:first) specifies how the current state is matched against # the wanted state. Use `:first` if the property is single valued, and (`:all`) otherwise. # @option options [Class] :parent (Puppet::Property) the super class of this property # @option options [Hash{String => Object}] :attributes a hash that is applied to the generated class # by calling setter methods corresponding to this hash's keys/value pairs. This is done before the given # block is evaluated. # @option options [Boolean] :boolean (false) specifies if this is a boolean parameter # @option options [Symbol] :retrieve the method to call on the provider (or `parent` if `provider` is not set) # to retrieve the current value of this property. # @option options [Symbol, Array] :required_features specifies required provider features by name # @return [Class] the created property # @yield [ ] a required block that is evaluated in the scope of the new property # @api public # @dsl type # def self.newproperty(name, options = {}, &block) name = name.intern # This is here for types that might still have the old method of defining # a parent class. unless options.is_a? Hash raise Puppet::DevError, "Options must be a hash, not #{options.inspect}" end raise Puppet::DevError, "Class #{self.name} already has a property named #{name}" if @validproperties.include?(name) if parent = options[:parent] options.delete(:parent) else parent = Puppet::Property end # We have to create our own, new block here because we want to define # an initial :retrieve method, if told to, and then eval the passed # block if available. prop = genclass(name, :parent => parent, :hash => @validproperties, :attributes => options) do # If they've passed a retrieve method, then override the retrieve # method on the class. if options[:retrieve] define_method(:retrieve) do provider.send(options[:retrieve]) end end class_eval(&block) if block end # If it's the 'ensure' property, always put it first. if name == :ensure @properties.unshift prop else @properties << prop end prop end def self.paramdoc(param) @paramhash[param].doc end # @return [Array] Returns the parameter names def self.parameters return [] unless defined?(@parameters) @parameters.collect { |klass| klass.name } end # @return [Puppet::Parameter] Returns the parameter class associated with the given parameter name. def self.paramclass(name) @paramhash[name] end # @return [Puppet::Property] Returns the property class ??? associated with the given property name def self.propertybyname(name) @validproperties[name] end # Returns whether or not the given name is the name of a property, parameter or meta-parameter # @return [Boolean] true if the given attribute name is the name of an existing property, parameter or meta-parameter # def self.validattr?(name) name = name.intern return true if name == :name @validattrs ||= {} unless @validattrs.include?(name) @validattrs[name] = !!(self.validproperty?(name) or self.validparameter?(name) or self.metaparam?(name)) end @validattrs[name] end # @return [Boolean] Returns true if the given name is the name of an existing property def self.validproperty?(name) name = name.intern @validproperties.include?(name) && @validproperties[name] end # @return [Array, {}] Returns a list of valid property names, or an empty hash if there are none. # @todo An empty hash is returned if there are no defined parameters (not an empty array). This looks like # a bug. # def self.validproperties return {} unless defined?(@parameters) @validproperties.keys end # @return [Boolean] Returns true if the given name is the name of an existing parameter def self.validparameter?(name) raise Puppet::DevError, "Class #{self} has not defined parameters" unless defined?(@parameters) !!(@paramhash.include?(name) or @@metaparamhash.include?(name)) end # (see validattr?) # @note see comment in code - how should this be documented? Are some of the other query methods deprecated? # (or should be). # @comment This is a forward-compatibility method - it's the validity interface we'll use in Puppet::Resource. def self.valid_parameter?(name) validattr?(name) end # @return [Boolean] Returns true if the wanted state of the resoure is that it should be absent (i.e. to be deleted). def deleting? obj = @parameters[:ensure] and obj.should == :absent end # Creates a new property value holder for the resource if it is valid and does not already exist # @return [Boolean] true if a new parameter was added, false otherwise def add_property_parameter(prop_name) if self.class.validproperty?(prop_name) && !@parameters[prop_name] self.newattr(prop_name) return true end false end # @return [Symbol, Boolean] Returns the name of the namevar if there is only one or false otherwise. # @comment This is really convoluted and part of the support for multiple namevars (?). # If there is only one namevar, the produced value is naturally this namevar, but if there are several? # The logic caches the name of the namevar if it is a single name, but otherwise always # calls key_attributes, and then caches the first if there was only one, otherwise it returns # false and caches this (which is then subsequently returned as a cache hit). # def name_var return @name_var_cache unless @name_var_cache.nil? key_attributes = self.class.key_attributes @name_var_cache = (key_attributes.length == 1) && key_attributes.first end # Gets the 'should' (wanted state) value of a parameter or property by name. # To explicitly get the 'is' (current state) value use `o.is(:name)`, and to explicitly get the 'should' value # use `o.should(:name)` # @param name [String] the name of the attribute to obtain the 'should' value for. # @return [Object] 'should'/wanted value of the given attribute def [](name) name = name.intern fail("Invalid parameter #{name}(#{name.inspect})") unless self.class.validattr?(name) if name == :name && nv = name_var name = nv end if obj = @parameters[name] # Note that if this is a property, then the value is the "should" value, # not the current value. obj.value else return nil end end # Sets the 'should' (wanted state) value of a property, or the value of a parameter. # @return # @raise [Puppet::Error] if the setting of the value fails, or if the given name is nil. # @raise [Puppet::ResourceError] when the parameter validation raises Puppet::Error or # ArgumentError def []=(name,value) name = name.intern fail("Invalid parameter #{name}") unless self.class.validattr?(name) if name == :name && nv = name_var name = nv end raise Puppet::Error.new("Got nil value for #{name}") if value.nil? property = self.newattr(name) if property begin # make sure the parameter doesn't have any errors property.value = value rescue Puppet::Error, ArgumentError => detail error = Puppet::ResourceError.new("Parameter #{name} failed on #{ref}: #{detail}") adderrorcontext(error, detail) raise error end end nil end # Removes an attribute from the object; useful in testing or in cleanup # when an error has been encountered # @todo Don't know what the attr is (name or Property/Parameter?). Guessing it is a String name... # @todo Is it possible to delete a meta-parameter? # @todo What does delete mean? Is it deleted from the type or is its value state 'is'/'should' deleted? # @param attr [String] the attribute to delete from this object. WHAT IS THE TYPE? # @raise [Puppet::DecError] when an attempt is made to delete an attribute that does not exists. # def delete(attr) attr = attr.intern if @parameters.has_key?(attr) @parameters.delete(attr) else raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end # Iterates over the properties that were set on this resource. # @yieldparam property [Puppet::Property] each property # @return [void] def eachproperty # properties is a private method properties.each { |property| yield property } end # Return the parameters, metaparams, and properties that have a value or were set by a default. Properties are # included since they are a subclass of parameter. # @return [Array] Array of parameter objects ( or subclass thereof ) def parameters_with_value self.class.allattrs.collect { |attr| parameter(attr) }.compact end # Iterates over all parameters with value currently set. # @yieldparam parameter [Puppet::Parameter] or a subclass thereof # @return [void] def eachparameter parameters_with_value.each { |parameter| yield parameter } end # Creates a transaction event. # Called by Transaction or by a property. # Merges the given options with the options `:resource`, `:file`, `:line`, and `:tags`, initialized from # values in this object. For possible options to pass (if any ????) see {Puppet::Transaction::Event}. # @todo Needs a better explanation "Why should I care who is calling this method?", What do I need to know # about events and how they work? Where can I read about them? # @param options [Hash] options merged with a fixed set of options defined by this method, passed on to {Puppet::Transaction::Event}. # @return [Puppet::Transaction::Event] the created event def event(options = {}) Puppet::Transaction::Event.new({:resource => self, :file => file, :line => line, :tags => tags}.merge(options)) end # @return [Object, nil] Returns the 'should' (wanted state) value for a specified property, or nil if the # given attribute name is not a property (i.e. if it is a parameter, meta-parameter, or does not exist). def should(name) name = name.intern (prop = @parameters[name] and prop.is_a?(Puppet::Property)) ? prop.should : nil end # Registers an attribute to this resource type insance. # Requires either the attribute name or class as its argument. # This is a noop if the named property/parameter is not supported # by this resource. Otherwise, an attribute instance is created # and kept in this resource's parameters hash. # @overload newattr(name) # @param name [Symbol] symbolic name of the attribute # @overload newattr(klass) # @param klass [Class] a class supported as an attribute class, i.e. a subclass of # Parameter or Property # @return [Object] An instance of the named Parameter or Property class associated # to this resource type instance, or nil if the attribute is not supported # def newattr(name) if name.is_a?(Class) klass = name name = klass.name end unless klass = self.class.attrclass(name) raise Puppet::Error, "Resource type #{self.class.name} does not support parameter #{name}" end if provider and ! provider.class.supports_parameter?(klass) missing = klass.required_features.find_all { |f| ! provider.class.feature?(f) } debug "Provider %s does not support features %s; not managing attribute %s" % [provider.class.name, missing.join(", "), name] return nil end return @parameters[name] if @parameters.include?(name) @parameters[name] = klass.new(:resource => self) end # Returns a string representation of the resource's containment path in # the catalog. # @return [String] def path @path ||= '/' + pathbuilder.join('/') end # Returns the value of this object's parameter given by name # @param name [String] the name of the parameter # @return [Object] the value def parameter(name) @parameters[name.to_sym] end # Returns a shallow copy of this object's hash of attributes by name. # Note that his not only comprises parameters, but also properties and metaparameters. # Changes to the contained parameters will have an effect on the parameters of this type, but changes to # the returned hash does not. # @return [Hash{String => Object}] a new hash being a shallow copy of the parameters map name to parameter def parameters @parameters.dup end # @return [Boolean] Returns whether the attribute given by name has been added # to this resource or not. def propertydefined?(name) name = name.intern unless name.is_a? Symbol @parameters.include?(name) end # Returns a {Puppet::Property} instance by name. # To return the value, use 'resource[param]' # @todo LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method, # this one should probably go away at some point. - Does this mean it should be deprecated ? # @return [Puppet::Property] the property with the given name, or nil if not a property or does not exist. def property(name) (obj = @parameters[name.intern] and obj.is_a?(Puppet::Property)) ? obj : nil end # @todo comment says "For any parameters or properties that have defaults and have not yet been # set, set them now. This method can be handed a list of attributes, # and if so it will only set defaults for those attributes." # @todo Needs a better explanation, and investigation about the claim an array can be passed (it is passed # to self.class.attrclass to produce a class on which a check is made if it has a method class :default (does # not seem to support an array... # @return [void] # def set_default(attr) return unless klass = self.class.attrclass(attr) return unless klass.method_defined?(:default) return if @parameters.include?(klass.name) return unless parameter = newattr(klass.name) if value = parameter.default and ! value.nil? parameter.value = value else @parameters.delete(parameter.name) end end # @todo the comment says: "Convert our object to a hash. This just includes properties." # @todo this is confused, again it is the @parameters instance variable that is consulted, and # each value is copied - does it contain "properties" and "parameters" or both? Does it contain # meta-parameters? # # @return [Hash{ ??? => ??? }] a hash of WHAT?. The hash is a shallow copy, any changes to the # objects returned in this hash will be reflected in the original resource having these attributes. # def to_hash rethash = {} @parameters.each do |name, obj| rethash[name] = obj.value end rethash end # @return [String] the name of this object's class # @todo Would that be "file" for the "File" resource type? of "File" or something else? # def type self.class.name end # @todo Comment says "Return a specific value for an attribute.", as opposed to what "An upspecific value"??? # @todo is this the 'is' or the 'should' value? # @todo why is the return restricted to things that respond to :value? (Only non structural basic data types # supported? # # @return [Object, nil] the value of the attribute having the given name, or nil if the given name is not # an attribute, or the referenced attribute does not respond to `:value`. def value(name) name = name.intern (obj = @parameters[name] and obj.respond_to?(:value)) ? obj.value : nil end # @todo What is this used for? Needs a better explanation. # @return [???] the version of the catalog or 0 if there is no catalog. def version return 0 unless catalog catalog.version end # @return [Array] Returns all of the property objects, in the order specified in the # class. # @todo "what does the 'order specified in the class' mean? The order the properties where added in the # ruby file adding a new type with new properties? # def properties self.class.properties.collect { |prop| @parameters[prop.name] }.compact end # Returns true if the type's notion of name is the identity of a resource. # See the overview of this class for a longer explanation of the concept _isomorphism_. # Defaults to true. # # @return [Boolan] true, if this type's name is isomorphic with the object def self.isomorphic? if defined?(@isomorphic) return @isomorphic else return true end end # @todo check that this gets documentation (it is at the class level as well as instance). # (see isomorphic?) def isomorphic? self.class.isomorphic? end # Returns true if the instance is a managed instance. # A 'yes' here means that the instance was created from the language, vs. being created # in order resolve other questions, such as finding a package in a list. # @note An object that is managed always stays managed, but an object that is not managed # may become managed later in its lifecycle. # @return [Boolean] true if the object is managed def managed? # Once an object is managed, it always stays managed; but an object # that is listed as unmanaged might become managed later in the process, # so we have to check that every time if @managed return @managed else @managed = false properties.each { |property| s = property.should if s and ! property.class.unmanaged @managed = true break end } return @managed end end ############################### # Code related to the container behaviour. # Returns true if the search should be done in depth-first order. # This implementation always returns false. # @todo What is this used for? # # @return [Boolean] true if the search should be done in depth first order. # def depthfirst? false end # Removes this object (FROM WHERE?) # @todo removes if from where? # @overload remove(rmdeps) # @deprecated Use remove() # @param rmdeps [Boolean] intended to indicate that all subscriptions should also be removed, ignored. # @overload remove() # @return [void] # def remove(rmdeps = true) # This is hackish (mmm, cut and paste), but it works for now, and it's # better than warnings. @parameters.each do |name, obj| obj.remove end @parameters.clear @parent = nil # Remove the reference to the provider. if self.provider @provider.clear @provider = nil end end ############################### # Code related to evaluating the resources. # Returns the ancestors - WHAT? # This implementation always returns an empty list. # @todo WHAT IS THIS ? # @return [Array] returns a list of ancestors. def ancestors [] end # Lifecycle method for a resource. This is called during graph creation. # It should perform any consistency checking of the catalog and raise a # Puppet::Error if the transaction should be aborted. # # It differs from the validate method, since it is called later during # initialization and can rely on self.catalog to have references to all # resources that comprise the catalog. # # @see Puppet::Transaction#add_vertex # @raise [Puppet::Error] If the pre-run check failed. # @return [void] # @abstract a resource type may implement this method to perform # validation checks that can query the complete catalog def pre_run_check end # Flushes the provider if supported by the provider, else no action. # This is called by the transaction. # @todo What does Flushing the provider mean? Why is it interesting to know that this is # called by the transaction? (It is not explained anywhere what a transaction is). # # @return [???, nil] WHAT DOES IT RETURN? GUESS IS VOID def flush self.provider.flush if self.provider and self.provider.respond_to?(:flush) end # Returns true if all contained objects are in sync. # @todo "contained in what?" in the given "in" parameter? # # @todo deal with the comment _"FIXME I don't think this is used on the type instances any more, # it's really only used for testing"_ # @return [Boolean] true if in sync, false otherwise. # def insync?(is) insync = true if property = @parameters[:ensure] unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '#{property.name}'" end ensureis = is[property] if property.safe_insync?(ensureis) and property.should == :absent return true end end properties.each { |property| unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '#{property.name}'" end propis = is[property] unless property.safe_insync?(propis) property.debug("Not in sync: #{propis.inspect} vs #{property.should.inspect}") insync = false #else # property.debug("In sync") end } #self.debug("#{self} sync status is #{insync}") insync end # Retrieves the current value of all contained properties. # Parameters and meta-parameters are not included in the result. # @todo As oposed to all non contained properties? How is this different than any of the other # methods that also "gets" properties/parameters/etc. ? # @return [Puppet::Resource] array of all property values (mix of types) # @raise [fail???] if there is a provider and it is not suitable for the host this is evaluated for. def retrieve fail "Provider #{provider.class.name} is not functional on this host" if self.provider.is_a?(Puppet::Provider) and ! provider.class.suitable? result = Puppet::Resource.new(self.class, title) # Provide the name, so we know we'll always refer to a real thing result[:name] = self[:name] unless self[:name] == title if ensure_prop = property(:ensure) or (self.class.validattr?(:ensure) and ensure_prop = newattr(:ensure)) result[:ensure] = ensure_state = ensure_prop.retrieve else ensure_state = nil end properties.each do |property| next if property.name == :ensure if ensure_state == :absent result[property] = :absent else result[property] = property.retrieve end end result end # Retrieve the current state of the system as a Puppet::Resource. For # the base Puppet::Type this does the same thing as #retrieve, but # specific types are free to implement #retrieve as returning a hash, # and this will call #retrieve and convert the hash to a resource. # This is used when determining when syncing a resource. # # @return [Puppet::Resource] A resource representing the current state # of the system. # # @api private def retrieve_resource resource = retrieve resource = Resource.new(self.class, title, :parameters => resource) if resource.is_a? Hash resource end # Given the hash of current properties, should this resource be treated as if it # currently exists on the system. May need to be overridden by types that offer up # more than just :absent and :present. def present?(current_values) current_values[:ensure] != :absent end # Returns a hash of the current properties and their values. # If a resource is absent, its value is the symbol `:absent` # @return [Hash{Puppet::Property => Object}] mapping of property instance to its value # def currentpropvalues # It's important to use the 'properties' method here, as it follows the order # in which they're defined in the class. It also guarantees that 'ensure' # is the first property, which is important for skipping 'retrieve' on # all the properties if the resource is absent. ensure_state = false return properties.inject({}) do | prophash, property| if property.name == :ensure ensure_state = property.retrieve prophash[property] = ensure_state else if ensure_state == :absent prophash[property] = :absent else prophash[property] = property.retrieve end end prophash end end # Returns the `noop` run mode status of this. # @return [Boolean] true if running in noop mode. def noop? # If we're not a host_config, we're almost certainly part of # Settings, and we want to ignore 'noop' return false if catalog and ! catalog.host_config? if defined?(@noop) @noop else Puppet[:noop] end end # (see #noop?) def noop noop? end # Retrieves all known instances. # @todo Retrieves them from where? Known to whom? # Either requires providers or must be overridden. # @raise [Puppet::DevError] when there are no providers and the implementation has not overridded this method. def self.instances raise Puppet::DevError, "#{self.name} has no providers and has not overridden 'instances'" if provider_hash.empty? # Put the default provider first, then the rest of the suitable providers. provider_instances = {} providers_by_source.collect do |provider| self.properties.find_all do |property| provider.supports_parameter?(property) end.collect do |property| property.name end provider.instances.collect do |instance| # We always want to use the "first" provider instance we find, unless the resource # is already managed and has a different provider set if other = provider_instances[instance.name] Puppet.debug "%s %s found in both %s and %s; skipping the %s version" % [self.name.to_s.capitalize, instance.name, other.class.name, instance.class.name, instance.class.name] next end provider_instances[instance.name] = instance result = new(:name => instance.name, :provider => instance) properties.each { |name| result.newattr(name) } result end end.flatten.compact end # Returns a list of one suitable provider per source, with the default provider first. # @todo Needs better explanation; what does "source" mean in this context? # @return [Array] list of providers # def self.providers_by_source # Put the default provider first (can be nil), then the rest of the suitable providers. sources = [] [defaultprovider, suitableprovider].flatten.uniq.collect do |provider| next if provider.nil? next if sources.include?(provider.source) sources << provider.source provider end.compact end # Converts a simple hash into a Resource instance. # @todo as opposed to a complex hash? Other raised exceptions? # @param [Hash{Symbol, String => Object}] hash resource attribute to value map to initialize the created resource from # @return [Puppet::Resource] the resource created from the hash # @raise [Puppet::Error] if a title is missing in the given hash def self.hash2resource(hash) hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result } title = hash.delete(:title) title ||= hash[:name] title ||= hash[key_attributes.first] if key_attributes.length == 1 raise Puppet::Error, "Title or name must be provided" unless title # Now create our resource. resource = Puppet::Resource.new(self, title) resource.catalog = hash.delete(:catalog) hash.each do |param, value| resource[param] = value end resource end # Returns an array of strings representing the containment heirarchy # (types/classes) that make up the path to the resource from the root # of the catalog. This is mostly used for logging purposes. # # @api private def pathbuilder if p = parent [p.pathbuilder, self.ref].flatten else [self.ref] end end ############################### # Add all of the meta-parameters. newmetaparam(:noop) do desc "Whether to apply this resource in noop mode. When applying a resource in noop mode, Puppet will check whether it is in sync, like it does when running normally. However, if a resource attribute is not in the desired state (as declared in the catalog), Puppet will take no action, and will instead report the changes it _would_ have made. These simulated changes will appear in the report sent to the puppet master, or be shown on the console if running puppet agent or puppet apply in the foreground. The simulated changes will not send refresh events to any subscribing or notified resources, although Puppet will log that a refresh event _would_ have been sent. **Important note:** [The `noop` setting](http://docs.puppetlabs.com/references/latest/configuration.html#noop) allows you to globally enable or disable noop mode, but it will _not_ override the `noop` metaparameter on individual resources. That is, the value of the global `noop` setting will _only_ affect resources that do not have an explicit value set for their `noop` attribute." newvalues(:true, :false) munge do |value| case value when true, :true, "true"; @resource.noop = true when false, :false, "false"; @resource.noop = false end end end newmetaparam(:schedule) do desc "A schedule to govern when Puppet is allowed to manage this resource. The value of this metaparameter must be the `name` of a `schedule` resource. This means you must declare a schedule resource, then refer to it by name; see [the docs for the `schedule` type](http://docs.puppetlabs.com/references/latest/type.html#schedule) for more info. schedule { 'everyday': period => daily, range => \"2-4\" } exec { \"/usr/bin/apt-get update\": schedule => 'everyday' } Note that you can declare the schedule resource anywhere in your manifests, as long as it ends up in the final compiled catalog." end newmetaparam(:audit) do desc "Marks a subset of this resource's unmanaged attributes for auditing. Accepts an attribute name, an array of attribute names, or `all`. Auditing a resource attribute has two effects: First, whenever a catalog is applied with puppet apply or puppet agent, Puppet will check whether that attribute of the resource has been modified, comparing its current value to the previous run; any change will be logged alongside any actions performed by Puppet while applying the catalog. Secondly, marking a resource attribute for auditing will include that attribute in inspection reports generated by puppet inspect; see the puppet inspect documentation for more details. Managed attributes for a resource can also be audited, but note that changes made by Puppet will be logged as additional modifications. (I.e. if a user manually edits a file whose contents are audited and managed, puppet agent's next two runs will both log an audit notice: the first run will log the user's edit and then revert the file to the desired state, and the second run will log the edit made by Puppet.)" validate do |list| list = Array(list).collect {|p| p.to_sym} unless list == [:all] list.each do |param| next if @resource.class.validattr?(param) fail "Cannot audit #{param}: not a valid attribute for #{resource}" end end end munge do |args| properties_to_audit(args).each do |param| next unless resource.class.validproperty?(param) resource.newattr(param) end end def all_properties resource.class.properties.find_all do |property| resource.provider.nil? or resource.provider.class.supports_parameter?(property) end.collect do |property| property.name end end def properties_to_audit(list) if !list.kind_of?(Array) && list.to_sym == :all list = all_properties else list = Array(list).collect { |p| p.to_sym } end end end newmetaparam(:loglevel) do desc "Sets the level that information will be logged. The log levels have the biggest impact when logs are sent to syslog (which is currently the default). The order of the log levels, in decreasing priority, is: * `crit` * `emerg` * `alert` * `err` * `warning` * `notice` * `info` / `verbose` * `debug` " defaultto :notice newvalues(*Puppet::Util::Log.levels) newvalues(:verbose) munge do |loglevel| val = super(loglevel) if val == :verbose val = :info end val end end newmetaparam(:alias) do desc %q{Creates an alias for the resource. Puppet uses this internally when you provide a symbolic title and an explicit namevar value: file { 'sshdconfig': path => $operatingsystem ? { solaris => '/usr/local/etc/ssh/sshd_config', default => '/etc/ssh/sshd_config', }, source => '...' } service { 'sshd': subscribe => File['sshdconfig'], } When you use this feature, the parser sets `sshdconfig` as the title, and the library sets that as an alias for the file so the dependency lookup in `Service['sshd']` works. You can use this metaparameter yourself, but note that aliases generally only work for creating relationships; anything else that refers to an existing resource (such as amending or overriding resource attributes in an inherited class) must use the resource's exact title. For example, the following code will not work: file { '/etc/ssh/sshd_config': owner => root, group => root, alias => 'sshdconfig', } File['sshdconfig'] { mode => 644, } There's no way here for the Puppet parser to know that these two stanzas should be affecting the same file. } munge do |aliases| aliases = [aliases] unless aliases.is_a?(Array) raise(ArgumentError, "Cannot add aliases without a catalog") unless @resource.catalog aliases.each do |other| if obj = @resource.catalog.resource(@resource.class.name, other) unless obj.object_id == @resource.object_id self.fail("#{@resource.title} can not create alias #{other}: object already exists") end next end # Newschool, add it to the catalog. @resource.catalog.alias(@resource, other) end end end newmetaparam(:tag) do desc "Add the specified tags to the associated resource. While all resources are automatically tagged with as much information as possible (e.g., each class and definition containing the resource), it can be useful to add your own tags to a given resource. Multiple tags can be specified as an array: file {'/etc/hosts': ensure => file, source => 'puppet:///modules/site/hosts', mode => 0644, tag => ['bootstrap', 'minimumrun', 'mediumrun'], } Tags are useful for things like applying a subset of a host's configuration with [the `tags` setting](/references/latest/configuration.html#tags) (e.g. `puppet agent --test --tags bootstrap`) or filtering alerts with [the `tagmail` report processor](http://docs.puppetlabs.com/references/latest/report.html#tagmail)." munge do |tags| tags = [tags] unless tags.is_a? Array tags.each do |tag| @resource.tag(tag) end end end # RelationshipMetaparam is an implementation supporting the meta-parameters `:require`, `:subscribe`, # `:notify`, and `:before`. # # class RelationshipMetaparam < Puppet::Parameter class << self attr_accessor :direction, :events, :callback, :subclasses end @subclasses = [] def self.inherited(sub) @subclasses << sub end # @return [Array] turns attribute value(s) into list of resources def munge(references) references = [references] unless references.is_a?(Array) references.collect do |ref| if ref.is_a?(Puppet::Resource) ref else Puppet::Resource.new(ref) end end end # Checks each reference to assert that what it references exists in the catalog. # # @raise [???fail] if the referenced resource can not be found # @return [void] def validate_relationship @value.each do |ref| unless @resource.catalog.resource(ref.to_s) description = self.class.direction == :in ? "dependency" : "dependent" fail ResourceError, "Could not find #{description} #{ref} for #{resource.ref}" end end end # Creates edges for all relationships. # The `:in` relationships are specified by the event-receivers, and `:out` # relationships are specified by the event generator. # @todo references to "event-receivers" and "event generator" means in this context - are those just # the resources at the two ends of the relationship? # This way 'source' and 'target' are consistent terms in both edges # and events, i.e. an event targets edges whose source matches # the event's source. The direction of the relationship determines # which resource is applied first and which resource is considered # to be the event generator. # @return [Array] # @raise [???fail] when a reference can not be resolved # def to_edges @value.collect do |reference| reference.catalog = resource.catalog # Either of the two retrieval attempts could have returned # nil. unless related_resource = reference.resolve self.fail "Could not retrieve dependency '#{reference}' of #{@resource.ref}" end # Are we requiring them, or vice versa? See the method docs # for futher info on this. if self.class.direction == :in source = related_resource target = @resource else source = @resource target = related_resource end if method = self.class.callback subargs = { :event => self.class.events, :callback => method } self.debug("subscribes to #{related_resource.ref}") else # If there's no callback, there's no point in even adding # a label. subargs = nil self.debug("requires #{related_resource.ref}") end Puppet::Relationship.new(source, target, subargs) end end end # @todo document this, have no clue what this does... it retuns "RelationshipMetaparam.subclasses" # def self.relationship_params RelationshipMetaparam.subclasses end # Note that the order in which the relationships params is defined # matters. The labelled params (notify and subcribe) must be later, # so that if both params are used, those ones win. It's a hackish # solution, but it works. newmetaparam(:require, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :NONE}) do desc "One or more resources that this resource depends on, expressed as [resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references). Multiple resources can be specified as an array of references. When this attribute is present: * The required resource(s) will be applied **before** this resource. This is one of the four relationship metaparameters, along with `before`, `notify`, and `subscribe`. For more context, including the alternate chaining arrow (`->` and `~>`) syntax, see [the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)." end newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) do desc "One or more resources that this resource depends on, expressed as [resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references). Multiple resources can be specified as an array of references. When this attribute is present: * The subscribed resource(s) will be applied _before_ this resource. * If Puppet makes changes to any of the subscribed resources, it will cause this resource to _refresh._ (Refresh behavior varies by resource type: services will restart, mounts will unmount and re-mount, etc. Not all types can refresh.) This is one of the four relationship metaparameters, along with `before`, `require`, and `notify`. For more context, including the alternate chaining arrow (`->` and `~>`) syntax, see [the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)." end newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) do desc "One or more resources that depend on this resource, expressed as [resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references). Multiple resources can be specified as an array of references. When this attribute is present: * This resource will be applied _before_ the dependent resource(s). This is one of the four relationship metaparameters, along with `require`, `notify`, and `subscribe`. For more context, including the alternate chaining arrow (`->` and `~>`) syntax, see [the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)." end newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) do desc "One or more resources that depend on this resource, expressed as [resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references). Multiple resources can be specified as an array of references. When this attribute is present: * This resource will be applied _before_ the notified resource(s). * If Puppet makes changes to this resource, it will cause all of the notified resources to _refresh._ (Refresh behavior varies by resource type: services will restart, mounts will unmount and re-mount, etc. Not all types can refresh.) This is one of the four relationship metaparameters, along with `before`, `require`, and `subscribe`. For more context, including the alternate chaining arrow (`->` and `~>`) syntax, see [the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)." end newmetaparam(:stage) do desc %{Which run stage this class should reside in. **Note: This metaparameter can only be used on classes,** and only when declaring them with the resource-like syntax. It cannot be used on normal resources or on classes declared with `include`. By default, all classes are declared in the `main` stage. To assign a class to a different stage, you must: * Declare the new stage as a [`stage` resource](http://docs.puppetlabs.com/references/latest/type.html#stage). * Declare an order relationship between the new stage and the `main` stage. * Use the resource-like syntax to declare the class, and set the `stage` metaparameter to the name of the desired stage. For example: stage { 'pre': before => Stage['main'], } class { 'apt-updates': stage => 'pre', } } end ############################### # All of the provider plumbing for the resource types. require 'puppet/provider' require 'puppet/util/provider_features' # Add the feature handling module. extend Puppet::Util::ProviderFeatures # The provider that has been selected for the instance of the resource type. # @return [Puppet::Provider,nil] the selected provider or nil, if none has been selected # attr_reader :provider # the Type class attribute accessors class << self # The loader of providers to use when loading providers from disk. # Although it looks like this attribute provides a way to operate with different loaders of # providers that is not the case; the attribute is written when a new type is created, # and should not be changed thereafter. # @api private # attr_accessor :providerloader # @todo Don't know if this is a name, or a reference to a Provider instance (now marked up as an instance # of Provider. # @return [Puppet::Provider, nil] The default provider for this type, or nil if non is defines # attr_writer :defaultprovider end # The default provider, or the most suitable provider if no default provider was set. # @note a warning will be issued if no default provider has been configured and a search for the most # suitable provider returns more than one equally suitable provider. # @return [Puppet::Provider, nil] the default or most suitable provider, or nil if no provider was found # def self.defaultprovider return @defaultprovider if @defaultprovider suitable = suitableprovider # Find which providers are a default for this system. defaults = suitable.find_all { |provider| provider.default? } # If we don't have any default we use suitable providers defaults = suitable if defaults.empty? max = defaults.collect { |provider| provider.specificity }.max defaults = defaults.find_all { |provider| provider.specificity == max } if defaults.length > 1 Puppet.warning( "Found multiple default providers for #{self.name}: #{defaults.collect { |i| i.name.to_s }.join(", ")}; using #{defaults[0].name}" ) end @defaultprovider = defaults.shift unless defaults.empty? end # @return [Hash{??? => Puppet::Provider}] Returns a hash of WHAT EXACTLY for the given type # @todo what goes into this hash? def self.provider_hash_by_type(type) @provider_hashes ||= {} @provider_hashes[type] ||= {} end # @return [Hash{ ??? => Puppet::Provider}] Returns a hash of WHAT EXACTLY for this type. # @see provider_hash_by_type method to get the same for some other type def self.provider_hash Puppet::Type.provider_hash_by_type(self.name) end # Returns the provider having the given name. # This will load a provider if it is not already loaded. The returned provider is the first found provider # having the given name, where "first found" semantics is defined by the {providerloader} in use. # # @param name [String] the name of the provider to get # @return [Puppet::Provider, nil] the found provider, or nil if no provider of the given name was found # def self.provider(name) name = name.intern # If we don't have it yet, try loading it. @providerloader.load(name) unless provider_hash.has_key?(name) provider_hash[name] end # Returns a list of loaded providers by name. # This method will not load/search for available providers. # @return [Array] list of loaded provider names # def self.providers provider_hash.keys end # Returns true if the given name is a reference to a provider and if this is a suitable provider for # this type. # @todo How does the provider know if it is suitable for the type? Is it just suitable for the platform/ # environment where this method is executing? # @param name [String] the name of the provider for which validity is checked # @return [Boolean] true if the given name references a provider that is suitable # def self.validprovider?(name) name = name.intern (provider_hash.has_key?(name) && provider_hash[name].suitable?) end # Creates a new provider of a type. # This method must be called directly on the type that it's implementing. # @todo Fix Confusing Explanations! # Is this a new provider of a Type (metatype), or a provider of an instance of Type (a resource), or # a Provider (the implementation of a Type's behavior). CONFUSED. It calls magically named methods like # "providify" ... # @param name [String, Symbol] the name of the WHAT? provider? type? # @param options [Hash{Symbol => Object}] a hash of options, used by this method, and passed on to {#genclass}, (see # it for additional options to pass). # @option options [Puppet::Provider] :parent the parent provider (what is this?) # @option options [Puppet::Type] :resource_type the resource type, defaults to this type if unspecified # @return [Puppet::Provider] a provider ??? # @raise [Puppet::DevError] when the parent provider could not be found. # def self.provide(name, options = {}, &block) name = name.intern if unprovide(name) Puppet.debug "Reloading #{name} #{self.name} provider" end parent = if pname = options[:parent] options.delete(:parent) if pname.is_a? Class pname else if provider = self.provider(pname) provider else raise Puppet::DevError, "Could not find parent provider #{pname} of #{name}" end end else Puppet::Provider end options[:resource_type] ||= self self.providify provider = genclass( name, :parent => parent, :hash => provider_hash, :prefix => "Provider", :block => block, :include => feature_module, :extend => feature_module, :attributes => options ) provider end # Ensures there is a `:provider` parameter defined. # Should only be called if there are providers. # @return [void] def self.providify return if @paramhash.has_key? :provider newparam(:provider) do # We're using a hacky way to get the name of our type, since there doesn't # seem to be a correct way to introspect this at the time this code is run. # We expect that the class in which this code is executed will be something # like Puppet::Type::Ssh_authorized_key::ParameterProvider. desc <<-EOT The specific backend to use for this `#{self.to_s.split('::')[2].downcase}` resource. You will seldom need to specify this --- Puppet will usually discover the appropriate provider for your platform. EOT # This is so we can refer back to the type to get a list of # providers for documentation. class << self # The reference to a parent type for the parameter `:provider` used to get a list of # providers for documentation purposes. # attr_accessor :parenttype end # Provides the ability to add documentation to a provider. # def self.doc # Since we're mixing @doc with text from other sources, we must normalize # its indentation with scrub. But we don't need to manually scrub the # provider's doc string, since markdown_definitionlist sanitizes its inputs. scrub(@doc) + "Available providers are:\n\n" + parenttype.providers.sort { |a,b| a.to_s <=> b.to_s }.collect { |i| markdown_definitionlist( i, scrub(parenttype().provider(i).doc) ) }.join end # For each resource, the provider param defaults to # the type's default provider defaultto { prov = @resource.class.defaultprovider prov.name if prov } validate do |provider_class| provider_class = provider_class[0] if provider_class.is_a? Array provider_class = provider_class.class.name if provider_class.is_a?(Puppet::Provider) unless @resource.class.provider(provider_class) raise ArgumentError, "Invalid #{@resource.class.name} provider '#{provider_class}'" end end munge do |provider| provider = provider[0] if provider.is_a? Array provider = provider.intern if provider.is_a? String @resource.provider = provider if provider.is_a?(Puppet::Provider) provider.class.name else provider end end end.parenttype = self end # @todo this needs a better explanation # Removes the implementation class of a given provider. # @return [Object] returns what {Puppet::Util::ClassGen#rmclass} returns def self.unprovide(name) if @defaultprovider and @defaultprovider.name == name @defaultprovider = nil end rmclass(name, :hash => provider_hash, :prefix => "Provider") end # Returns a list of suitable providers for the given type. # A call to this method will load all providers if not already loaded and ask each if it is # suitable - those that are are included in the result. # @note This method also does some special processing which rejects a provider named `:fake` (for testing purposes). # @return [Array] Returns an array of all suitable providers. # def self.suitableprovider providerloader.loadall if provider_hash.empty? provider_hash.find_all { |name, provider| provider.suitable? }.collect { |name, provider| provider }.reject { |p| p.name == :fake } # For testing end # @return [Boolean] Returns true if this is something else than a `:provider`, or if it # is a provider and it is suitable, or if there is a default provider. Otherwise, false is returned. # def suitable? # If we don't use providers, then we consider it suitable. return true unless self.class.paramclass(:provider) # We have a provider and it is suitable. return true if provider && provider.class.suitable? # We're using the default provider and there is one. if !provider and self.class.defaultprovider self.provider = self.class.defaultprovider.name return true end # We specified an unsuitable provider, or there isn't any suitable # provider. false end # Sets the provider to the given provider/name. # @overload provider=(name) # Sets the provider to the result of resolving the name to an instance of Provider. # @param name [String] the name of the provider # @overload provider=(provider) # Sets the provider to the given instances of Provider. # @param provider [Puppet::Provider] the provider to set # @return [Puppet::Provider] the provider set # @raise [ArgumentError] if the provider could not be found/resolved. # def provider=(name) if name.is_a?(Puppet::Provider) @provider = name @provider.resource = self elsif klass = self.class.provider(name) @provider = klass.new(self) else raise ArgumentError, "Could not find #{name} provider of #{self.class.name}" end end ############################### # All of the relationship code. # Adds a block producing a single name (or list of names) of the given resource type name to autorequire. # Resources in the catalog that have the named type and a title that is included in the result will be linked # to the calling resource as a requirement. # # @example Autorequire the files File['foo', 'bar'] # autorequire( 'file', {|| ['foo', 'bar'] }) # # @param name [String] the name of a type of which one or several resources should be autorequired e.g. "file" # @yield [ ] a block returning list of names of given type to auto require # @yieldreturn [String, Array] one or several resource names for the named type # @return [void] # @dsl type # @api public # def self.autorequire(name, &block) @autorequires ||= {} @autorequires[name] = block end # Provides iteration over added auto-requirements (see {autorequire}). # @yieldparam type [String] the name of the type to autoriquire an instance of # @yieldparam block [Proc] a block producing one or several dependencies to auto require (see {autorequire}). # @yieldreturn [void] # @return [void] def self.eachautorequire @autorequires ||= {} @autorequires.each { |type, block| yield(type, block) } end # Adds dependencies to the catalog from added autorequirements. # See {autorequire} for how to add an auto-requirement. # @todo needs details - see the param rel_catalog, and type of this param # @param rel_catalog [Puppet::Resource::Catalog, nil] the catalog to # add dependencies to. Defaults to the current catalog (set when the # type instance was added to a catalog) # @raise [Puppet::DevError] if there is no catalog # def autorequire(rel_catalog = nil) rel_catalog ||= catalog raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog reqs = [] self.class.eachautorequire { |type, block| # Ignore any types we can't find, although that would be a bit odd. next unless Puppet::Type.type(type) # Retrieve the list of names from the block. next unless list = self.instance_eval(&block) list = [list] unless list.is_a?(Array) # Collect the current prereqs list.each { |dep| # Support them passing objects directly, to save some effort. unless dep.is_a? Puppet::Type # Skip autorequires that we aren't managing unless dep = rel_catalog.resource(type, dep) next end end reqs << Puppet::Relationship.new(dep, self) } } reqs end # Builds the dependencies associated with this resource. # # @return [Array] list of relationships to other resources def builddepends # Handle the requires self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.to_edges end end.flatten.reject { |r| r.nil? } end # Sets the initial list of tags to associate to this resource. # # @return [void] ??? def tags=(list) tag(self.class.name) tag(*list) end # @comment - these two comments were floating around here, and turned up as documentation # for the attribute "title", much to my surprise and amusement. Clearly these comments # are orphaned ... I think they can just be removed as what they say should be covered # by the now added yardoc. (Yo! to quote some of the other actual awsome specific comments applicable # to objects called from elsewhere, or not. ;-) # # @comment Types (which map to resources in the languages) are entirely composed of # attribute value pairs. Generally, Puppet calls any of these things an # 'attribute', but these attributes always take one of three specific # forms: parameters, metaparams, or properties. # @comment In naming methods, I have tried to consistently name the method so # that it is clear whether it operates on all attributes (thus has 'attr' in # the method name, or whether it operates on a specific type of attributes. # The title attribute of WHAT ??? # @todo Figure out what this is the title attribute of (it appears on line 1926 currently). # @return [String] the title attr_writer :title # The noop attribute of WHAT ??? does WHAT??? # @todo Figure out what this is the noop attribute of (it appears on line 1931 currently). # @return [???] the noop WHAT ??? (mode? if so of what, or noop for an instance of the type, or for all # instances of a type, or for what??? # attr_writer :noop include Enumerable # class methods dealing with Type management public # The Type class attribute accessors class << self # @return [String] the name of the resource type; e.g., "File" # attr_reader :name # @return [Boolean] true if the type should send itself a refresh event on change. # attr_accessor :self_refresh include Enumerable, Puppet::Util::ClassGen include Puppet::MetaType::Manager include Puppet::Util include Puppet::Util::Logging end # Initializes all of the variables that must be initialized for each subclass. # @todo Does the explanation make sense? # @return [void] def self.initvars # all of the instances of this class @objects = Hash.new @aliases = Hash.new @defaults = {} @parameters ||= [] @validproperties = {} @properties = [] @parameters = [] @paramhash = {} @paramdoc = Hash.new { |hash,key| key = key.intern if key.is_a?(String) if hash.include?(key) hash[key] else "Param Documentation for #{key} not found" end } @doc ||= "" end # Returns the name of this type (if specified) or the parent type #to_s. # The returned name is on the form "Puppet::Type::", where the first letter of name is # capitalized. # @return [String] the fully qualified name Puppet::Type:: where the first letter of name is captialized # def self.to_s if defined?(@name) "Puppet::Type::#{@name.to_s.capitalize}" else super end end # Creates a `validate` method that is used to validate a resource before it is operated on. # The validation should raise exceptions if the validation finds errors. (It is not recommended to # issue warnings as this typically just ends up in a logfile - you should fail if a validation fails). # The easiest way to raise an appropriate exception is to call the method {Puppet::Util::Errors.fail} with # the message as an argument. # # @yield [ ] a required block called with self set to the instance of a Type class representing a resource. # @return [void] # @dsl type # @api public # def self.validate(&block) define_method(:validate, &block) end # @return [String] The file from which this type originates from attr_accessor :file # @return [Integer] The line in {#file} from which this type originates from attr_accessor :line # @todo what does this mean "this resource" (sounds like this if for an instance of the type, not the meta Type), # but not sure if this is about the catalog where the meta Type is included) # @return [??? TODO] The catalog that this resource is stored in. attr_accessor :catalog # @return [Boolean] Flag indicating if this type is exported attr_accessor :exported # @return [Boolean] Flag indicating if the type is virtual (it should not be). attr_accessor :virtual # Creates a log entry with the given message at the log level specified by the parameter `loglevel` # @return [void] # def log(msg) Puppet::Util::Log.create( :level => @parameters[:loglevel].value, :message => msg, :source => self ) end # instance methods related to instance intrinsics # e.g., initialize and name public # @return [Hash] hash of parameters originally defined # @api private attr_reader :original_parameters # Creates an instance of Type from a hash or a {Puppet::Resource}. # @todo Unclear if this is a new Type or a new instance of a given type (the initialization ends # with calling validate - which seems like validation of an instance of a given type, not a new # meta type. # # @todo Explain what the Hash and Resource are. There seems to be two different types of # resources; one that causes the title to be set to resource.title, and one that # causes the title to be resource.ref ("for components") - what is a component? # # @overload initialize(hash) # @param [Hash] hash # @raise [Puppet::ResourceError] when the type validation raises # Puppet::Error or ArgumentError # @overload initialize(resource) # @param resource [Puppet:Resource] # @raise [Puppet::ResourceError] when the type validation raises # Puppet::Error or ArgumentError # def initialize(resource) resource = self.class.hash2resource(resource) unless resource.is_a?(Puppet::Resource) # The list of parameter/property instances. @parameters = {} # Set the title first, so any failures print correctly. if resource.type.to_s.downcase.to_sym == self.class.name self.title = resource.title else # This should only ever happen for components self.title = resource.ref end [:file, :line, :catalog, :exported, :virtual].each do |getter| setter = getter.to_s + "=" if val = resource.send(getter) self.send(setter, val) end end @tags = resource.tags @original_parameters = resource.to_hash set_name(@original_parameters) set_default(:provider) set_parameters(@original_parameters) begin self.validate if self.respond_to?(:validate) rescue Puppet::Error, ArgumentError => detail error = Puppet::ResourceError.new("Validation of #{ref} failed: #{detail}") adderrorcontext(error, detail) raise error end end private # Sets the name of the resource from a hash containing a mapping of `name_var` to value. # Sets the value of the property/parameter appointed by the `name_var` (if it is defined). The value set is # given by the corresponding entry in the given hash - e.g. if name_var appoints the name `:path` the value # of `:path` is set to the value at the key `:path` in the given hash. As a side effect this key/value is then # removed from the given hash. # # @note This method mutates the given hash by removing the entry with a key equal to the value # returned from name_var! # @param hash [Hash] a hash of what # @return [void] def set_name(hash) self[name_var] = hash.delete(name_var) if name_var end # Sets parameters from the given hash. # Values are set in _attribute order_ i.e. higher priority attributes before others, otherwise in # the order they were specified (as opposed to just setting them in the order they happen to appear in # when iterating over the given hash). # # Attributes that are not included in the given hash are set to their default value. # # @todo Is this description accurate? Is "ensure" an example of such a higher priority attribute? # @return [void] # @raise [Puppet::DevError] when impossible to set the value due to some problem # @raise [ArgumentError, TypeError, Puppet::Error] when faulty arguments have been passed # def set_parameters(hash) # Use the order provided by allattrs, but add in any # extra attributes from the resource so we get failures # on invalid attributes. no_values = [] (self.class.allattrs + hash.keys).uniq.each do |attr| begin # Set any defaults immediately. This is mostly done so # that the default provider is available for any other # property validation. if hash.has_key?(attr) self[attr] = hash[attr] else no_values << attr end rescue ArgumentError, Puppet::Error, TypeError raise rescue => detail error = Puppet::DevError.new( "Could not set #{attr} on #{self.class.name}: #{detail}") error.set_backtrace(detail.backtrace) raise error end end no_values.each do |attr| set_default(attr) end end public # Finishes any outstanding processing. # This method should be called as a final step in setup, # to allow the parameters that have associated auto-require needs to be processed. # # @todo what is the expected sequence here - who is responsible for calling this? When? # Is the returned type correct? # @return [Array] the validated list/set of attributes # def finish # Call post_compile hook on every parameter that implements it. This includes all subclasses # of parameter including, but not limited to, regular parameters, metaparameters, relationship # parameters, and properties. eachparameter do |parameter| parameter.post_compile if parameter.respond_to? :post_compile end # Make sure all of our relationships are valid. Again, must be done # when the entire catalog is instantiated. self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.validate_relationship end end.flatten.reject { |r| r.nil? } end # @comment For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. # Returns the resource's name # @todo There is a comment in source that this is not quite the same as ':title' and that a switch should # be made... # @return [String] the name of a resource def name self[:name] end # Returns the parent of this in the catalog. In case of an erroneous catalog # where multiple parents have been produced, the first found (non # deterministic) parent is returned. # @return [Puppet::Type, nil] the # containing resource or nil if there is no catalog or no containing # resource. def parent return nil unless catalog @parent ||= if parents = catalog.adjacent(self, :direction => :in) parents.shift else nil end end # Returns a reference to this as a string in "Type[name]" format. # @return [String] a reference to this object on the form 'Type[name]' # def ref # memoizing this is worthwhile ~ 3 percent of calls are the "first time # around" in an average run of Puppet. --daniel 2012-07-17 @ref ||= "#{self.class.name.to_s.capitalize}[#{self.title}]" end # (see self_refresh) # @todo check that meaningful yardoc is produced - this method delegates to "self.class.self_refresh" # @return [Boolean] - ??? returns true when ... what? # def self_refresh? self.class.self_refresh end # Marks the object as "being purged". # This method is used by transactions to forbid deletion when there are dependencies. # @todo what does this mean; "mark that we are purging" (purging what from where). How to use/when? # Is this internal API in transactions? # @see purging? def purging @purging = true end # Returns whether this resource is being purged or not. # This method is used by transactions to forbid deletion when there are dependencies. # @return [Boolean] the current "purging" state # def purging? if defined?(@purging) @purging else false end end # Returns the title of this object, or its name if title was not explicetly set. # If the title is not already set, it will be computed by looking up the {#name_var} and using # that value as the title. # @todo it is somewhat confusing that if the name_var is a valid parameter, it is assumed to # be the name_var called :name, but if it is a property, it uses the name_var. # It is further confusing as Type in some respects supports multiple namevars. # # @return [String] Returns the title of this object, or its name if title was not explicetly set. # @raise [??? devfail] if title is not set, and name_var can not be found. def title unless @title if self.class.validparameter?(name_var) @title = self[:name] elsif self.class.validproperty?(name_var) @title = self.should(name_var) else self.devfail "Could not find namevar #{name_var} for #{self.class.name}" end end @title end # Produces a reference to this in reference format. # @see #ref # def to_s self.ref end # Convert this resource type instance to a Puppet::Resource. # @return [Puppet::Resource] Returns a serializable representation of this resource # def to_resource resource = self.retrieve_resource resource.tag(*self.tags) @parameters.each do |name, param| # Avoid adding each instance name twice next if param.class.isnamevar? and param.value == self.title # We've already got property values next if param.is_a?(Puppet::Property) resource[name] = param.value end resource end # @return [Boolean] Returns whether the resource is virtual or not def virtual?; !!@virtual; end # @return [Boolean] Returns whether the resource is exported or not def exported?; !!@exported; end # @return [Boolean] Returns whether the resource is applicable to `:device` # Returns true if a resource of this type can be evaluated on a 'network device' kind # of hosts. # @api private def appliable_to_device? self.class.can_apply_to(:device) end # @return [Boolean] Returns whether the resource is applicable to `:host` # Returns true if a resource of this type can be evaluated on a regular generalized computer (ie not an appliance like a network device) # @api private def appliable_to_host? self.class.can_apply_to(:host) end end end require 'puppet/provider' puppet/network.rb000064400000000135147634041260010105 0ustar00# Just a stub, so we can correctly scope other classes. module Puppet::Network # :nodoc: end puppet/daemon.rb000064400000012275147634041260007667 0ustar00require 'puppet/application' require 'puppet/scheduler' # Run periodic actions and a network server in a daemonized process. # # A Daemon has 3 parts: # * config reparse # * (optional) an agent that responds to #run # * (optional) a server that response to #stop, #start, and #wait_for_shutdown # # The config reparse will occur periodically based on Settings. The server will # be started and is expected to manage its own run loop (and so not block the # start call). The server will, however, still be waited for by using the # #wait_for_shutdown method. The agent is run periodically and a time interval # based on Settings. The config reparse will update this time interval when # needed. # # The Daemon is also responsible for signal handling, starting, stopping, # running the agent on demand, and reloading the entire process. It ensures # that only one Daemon is running by using a lockfile. # # @api private class Puppet::Daemon SIGNAL_CHECK_INTERVAL = 5 attr_accessor :agent, :server, :argv attr_reader :signals def initialize(pidfile, scheduler = Puppet::Scheduler::Scheduler.new()) @scheduler = scheduler @pidfile = pidfile @signals = [] end def daemonname Puppet.run_mode.name end # Put the daemon into the background. def daemonize if pid = fork Process.detach(pid) exit(0) end create_pidfile # Get rid of console logging Puppet::Util::Log.close(:console) Process.setsid Dir.chdir("/") close_streams end # Close stdin/stdout/stderr so that we can finish our transition into 'daemon' mode. # @return nil def self.close_streams() Puppet.debug("Closing streams for daemon mode") begin $stdin.reopen "/dev/null" $stdout.reopen "/dev/null", "a" $stderr.reopen $stdout Puppet::Util::Log.reopen Puppet.debug("Finished closing streams for daemon mode") rescue => detail Puppet.err "Could not start #{Puppet.run_mode.name}: #{detail}" Puppet::Util::replace_file("/tmp/daemonout", 0644) do |f| f.puts "Could not start #{Puppet.run_mode.name}: #{detail}" end exit(12) end end # Convenience signature for calling Puppet::Daemon.close_streams def close_streams() Puppet::Daemon.close_streams end def reexec raise Puppet::DevError, "Cannot reexec unless ARGV arguments are set" unless argv command = $0 + " " + argv.join(" ") Puppet.notice "Restarting with '#{command}'" stop(:exit => false) exec(command) end def reload return unless agent if agent.running? Puppet.notice "Not triggering already-running agent" return end agent.run({:splay => false}) end def restart Puppet::Application.restart! reexec unless agent and agent.running? end def reopen_logs Puppet::Util::Log.reopen end # Trap a couple of the main signals. This should probably be handled # in a way that anyone else can register callbacks for traps, but, eh. def set_signal_traps [:INT, :TERM].each do |signal| Signal.trap(signal) do Puppet.notice "Caught #{signal}; exiting" stop end end # extended signals not supported under windows if !Puppet.features.microsoft_windows? signals = {:HUP => :restart, :USR1 => :reload, :USR2 => :reopen_logs } signals.each do |signal, method| Signal.trap(signal) do Puppet.notice "Caught #{signal}; storing #{method}" @signals << method end end end end # Stop everything def stop(args = {:exit => true}) Puppet::Application.stop! server.stop if server remove_pidfile Puppet::Util::Log.close_all exit if args[:exit] end def start set_signal_traps create_pidfile raise Puppet::DevError, "Daemons must have an agent, server, or both" unless agent or server # Start the listening server, if required. server.start if server # Finally, loop forever running events - or, at least, until we exit. run_event_loop server.wait_for_shutdown if server end private # Create a pidfile for our daemon, so we can be stopped and others # don't try to start. def create_pidfile raise "Could not create PID file: #{@pidfile.file_path}" unless @pidfile.lock end # Remove the pid file for our daemon. def remove_pidfile @pidfile.unlock end def run_event_loop agent_run = Puppet::Scheduler.create_job(Puppet[:runinterval], Puppet[:splay], Puppet[:splaylimit]) do # Splay for the daemon is handled in the scheduler agent.run(:splay => false) end reparse_run = Puppet::Scheduler.create_job(Puppet[:filetimeout]) do Puppet.settings.reparse_config_files agent_run.run_interval = Puppet[:runinterval] if Puppet[:filetimeout] == 0 reparse_run.disable else reparse_run.run_interval = Puppet[:filetimeout] end end signal_loop = Puppet::Scheduler.create_job(SIGNAL_CHECK_INTERVAL) do while method = @signals.shift Puppet.notice "Processing #{method}" send(method) end end reparse_run.disable if Puppet[:filetimeout] == 0 agent_run.disable unless agent @scheduler.run_loop([reparse_run, agent_run, signal_loop]) end end puppet/vendor.rb000064400000003736147634041260007723 0ustar00module Puppet # Simple module to manage vendored code. # # To vendor a library: # # * Download its whole git repo or untar into `lib/puppet/vendor/` # * Create a vendor/puppetload_libraryname.rb file to add its libdir into the $:. # (Look at existing load_xxx files, they should all follow the same pattern). # * Add a /PUPPET_README.md file describing what the library is for # and where it comes from. # * To load the vendored lib upfront, add a `require ''`line to # `vendor/require_vendored.rb`. # * To load the vendored lib on demand, add a comment to `vendor/require_vendored.rb` # to make it clear it should not be loaded upfront. # # At runtime, the #load_vendored method should be called. It will ensure # all vendored libraries are added to the global `$:` path, and # will then call execute the up-front loading specified in `vendor/require_vendored.rb`. # # The intention is to not change vendored libraries and to eventually # make adding them in optional so that distros can simply adjust their # packaging to exclude this directory and the various load_xxx.rb scripts # if they wish to install these gems as native packages. # class Vendor class << self # @api private def vendor_dir File.join([File.dirname(File.expand_path(__FILE__)), "vendor"]) end # @api private def load_entry(entry) Puppet.debug("Loading vendored #{$1}") load "#{vendor_dir}/#{entry}" end # @api private def require_libs require 'puppet/vendor/require_vendored' end # Configures the path for all vendored libraries and loads required libraries. # (This is the entry point for loading vendored libraries). # def load_vendored Dir.entries(vendor_dir).each do |entry| if entry.match(/load_(\w+?)\.rb$/) load_entry entry end end require_libs end end end end puppet/interface.rb000064400000016211147634041260010356 0ustar00require 'puppet' require 'puppet/util/autoload' require 'prettyprint' require 'semver' # @api public class Puppet::Interface require 'puppet/interface/documentation' require 'puppet/interface/face_collection' require 'puppet/interface/action' require 'puppet/interface/action_builder' require 'puppet/interface/action_manager' require 'puppet/interface/option' require 'puppet/interface/option_builder' require 'puppet/interface/option_manager' include FullDocs include Puppet::Interface::ActionManager extend Puppet::Interface::ActionManager include Puppet::Interface::OptionManager extend Puppet::Interface::OptionManager include Puppet::Util class << self # This is just so we can search for actions. We only use its # list of directories to search. # @api private # @deprecated def autoloader Puppet.deprecation_warning("Puppet::Interface.autoloader is deprecated; please use Puppet::Interface#loader instead") @autoloader ||= Puppet::Util::Autoload.new(:application, "puppet/face") end # Lists all loaded faces # @return [Array] The names of the loaded faces def faces Puppet::Interface::FaceCollection.faces end # Register a face # @param instance [Puppet::Interface] The face # @return [void] # @api private def register(instance) Puppet::Interface::FaceCollection.register(instance) end # Defines a new Face. # @todo Talk about using Faces DSL inside the block # # @param name [Symbol] the name of the face # @param version [String] the version of the face (this should # conform to {http://semver.org/ Semantic Versioning}) # @overload define(name, version, {|| ... }) # @return [Puppet::Interface] The created face # @api public # @dsl Faces def define(name, version, &block) face = Puppet::Interface::FaceCollection[name, version] if face.nil? then face = self.new(name, version) Puppet::Interface::FaceCollection.register(face) # REVISIT: Shouldn't this be delayed until *after* we evaluate the # current block, not done before? --daniel 2011-04-07 face.load_actions end face.instance_eval(&block) if block_given? return face end # Retrieves a face by name and version. Use `:current` for the # version to get the most recent available version. # # @param name [Symbol] the name of the face # @param version [String, :current] the version of the face # # @return [Puppet::Interface] the face # # @api public def face?(name, version) Puppet::Interface::FaceCollection[name, version] end # Retrieves a face by name and version # # @param name [Symbol] the name of the face # @param version [String] the version of the face # # @return [Puppet::Interface] the face # # @api public def [](name, version) unless face = Puppet::Interface::FaceCollection[name, version] # REVISIT (#18042) no sense in rechecking if version == :current -- josh if Puppet::Interface::FaceCollection[name, :current] raise Puppet::Error, "Could not find version #{version} of #{name}" else raise Puppet::Error, "Could not find Puppet Face #{name.to_s}" end end face end # Retrieves an action for a face # @param name [Symbol] The face # @param action [Symbol] The action name # @param version [String, :current] The version of the face # @return [Puppet::Interface::Action] The action def find_action(name, action, version = :current) Puppet::Interface::FaceCollection.get_action_for_face(name, action, version) end end ######################################################################## # Documentation. We currently have to rewrite both getters because we share # the same instance between build-time and the runtime instance. When that # splits out this should merge into a module that both the action and face # include. --daniel 2011-04-17 # Returns the synopsis for the face. This shows basic usage and global # options. # @return [String] usage synopsis # @api private def synopsis build_synopsis self.name, '' end ######################################################################## # The name of the face # @return [Symbol] # @api private attr_reader :name # The version of the face # @return [SemVer] attr_reader :version # The autoloader instance for the face # @return [Puppet::Util::Autoload] # @api private attr_reader :loader private :loader # @api private def initialize(name, version, &block) unless SemVer.valid?(version) raise ArgumentError, "Cannot create face #{name.inspect} with invalid version number '#{version}'!" end @name = Puppet::Interface::FaceCollection.underscorize(name) @version = SemVer.new(version) # The few bits of documentation we actually demand. The default license # is a favour to our end users; if you happen to get that in a core face # report it as a bug, please. --daniel 2011-04-26 @authors = [] @license = 'All Rights Reserved' @loader = Puppet::Util::Autoload.new(@name, "puppet/face/#{@name}") instance_eval(&block) if block_given? end # Loads all actions defined in other files. # # @return [void] # @api private def load_actions loader.loadall end # Returns a string representation with the face's name and version # @return [String] def to_s "Puppet::Face[#{name.inspect}, #{version.inspect}]" end ######################################################################## # Action decoration, whee! You are not expected to care about this code, # which exists to support face building and construction. I marked these # private because the implementation is crude and ugly, and I don't yet know # enough to work out how to make it clean. # # Once we have established that these methods will likely change radically, # to be unrecognizable in the final outcome. At which point we will throw # all this away, replace it with something nice, and work out if we should # be making this visible to the outside world... --daniel 2011-04-14 private # @return [void] # @api private def __invoke_decorations(type, action, passed_args = [], passed_options = {}) [:before, :after].member?(type) or fail "unknown decoration type #{type}" # Collect the decoration methods matching our pass. methods = action.options.select do |name| passed_options.has_key? name end.map do |name| action.get_option(name).__decoration_name(type) end methods.reverse! if type == :after # Exceptions here should propagate up; this implements a hook we can use # reasonably for option validation. methods.each do |hook| respond_to? hook and self.__send__(hook, action, passed_args, passed_options) end end # @return [void] # @api private def __add_method(name, proc) meta_def(name, &proc) method(name).unbind end # @return [void] # @api private def self.__add_method(name, proc) define_method(name, proc) instance_method(name) end end puppet/data_binding.rb000064400000000552147634041260011022 0ustar00require 'puppet/indirector' # A class for managing data lookups class Puppet::DataBinding class LookupError < Puppet::Error; end # Set up indirection, so that data can be looked for in the compiler extend Puppet::Indirector indirects(:data_binding, :terminus_setting => :data_binding_terminus, :doc => "Where to find external data bindings.") end puppet/file_bucket/dipper.rb000064400000007115147634041260012160 0ustar00require 'pathname' require 'puppet/file_bucket' require 'puppet/file_bucket/file' require 'puppet/indirector/request' class Puppet::FileBucket::Dipper include Puppet::Util::Checksums # This is a transitional implementation that uses REST # to access remote filebucket files. attr_accessor :name # Creates a bucket client def initialize(hash = {}) # Emulate the XMLRPC client server = hash[:Server] port = hash[:Port] || Puppet[:masterport] environment = Puppet[:environment] if hash.include?(:Path) @local_path = hash[:Path] @rest_path = nil else @local_path = nil @rest_path = "https://#{server}:#{port}/#{environment}/file_bucket_file/" end @checksum_type = Puppet[:digest_algorithm].to_sym @digest = method(@checksum_type) end def local? !! @local_path end # Backs up a file to the file bucket def backup(file) file_handle = Puppet::FileSystem.pathname(file) raise(ArgumentError, "File #{file} does not exist") unless Puppet::FileSystem.exist?(file_handle) begin file_bucket_file = Puppet::FileBucket::File.new(file_handle, :bucket_path => @local_path) files_original_path = absolutize_path(file) dest_path = "#{@rest_path}#{file_bucket_file.name}/#{files_original_path}" file_bucket_path = "#{@rest_path}#{file_bucket_file.checksum_type}/#{file_bucket_file.checksum_data}/#{files_original_path}" # Make a HEAD request for the file so that we don't waste time # uploading it if it already exists in the bucket. unless Puppet::FileBucket::File.indirection.head(file_bucket_path) Puppet::FileBucket::File.indirection.save(file_bucket_file, dest_path) end return file_bucket_file.checksum_data rescue => detail message = "Could not back up #{file}: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message, detail.backtrace end end # Retrieves a file by sum. def getfile(sum) get_bucket_file(sum).to_s end # Retrieves a FileBucket::File by sum. def get_bucket_file(sum) source_path = "#{@rest_path}#{@checksum_type}/#{sum}" file_bucket_file = Puppet::FileBucket::File.indirection.find(source_path, :bucket_path => @local_path) raise Puppet::Error, "File not found" unless file_bucket_file file_bucket_file end # Restores the file def restore(file, sum) restore = true file_handle = Puppet::FileSystem.pathname(file) if Puppet::FileSystem.exist?(file_handle) cursum = Puppet::FileBucket::File.new(file_handle).checksum_data() # if the checksum has changed... # this might be extra effort if cursum == sum restore = false end end if restore if newcontents = get_bucket_file(sum) newsum = newcontents.checksum_data changed = nil if Puppet::FileSystem.exist?(file_handle) and ! Puppet::FileSystem.writable?(file_handle) changed = Puppet::FileSystem.stat(file_handle).mode ::File.chmod(changed | 0200, file) end ::File.open(file, ::File::WRONLY|::File::TRUNC|::File::CREAT) { |of| of.binmode source_stream = newcontents.stream do |source_stream| FileUtils.copy_stream(source_stream, of) end #of.print(newcontents) } ::File.chmod(changed, file) if changed else Puppet.err "Could not find file with checksum #{sum}" return nil end return newsum else return nil end end private def absolutize_path( path ) Pathname.new(path).realpath end end puppet/file_bucket/file.rb000064400000007520147634041260011614 0ustar00require 'puppet/file_bucket' require 'puppet/indirector' require 'puppet/util/checksums' require 'digest/md5' require 'stringio' class Puppet::FileBucket::File # This class handles the abstract notion of a file in a filebucket. # There are mechanisms to save and load this file locally and remotely in puppet/indirector/filebucketfile/* # There is a compatibility class that emulates pre-indirector filebuckets in Puppet::FileBucket::Dipper extend Puppet::Indirector indirects :file_bucket_file, :terminus_class => :selector attr :bucket_path def self.supported_formats [:s, :pson] end def self.default_format # This should really be :raw, like is done for Puppet::FileServing::Content # but this class hasn't historically supported `from_raw`, so switching # would break compatibility between newer 3.x agents talking to older 3.x # masters. However, to/from_s has been supported and achieves the desired # result without breaking compatibility. :s end def initialize(contents, options = {}) case contents when String @contents = StringContents.new(contents) when Pathname @contents = FileContents.new(contents) else raise ArgumentError.new("contents must be a String or Pathname, got a #{contents.class}") end @bucket_path = options.delete(:bucket_path) @checksum_type = Puppet[:digest_algorithm].to_sym raise ArgumentError.new("Unknown option(s): #{options.keys.join(', ')}") unless options.empty? end # @return [Num] The size of the contents def size @contents.size() end # @return [IO] A stream that reads the contents def stream(&block) @contents.stream(&block) end def checksum_type @checksum_type.to_s end def checksum "{#{checksum_type}}#{checksum_data}" end def checksum_data @checksum_data ||= @contents.checksum_data(@checksum_type) end def to_s @contents.to_s end def contents to_s end def name "#{checksum_type}/#{checksum_data}" end def self.from_s(contents) self.new(contents) end def to_data_hash # Note that this serializes the entire data to a string and places it in a hash. { "contents" => contents.to_s } end def self.from_data_hash(data) self.new(data["contents"]) end def to_pson Puppet.deprecation_warning("Serializing Puppet::FileBucket::File objects to pson is deprecated.") to_data_hash.to_pson end # This method is deprecated, but cannot be removed for awhile, otherwise # older agents sending pson couldn't backup to filebuckets on newer masters def self.from_pson(pson) Puppet.deprecation_warning("Deserializing Puppet::FileBucket::File objects from pson is deprecated. Upgrade to a newer version.") self.from_data_hash(pson) end private class StringContents def initialize(content) @contents = content; end def stream(&block) s = StringIO.new(@contents) begin block.call(s) ensure s.close end end def size @contents.size end def checksum_data(base_method) Puppet.info("Computing checksum on string") Puppet::Util::Checksums.method(base_method).call(@contents) end def to_s # This is not so horrible as for FileContent, but still possible to mutate the content that the # checksum is based on... so semi horrible... return @contents; end end class FileContents def initialize(path) @path = path end def stream(&block) Puppet::FileSystem.open(@path, nil, 'rb', &block) end def size Puppet::FileSystem.size(@path) end def checksum_data(base_method) Puppet.info("Computing checksum on file #{@path}") Puppet::Util::Checksums.method(:"#{base_method}_file").call(@path) end def to_s Puppet::FileSystem::binread(@path) end end end puppet/file_system/path_pattern.rb000064400000004353147634041260013436 0ustar00require 'pathname' module Puppet::FileSystem class PathPattern class InvalidPattern < Puppet::Error; end TRAVERSAL = /^\.\.$/ ABSOLUTE_UNIX = /^\// ABSOLUTE_WINDOWS = /^[a-z]:/i #ABSOLUT_VODKA #notappearinginthisclass CURRENT_DRIVE_RELATIVE_WINDOWS = /^\\/ def self.relative(pattern) RelativePathPattern.new(pattern) end def self.absolute(pattern) AbsolutePathPattern.new(pattern) end class << self protected :new end # @param prefix [AbsolutePathPattern] An absolute path pattern instance # @return [AbsolutePathPattern] A new AbsolutePathPattern prepended with # the passed prefix's pattern. def prefix_with(prefix) new_pathname = prefix.pathname + pathname self.class.absolute(new_pathname.to_s) end def glob Dir.glob(pathname.to_s) end def to_s pathname.to_s end protected attr_reader :pathname private def validate @pathname.each_filename do |e| if e =~ TRAVERSAL raise(InvalidPattern, "PathPatterns cannot be created with directory traversals.") end end case @pathname.to_s when CURRENT_DRIVE_RELATIVE_WINDOWS raise(InvalidPattern, "A PathPattern cannot be a Windows current drive relative path.") end end def initialize(pattern) begin @pathname = Pathname.new(pattern.strip) rescue ArgumentError => error raise InvalidPattern.new("PathPatterns cannot be created with a zero byte.", error) end validate end end class RelativePathPattern < PathPattern def absolute? false end def validate super case @pathname.to_s when ABSOLUTE_WINDOWS raise(InvalidPattern, "A relative PathPattern cannot be prefixed with a drive.") when ABSOLUTE_UNIX raise(InvalidPattern, "A relative PathPattern cannot be an absolute path.") end end end class AbsolutePathPattern < PathPattern def absolute? true end def validate super if @pathname.to_s !~ ABSOLUTE_UNIX and @pathname.to_s !~ ABSOLUTE_WINDOWS raise(InvalidPattern, "An absolute PathPattern cannot be a relative path.") end end end end puppet/file_system/memory_impl.rb000064400000002563147634041260013277 0ustar00class Puppet::FileSystem::MemoryImpl def initialize(*files) @files = files + all_children_of(files) end def exist?(path) path.exist? end def directory?(path) path.directory? end def file?(path) path.file? end def executable?(path) path.executable? end def children(path) path.children end def each_line(path, &block) path.each_line(&block) end def pathname(path) find(path) || Puppet::FileSystem::MemoryFile.a_missing_file(path) end def basename(path) path.duplicate_as(File.basename(path_string(path))) end def path_string(object) object.path end def read(path) handle = assert_path(path).handle handle.read end def read_preserve_line_endings(path) read(path) end def open(path, *args, &block) handle = assert_path(path).handle if block_given? yield handle else return handle end end def assert_path(path) if path.is_a?(Puppet::FileSystem::MemoryFile) path else find(path) or raise ArgumentError, "Unable to find registered object for #{path.inspect}" end end private def find(path) @files.find { |file| file.path == path } end def all_children_of(files) children = files.collect(&:children).flatten if children.empty? [] else children + all_children_of(children) end end end puppet/file_system/memory_file.rb000064400000003047147634041260013253 0ustar00# An in-memory file abstraction. Commonly used with Puppet::FileSystem::File#overlay # @api private class Puppet::FileSystem::MemoryFile attr_reader :path, :children def self.a_missing_file(path) new(path, :exist? => false, :executable? => false) end def self.a_regular_file_containing(path, content) new(path, :exist? => true, :executable? => false, :content => content) end def self.an_executable(path) new(path, :exist? => true, :executable? => true) end def self.a_directory(path, children = []) new(path, :exist? => true, :excutable? => true, :directory? => true, :children => children) end def initialize(path, properties) @path = path @properties = properties @children = (properties[:children] || []).collect do |child| child.duplicate_as(File.join(@path, child.path)) end end def directory?; @properties[:directory?]; end def exist?; @properties[:exist?]; end def executable?; @properties[:executable?]; end def each_line(&block) handle.each_line(&block) end def handle raise Errno::ENOENT unless exist? StringIO.new(@properties[:content] || '') end def duplicate_as(other_path) self.class.new(other_path, @properties) end def absolute? Pathname.new(path).absolute? end def to_path path end def to_s to_path end # Used by Ruby 1.8.7 file system abstractions when operating on Pathname like things. def to_str to_path end def inspect "" end end puppet/file_system/file19windows.rb000064400000006257147634041260013456 0ustar00require 'puppet/file_system/file19' require 'puppet/util/windows' class Puppet::FileSystem::File19Windows < Puppet::FileSystem::File19 def exist?(path) if ! Puppet.features.manages_symlinks? return ::File.exist?(path) end path = path.to_str if path.respond_to?(:to_str) # support WatchedFile path = path.to_s # support String and Pathname begin if Puppet::Util::Windows::File.symlink?(path) path = Puppet::Util::Windows::File.readlink(path) end ! Puppet::Util::Windows::File.stat(path).nil? rescue # generally INVALID_HANDLE_VALUE which means 'file not found' false end end def symlink(path, dest, options = {}) raise_if_symlinks_unsupported dest_exists = exist?(dest) # returns false on dangling symlink dest_stat = Puppet::Util::Windows::File.stat(dest) if dest_exists # silent fail to preserve semantics of original FileUtils return 0 if dest_exists && dest_stat.ftype == 'directory' if dest_exists && dest_stat.ftype == 'file' && options[:force] != true raise(Errno::EEXIST, "#{dest} already exists and the :force option was not specified") end if options[:noop] != true ::File.delete(dest) if dest_exists # can only be file Puppet::Util::Windows::File.symlink(path, dest) end 0 end def symlink?(path) return false if ! Puppet.features.manages_symlinks? Puppet::Util::Windows::File.symlink?(path) end def readlink(path) raise_if_symlinks_unsupported Puppet::Util::Windows::File.readlink(path) end def unlink(*file_names) if ! Puppet.features.manages_symlinks? return ::File.unlink(*file_names) end file_names.each do |file_name| file_name = file_name.to_s # handle PathName stat = Puppet::Util::Windows::File.stat(file_name) rescue nil # sigh, Ruby + Windows :( if stat && stat.ftype == 'directory' if Puppet::Util::Windows::File.symlink?(file_name) Dir.rmdir(file_name) else raise Errno::EPERM.new(file_name) end else ::File.unlink(file_name) end end file_names.length end def stat(path) Puppet::Util::Windows::File.stat(path) end def lstat(path) if ! Puppet.features.manages_symlinks? return Puppet::Util::Windows::File.stat(path) end Puppet::Util::Windows::File.lstat(path) end def chmod(mode, path) Puppet::Util::Windows::Security.set_mode(mode, path.to_s) end def read_preserve_line_endings(path) contents = path.read( :mode => 'rb', :encoding => Encoding::UTF_8) contents = path.read( :mode => 'rb', :encoding => Encoding::default_external) unless contents.valid_encoding? contents = path.read unless contents.valid_encoding? contents end private def raise_if_symlinks_unsupported if ! Puppet.features.manages_symlinks? msg = "This version of Windows does not support symlinks. Windows Vista / 2008 or higher is required." raise Puppet::Util::Windows::Error.new(msg) end if ! Puppet::Util::Windows::Process.process_privilege_symlink? Puppet.warning "The current user does not have the necessary permission to manage symlinks." end end end puppet/file_system/uniquefile.rb000064400000007763147634041260013123 0ustar00require 'puppet/file_system' require 'delegate' require 'tmpdir' # A class that provides `Tempfile`-like capabilities, but does not attempt to # manage the deletion of the file for you. API is identical to the # normal `Tempfile` class. # # @api public class Puppet::FileSystem::Uniquefile < DelegateClass(File) # Convenience method which ensures that the file is closed and # unlinked before returning # # @param identifier [String] additional part of generated pathname # @yieldparam file [File] the temporary file object # @return result of the passed block # @api private def self.open_tmp(identifier) f = new(identifier) yield f ensure if f f.close! end end def initialize(basename, *rest) create_tmpname(basename, *rest) do |tmpname, n, opts| mode = File::RDWR|File::CREAT|File::EXCL perm = 0600 if opts mode |= opts.delete(:mode) || 0 opts[:perm] = perm perm = nil else opts = perm end self.class.locking(tmpname) do @tmpfile = File.open(tmpname, mode, opts) @tmpname = tmpname end @mode = mode & ~(File::CREAT|File::EXCL) perm or opts.freeze @opts = opts end super(@tmpfile) end # Opens or reopens the file with mode "r+". def open @tmpfile.close if @tmpfile @tmpfile = File.open(@tmpname, @mode, @opts) __setobj__(@tmpfile) end def _close begin @tmpfile.close if @tmpfile ensure @tmpfile = nil end end protected :_close def close(unlink_now=false) if unlink_now close! else _close end end def close! _close unlink end def unlink return unless @tmpname begin File.unlink(@tmpname) rescue Errno::ENOENT rescue Errno::EACCES # may not be able to unlink on Windows; just ignore return end @tmpname = nil end alias delete unlink # Returns the full path name of the temporary file. # This will be nil if #unlink has been called. def path @tmpname end private def make_tmpname(prefix_suffix, n) case prefix_suffix when String prefix = prefix_suffix suffix = "" when Array prefix = prefix_suffix[0] suffix = prefix_suffix[1] else raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}" end t = Time.now.strftime("%Y%m%d") path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}" path << "-#{n}" if n path << suffix end def create_tmpname(basename, *rest) if opts = try_convert_to_hash(rest[-1]) opts = opts.dup if rest.pop.equal?(opts) max_try = opts.delete(:max_try) opts = [opts] else opts = [] end tmpdir, = *rest if $SAFE > 0 and tmpdir.tainted? tmpdir = '/tmp' else tmpdir ||= tmpdir() end n = nil begin path = File.expand_path(make_tmpname(basename, n), tmpdir) yield(path, n, *opts) rescue Errno::EEXIST n ||= 0 n += 1 retry if !max_try or n < max_try raise "cannot generate temporary name using `#{basename}' under `#{tmpdir}'" end path end def try_convert_to_hash(h) begin h.to_hash rescue NoMethodError => e nil end end @@systmpdir ||= defined?(Etc.systmpdir) ? Etc.systmpdir : '/tmp' def tmpdir tmp = '.' if $SAFE > 0 tmp = @@systmpdir else for dir in [ENV['TMPDIR'], ENV['TMP'], ENV['TEMP'], @@systmpdir, '/tmp'] if dir and stat = File.stat(dir) and stat.directory? and stat.writable? tmp = dir break end rescue nil end File.expand_path(tmp) end end class << self # yields with locking for +tmpname+ and returns the result of the # block. def locking(tmpname) lock = tmpname + '.lock' mkdir(lock) yield ensure rmdir(lock) if lock end def mkdir(*args) Dir.mkdir(*args) end def rmdir(*args) Dir.rmdir(*args) end end endpuppet/file_system/file_impl.rb000064400000005161147634041260012703 0ustar00# Abstract implementation of the Puppet::FileSystem # class Puppet::FileSystem::FileImpl def pathname(path) path.is_a?(Pathname) ? path : Pathname.new(path) end def assert_path(path) return path if path.is_a?(Pathname) # Some paths are string, or in the case of WatchedFile, it pretends to be # one by implementing to_str. if path.respond_to?(:to_str) Pathname.new(path) else raise ArgumentError, "FileSystem implementation expected Pathname, got: '#{path.class}'" end end def path_string(path) path.to_s end def open(path, mode, options, &block) ::File.open(path, options, mode, &block) end def dir(path) path.dirname end def basename(path) path.basename.to_s end def size(path) path.size end def exclusive_create(path, mode, &block) opt = File::CREAT | File::EXCL | File::WRONLY self.open(path, mode, opt, &block) end def exclusive_open(path, mode, options = 'r', timeout = 300, &block) wait = 0.001 + (Kernel.rand / 1000) written = false while !written ::File.open(path, options, mode) do |rf| if rf.flock(::File::LOCK_EX|::File::LOCK_NB) yield rf written = true else sleep wait timeout -= wait wait *= 2 if timeout < 0 raise Timeout::Error, "Timeout waiting for exclusive lock on #{@path}" end end end end end def each_line(path, &block) ::File.open(path) do |f| f.each_line do |line| yield line end end end def read(path) path.read end def read_preserve_line_endings(path) read(path) end def binread(path) raise NotImplementedError end def exist?(path) ::File.exist?(path) end def directory?(path) ::File.directory?(path) end def file?(path) ::File.file?(path) end def executable?(path) ::File.executable?(path) end def writable?(path) path.writable? end def touch(path) ::FileUtils.touch(path) end def mkpath(path) path.mkpath end def children(path) path.children end def symlink(path, dest, options = {}) FileUtils.symlink(path, dest, options) end def symlink?(path) File.symlink?(path) end def readlink(path) File.readlink(path) end def unlink(*paths) File.unlink(*paths) end def stat(path) File.stat(path) end def lstat(path) File.lstat(path) end def compare_stream(path, stream) open(path, 0, 'rb') { |this| FileUtils.compare_stream(this, stream) } end def chmod(mode, path) FileUtils.chmod(mode, path) end end puppet/file_system/file19.rb000064400000002201147634041260012024 0ustar00class Puppet::FileSystem::File19 < Puppet::FileSystem::FileImpl def binread(path) path.binread end # Provide an encoding agnostic version of compare_stream # # The FileUtils implementation in Ruby 2.0+ was modified in a manner where # it cannot properly compare File and StringIO instances. To sidestep that # issue this method reimplements the faster 2.0 version that will correctly # compare binary File and StringIO streams. def compare_stream(path, stream) open(path, 0, 'rb') do |this| bsize = stream_blksize(this, stream) sa = "".force_encoding('ASCII-8BIT') sb = "".force_encoding('ASCII-8BIT') begin this.read(bsize, sa) stream.read(bsize, sb) return true if sa.empty? && sb.empty? end while sa == sb false end end private def stream_blksize(*streams) streams.each do |s| next unless s.respond_to?(:stat) size = blksize(s.stat) return size if size end default_blksize() end def blksize(st) s = st.blksize return nil unless s return nil if s == 0 s end def default_blksize 1024 end end puppet/file_system/file18.rb000064400000000211147634041260012022 0ustar00class Puppet::FileSystem::File18 < Puppet::FileSystem::FileImpl def binread(path) ::File.open(path, 'rb') { |f| f.read } end end puppet/indirector/direct_file_server.rb000064400000001047147634041260014420 0ustar00require 'puppet/file_serving/terminus_helper' require 'puppet/indirector/terminus' class Puppet::Indirector::DirectFileServer < Puppet::Indirector::Terminus include Puppet::FileServing::TerminusHelper def find(request) return nil unless Puppet::FileSystem.exist?(request.key) instance = model.new(request.key) instance.links = request.options[:links] if request.options[:links] instance end def search(request) return nil unless Puppet::FileSystem.exist?(request.key) path2instances(request, request.key) end end puppet/indirector/instrumentation_data.rb000064400000000131147634041260015006 0ustar00# A stub class, so our constants work. class Puppet::Indirector::InstrumentationData end puppet/indirector/errors.rb000064400000000143147634041260012071 0ustar00require 'puppet/error' module Puppet::Indirector class ValidationError < Puppet::Error; end end puppet/indirector/hiera.rb000064400000002152147634041260011647 0ustar00require 'puppet/indirector/terminus' require 'hiera/scope' class Puppet::Indirector::Hiera < Puppet::Indirector::Terminus def initialize(*args) if ! Puppet.features.hiera? raise "Hiera terminus not supported without hiera library" end super end if defined?(::Psych::SyntaxError) DataBindingExceptions = [::StandardError, ::Psych::SyntaxError] else DataBindingExceptions = [::StandardError] end def find(request) hiera.lookup(request.key, nil, Hiera::Scope.new(request.options[:variables]), nil, nil) rescue *DataBindingExceptions => detail raise Puppet::DataBinding::LookupError.new(detail.message, detail) end private def self.hiera_config hiera_config = Puppet.settings[:hiera_config] config = {} if Puppet::FileSystem.exist?(hiera_config) config = Hiera::Config.load(hiera_config) else Puppet.warning "Config file #{hiera_config} not found, using Hiera defaults" end config[:logger] = 'puppet' config end def self.hiera @hiera ||= Hiera.new(:config => hiera_config) end def hiera self.class.hiera end end puppet/indirector/file_server.rb000064400000004023147634041260013063 0ustar00require 'puppet/file_serving/configuration' require 'puppet/file_serving/fileset' require 'puppet/file_serving/terminus_helper' require 'puppet/indirector/terminus' # Look files up using the file server. class Puppet::Indirector::FileServer < Puppet::Indirector::Terminus include Puppet::FileServing::TerminusHelper # Is the client authorized to perform this action? def authorized?(request) return false unless [:find, :search].include?(request.method) mount, file_path = configuration.split_path(request) # If we're not serving this mount, then access is denied. return false unless mount mount.allowed?(request.node, request.ip) end # Find our key using the fileserver. def find(request) mount, relative_path = configuration.split_path(request) return nil unless mount # The mount checks to see if the file exists, and returns nil # if not. return nil unless path = mount.find(relative_path, request) result = model.new(path) result.links = request.options[:links] if request.options[:links] result.collect(request.options[:source_permissions]) result end # Search for files. This returns an array rather than a single # file. def search(request) mount, relative_path = configuration.split_path(request) unless mount and paths = mount.search(relative_path, request) Puppet.info "Could not find filesystem info for file '#{request.key}' in environment #{request.environment}" return nil end filesets = paths.collect do |path| # Filesets support indirector requests as an options collection Puppet::FileServing::Fileset.new(path, request) end Puppet::FileServing::Fileset.merge(*filesets).collect do |file, base_path| inst = model.new(base_path, :relative_path => file) inst.links = request.options[:links] if request.options[:links] inst.collect inst end end private # Our fileserver configuration, if needed. def configuration Puppet::FileServing::Configuration.configuration end end puppet/indirector/queue.rb000064400000005433147634041260011710 0ustar00require 'puppet/indirector/terminus' require 'puppet/util/queue' require 'puppet/util' # Implements the :queue abstract indirector terminus type, for storing # model instances to a message queue, presumably for the purpose of out-of-process # handling of changes related to the model. # # Relies upon Puppet::Util::Queue for registry and client object management, # and specifies a default queue type of :stomp, appropriate for use with a variety of message brokers. # # It's up to the queue client type to instantiate itself correctly based on Puppet configuration information. # # A single queue client is maintained for the abstract terminus, meaning that you can only use one type # of queue client, one message broker solution, etc., with the indirection mechanism. # # Per-indirection queues are assumed, based on the indirection name. If the :catalog indirection makes # use of this :queue terminus, queue operations work against the "catalog" queue. It is up to the queue # client library to handle queue creation as necessary (for a number of popular queuing solutions, queue # creation is automatic and not a concern). class Puppet::Indirector::Queue < Puppet::Indirector::Terminus extend ::Puppet::Util::Queue include Puppet::Util def initialize(*args) super end # Queue has no idiomatic "find" def find(request) nil end # Place the request on the queue def save(request) result = nil benchmark :info, "Queued #{indirection.name} for #{request.key}" do result = client.publish_message(queue, request.instance.render(:pson)) end result rescue => detail msg = "Could not write #{request.key} to queue: #{detail}\nInstance::#{request.instance}\n client : #{client}" raise Puppet::Error, msg, detail.backtrace end def self.queue indirection_name end def queue self.class.queue end # Returns the singleton queue client object. def client self.class.client end # converts the _message_ from deserialized format to an actual model instance. def self.intern(message) result = nil benchmark :info, "Loaded queued #{indirection.name}" do result = model.convert_from(:pson, message) end result end # Provides queue subscription functionality; for a given indirection, use this method on the terminus # to subscribe to the indirection-specific queue. Your _block_ will be executed per new indirection # model received from the queue, with _obj_ being the model instance. def self.subscribe client.subscribe(queue) do |msg| begin yield(self.intern(msg)) rescue => detail Puppet.log_exception(detail, "Error occurred with subscription to queue #{queue} for indirection #{indirection_name}: #{detail}") end end end end puppet/indirector/catalog/queue.rb000064400000000521147634041260013313 0ustar00require 'puppet/resource/catalog' require 'puppet/indirector/queue' class Puppet::Resource::Catalog::Queue < Puppet::Indirector::Queue desc "Part of async storeconfigs, requiring the puppet queue daemon. ActiveRecord-based storeconfigs and inventory are deprecated. See http://links.puppetlabs.com/activerecord-deprecation" end puppet/indirector/catalog/active_record.rb000064400000002306147634041260015003 0ustar00require 'puppet/rails/host' require 'puppet/indirector/active_record' require 'puppet/resource/catalog' class Puppet::Resource::Catalog::ActiveRecord < Puppet::Indirector::ActiveRecord use_ar_model Puppet::Rails::Host desc "A component of ActiveRecord storeconfigs. ActiveRecord-based storeconfigs and inventory are deprecated. See http://links.puppetlabs.com/activerecord-deprecation" def initialize Puppet.deprecation_warning "ActiveRecord-based storeconfigs and inventory are deprecated. See http://links.puppetlabs.com/activerecord-deprecation" super end # We don't retrieve catalogs from storeconfigs def find(request) nil end # Save the values from a Facts instance as the facts on a Rails Host instance. def save(request) catalog = request.instance host = ar_model.find_by_name(catalog.name) || ar_model.create(:name => catalog.name) host.railsmark "Saved catalog to database" do host.merge_resources(catalog.vertices) host.last_compile = Time.now if node = Puppet::Node.indirection.find(catalog.name) host.ip = node.parameters["ipaddress"] host.environment = node.environment.to_s end host.save end end end puppet/indirector/catalog/static_compiler.rb000064400000020600147634041260015350 0ustar00require 'puppet/node' require 'puppet/resource/catalog' require 'puppet/indirector/catalog/compiler' class Puppet::Resource::Catalog::StaticCompiler < Puppet::Resource::Catalog::Compiler desc %q{Compiles catalogs on demand using the optional static compiler. This functions similarly to the normal compiler, but it replaces puppet:/// file URLs with explicit metadata and file content hashes, expecting puppet agent to fetch the exact specified content from the filebucket. This guarantees that a given catalog will always result in the same file states. It also decreases catalog application time and fileserver load, at the cost of increased compilation time. This terminus works today, but cannot be used without additional configuration. Specifically: * You must create a special filebucket resource --- with the title `puppet` and the `path` attribute set to `false` --- in site.pp or somewhere else where it will be added to every node's catalog. Using `puppet` as the title is mandatory; the static compiler treats this title as magical. filebucket { puppet: path => false, } * You must set `catalog_terminus = static_compiler` in the puppet master's puppet.conf. * The puppet master's auth.conf must allow authenticated nodes to access the `file_bucket_file` endpoint. This is enabled by default (see the `path /file` rule), but if you have made your auth.conf more restrictive, you may need to re-enable it.) * If you are using multiple puppet masters, you must configure load balancer affinity for agent nodes. This is because puppet masters other than the one that compiled a given catalog may not have stored the required file contents in their filebuckets.} def find(request) return nil unless catalog = super raise "Did not get catalog back" unless catalog.is_a?(model) catalog.resources.find_all { |res| res.type == "File" }.each do |resource| next unless source = resource[:source] next unless source =~ /^puppet:/ file = resource.to_ral if file.recurse? add_children(request.key, catalog, resource, file) else find_and_replace_metadata(request.key, resource, file) end end catalog end # Take a resource with a fileserver based file source remove the source # parameter, and insert the file metadata into the resource. # # This method acts to do the fileserver metadata retrieval in advance, while # the file source is local and doesn't require an HTTP request. It retrieves # the file metadata for a given file resource, removes the source parameter # from the resource, inserts the metadata into the file resource, and uploads # the file contents of the source to the file bucket. # # @param host [String] The host name of the node requesting this catalog # @param resource [Puppet::Resource] The resource to replace the metadata in # @param file [Puppet::Type::File] The file RAL associated with the resource def find_and_replace_metadata(host, resource, file) # We remove URL info from it, so it forces a local copy # rather than routing through the network. # Weird, but true. newsource = file[:source][0].sub("puppet:///", "") file[:source][0] = newsource raise "Could not get metadata for #{resource[:source]}" unless metadata = file.parameter(:source).metadata replace_metadata(host, resource, metadata) end # Rewrite a given file resource with the metadata from a fileserver based file # # This performs the actual metadata rewrite for the given file resource and # uploads the content of the source file to the filebucket. # # @param host [String] The host name of the node requesting this catalog # @param resource [Puppet::Resource] The resource to add the metadata to # @param metadata [Puppet::FileServing::Metadata] The metadata of the given fileserver based file def replace_metadata(host, resource, metadata) [:mode, :owner, :group].each do |param| resource[param] ||= metadata.send(param) end resource[:ensure] = metadata.ftype if metadata.ftype == "file" unless resource[:content] resource[:content] = metadata.checksum resource[:checksum] = metadata.checksum_type end end store_content(resource) if resource[:ensure] == "file" old_source = resource.delete(:source) Puppet.info "Metadata for #{resource} in catalog for '#{host}' added from '#{old_source}'" end # Generate children resources for a recursive file and add them to the catalog. # # @param host [String] The host name of the node requesting this catalog # @param catalog [Puppet::Resource::Catalog] # @param resource [Puppet::Resource] # @param file [Puppet::Type::File] The file RAL associated with the resource def add_children(host, catalog, resource, file) file = resource.to_ral children = get_child_resources(host, catalog, resource, file) remove_existing_resources(children, catalog) children.each do |name, res| catalog.add_resource res catalog.add_edge(resource, res) end end # Given a recursive file resource, recursively generate its children resources # # @param host [String] The host name of the node requesting this catalog # @param catalog [Puppet::Resource::Catalog] # @param resource [Puppet::Resource] # @param file [Puppet::Type::File] The file RAL associated with the resource # # @return [Array] The recursively generated File resources for the given resource def get_child_resources(host, catalog, resource, file) sourceselect = file[:sourceselect] children = {} source = resource[:source] # This is largely a copy of recurse_remote in File total = file[:source].collect do |source| next unless result = file.perform_recursion(source) return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory" result.each { |data| data.source = "#{source}/#{data.relative_path}" } break result if result and ! result.empty? and sourceselect == :first result end.flatten.compact # This only happens if we have sourceselect == :all unless sourceselect == :first found = [] total.reject! do |data| result = found.include?(data.relative_path) found << data.relative_path unless found.include?(data.relative_path) result end end total.each do |meta| # This is the top-level parent directory if meta.relative_path == "." replace_metadata(host, resource, meta) next end children[meta.relative_path] ||= Puppet::Resource.new(:file, File.join(file[:path], meta.relative_path)) # I think this is safe since it's a URL, not an actual file children[meta.relative_path][:source] = source + "/" + meta.relative_path resource.each do |param, value| # These should never be passed to our children. unless [:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].include? param children[meta.relative_path][param] = value end end replace_metadata(host, children[meta.relative_path], meta) end children end # Remove any file resources in the catalog that will be duplicated by the # given file resources. # # @param children [Array] # @param catalog [Puppet::Resource::Catalog] def remove_existing_resources(children, catalog) existing_names = catalog.resources.collect { |r| r.to_s } both = (existing_names & children.keys).inject({}) { |hash, name| hash[name] = true; hash } both.each { |name| children.delete(name) } end # Retrieve the source of a file resource using a fileserver based source and # upload it to the filebucket. # # @param resource [Puppet::Resource] def store_content(resource) @summer ||= Object.new @summer.extend(Puppet::Util::Checksums) type = @summer.sumtype(resource[:content]) sum = @summer.sumdata(resource[:content]) if Puppet::FileBucket::File.indirection.find("#{type}/#{sum}") Puppet.info "Content for '#{resource[:source]}' already exists" else Puppet.info "Storing content for source '#{resource[:source]}'" content = Puppet::FileServing::Content.indirection.find(resource[:source]) file = Puppet::FileBucket::File.new(content.content) Puppet::FileBucket::File.indirection.save(file) end end end puppet/indirector/catalog/store_configs.rb000064400000000374147634041260015041 0ustar00require 'puppet/indirector/store_configs' require 'puppet/resource/catalog' class Puppet::Resource::Catalog::StoreConfigs < Puppet::Indirector::StoreConfigs desc %q{Part of the "storeconfigs" feature. Should not be directly set by end users.} end puppet/indirector/catalog/json.rb000064400000000307147634041260013142 0ustar00require 'puppet/resource/catalog' require 'puppet/indirector/json' class Puppet::Resource::Catalog::Json < Puppet::Indirector::JSON desc "Store catalogs as flat files, serialized using JSON." end puppet/indirector/catalog/rest.rb000064400000000275147634041260013152 0ustar00require 'puppet/resource/catalog' require 'puppet/indirector/rest' class Puppet::Resource::Catalog::Rest < Puppet::Indirector::REST desc "Find resource catalogs over HTTP via REST." end puppet/indirector/catalog/yaml.rb000064400000001026147634041260013132 0ustar00require 'puppet/resource/catalog' require 'puppet/indirector/yaml' class Puppet::Resource::Catalog::Yaml < Puppet::Indirector::Yaml desc "Store catalogs as flat files, serialized using YAML." private # Override these, because yaml doesn't want to convert our self-referential # objects. This is hackish, but eh. def from_yaml(text) if config = YAML.load(text) return config end end def to_yaml(config) # We can't yaml-dump classes. #config.edgelist_class = nil YAML.dump(config) end end puppet/indirector/catalog/msgpack.rb000064400000000327147634041260013620 0ustar00require 'puppet/resource/catalog' require 'puppet/indirector/msgpack' class Puppet::Resource::Catalog::Msgpack < Puppet::Indirector::Msgpack desc "Store catalogs as flat files, serialized using MessagePack." end puppet/indirector/catalog/compiler.rb000064400000013133147634041260014004 0ustar00require 'puppet/node' require 'puppet/resource/catalog' require 'puppet/indirector/code' require 'puppet/util/profiler' require 'yaml' class Puppet::Resource::Catalog::Compiler < Puppet::Indirector::Code desc "Compiles catalogs on demand using Puppet's compiler." include Puppet::Util attr_accessor :code def extract_facts_from_request(request) return unless text_facts = request.options[:facts] unless format = request.options[:facts_format] raise ArgumentError, "Facts but no fact format provided for #{request.key}" end Puppet::Util::Profiler.profile("Found facts", [:compiler, :find_facts]) do # If the facts were encoded as yaml, then the param reconstitution system # in Network::HTTP::Handler will automagically deserialize the value. if text_facts.is_a?(Puppet::Node::Facts) facts = text_facts else # We unescape here because the corresponding code in Puppet::Configurer::FactHandler escapes facts = Puppet::Node::Facts.convert_from(format, CGI.unescape(text_facts)) end unless facts.name == request.key raise Puppet::Error, "Catalog for #{request.key.inspect} was requested with fact definition for the wrong node (#{facts.name.inspect})." end facts.add_timestamp options = { :environment => request.environment, :transaction_uuid => request.options[:transaction_uuid], } Puppet::Node::Facts.indirection.save(facts, nil, options) end end # Compile a node's catalog. def find(request) extract_facts_from_request(request) node = node_from_request(request) node.trusted_data = Puppet.lookup(:trusted_information) { Puppet::Context::TrustedInformation.local(node) }.to_h if catalog = compile(node) return catalog else # This shouldn't actually happen; we should either return # a config or raise an exception. return nil end end # filter-out a catalog to remove exported resources def filter(catalog) return catalog.filter { |r| r.virtual? } if catalog.respond_to?(:filter) catalog end def initialize Puppet::Util::Profiler.profile("Setup server facts for compiling", [:compiler, :init_server_facts]) do set_server_facts end end # Is our compiler part of a network, or are we just local? def networked? Puppet.run_mode.master? end private # Add any extra data necessary to the node. def add_node_data(node) # Merge in our server-side facts, so they can be used during compilation. node.merge(@server_facts) end # Compile the actual catalog. def compile(node) str = "Compiled catalog for #{node.name}" str += " in environment #{node.environment}" if node.environment config = nil benchmark(:notice, str) do Puppet::Util::Profiler.profile(str, [:compiler, :compile, node.environment, node.name]) do begin config = Puppet::Parser::Compiler.compile(node) rescue Puppet::Error => detail Puppet.err(detail.to_s) if networked? raise end end end config end # Turn our host name into a node object. def find_node(name, environment, transaction_uuid) Puppet::Util::Profiler.profile("Found node information", [:compiler, :find_node]) do node = nil begin node = Puppet::Node.indirection.find(name, :environment => environment, :transaction_uuid => transaction_uuid) rescue => detail message = "Failed when searching for node #{name}: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message, detail.backtrace end # Add any external data to the node. if node add_node_data(node) end node end end # Extract the node from the request, or use the request # to find the node. def node_from_request(request) if node = request.options[:use_node] if request.remote? raise Puppet::Error, "Invalid option use_node for a remote request" else return node end end # We rely on our authorization system to determine whether the connected # node is allowed to compile the catalog's node referenced by key. # By default the REST authorization system makes sure only the connected node # can compile his catalog. # This allows for instance monitoring systems or puppet-load to check several # node's catalog with only one certificate and a modification to auth.conf # If no key is provided we can only compile the currently connected node. name = request.key || request.node if node = find_node(name, request.environment, request.options[:transaction_uuid]) return node end raise ArgumentError, "Could not find node '#{name}'; cannot compile" end # Initialize our server fact hash; we add these to each client, and they # won't change while we're running, so it's safe to cache the values. def set_server_facts @server_facts = {} # Add our server version to the fact list @server_facts["serverversion"] = Puppet.version.to_s # And then add the server name and IP {"servername" => "fqdn", "serverip" => "ipaddress" }.each do |var, fact| if value = Facter.value(fact) @server_facts[var] = value else Puppet.warning "Could not retrieve fact #{fact}" end end if @server_facts["servername"].nil? host = Facter.value(:hostname) if domain = Facter.value(:domain) @server_facts["servername"] = [host, domain].join(".") else @server_facts["servername"] = host end end end end puppet/indirector/instrumentation_probe/local.rb000064400000001274147634041260016307 0ustar00require 'puppet/indirector/instrumentation_probe' require 'puppet/indirector/code' require 'puppet/util/instrumentation/indirection_probe' class Puppet::Indirector::InstrumentationProbe::Local < Puppet::Indirector::Code desc "Undocumented." def find(request) end def search(request) probes = [] Puppet::Util::Instrumentation::Instrumentable.each_probe do |probe| probes << Puppet::Util::Instrumentation::IndirectionProbe.new("#{probe.klass}.#{probe.method}") end probes end def save(request) Puppet::Util::Instrumentation::Instrumentable.enable_probes end def destroy(request) Puppet::Util::Instrumentation::Instrumentable.disable_probes end end puppet/indirector/instrumentation_probe/rest.rb000064400000000301147634041260016160 0ustar00require 'puppet/indirector/rest' require 'puppet/indirector/instrumentation_probe' class Puppet::Indirector::InstrumentationProbe::Rest < Puppet::Indirector::REST desc "Undocumented." end puppet/indirector/ssl_file.rb000064400000012772147634041260012370 0ustar00require 'puppet/ssl' class Puppet::Indirector::SslFile < Puppet::Indirector::Terminus # Specify the directory in which multiple files are stored. def self.store_in(setting) @directory_setting = setting end # Specify a single file location for storing just one file. # This is used for things like the CRL. def self.store_at(setting) @file_setting = setting end # Specify where a specific ca file should be stored. def self.store_ca_at(setting) @ca_setting = setting end class << self attr_reader :directory_setting, :file_setting, :ca_setting end # The full path to where we should store our files. def self.collection_directory return nil unless directory_setting Puppet.settings[directory_setting] end # The full path to an individual file we would be managing. def self.file_location return nil unless file_setting Puppet.settings[file_setting] end # The full path to a ca file we would be managing. def self.ca_location return nil unless ca_setting Puppet.settings[ca_setting] end # We assume that all files named 'ca' are pointing to individual ca files, # rather than normal host files. It's a bit hackish, but all the other # solutions seemed even more hackish. def ca?(name) name == Puppet::SSL::Host.ca_name end def initialize Puppet.settings.use(:main, :ssl) (collection_directory || file_location) or raise Puppet::DevError, "No file or directory setting provided; terminus #{self.class.name} cannot function" end def path(name) if name =~ Puppet::Indirector::BadNameRegexp then Puppet.crit("directory traversal detected in #{self.class}: #{name.inspect}") raise ArgumentError, "invalid key" end if ca?(name) and ca_location ca_location elsif collection_directory File.join(collection_directory, name.to_s + ".pem") else file_location end end # Remove our file. def destroy(request) path = Puppet::FileSystem.pathname(path(request.key)) return false unless Puppet::FileSystem.exist?(path) Puppet.notice "Removing file #{model} #{request.key} at '#{path}'" begin Puppet::FileSystem.unlink(path) rescue => detail raise Puppet::Error, "Could not remove #{request.key}: #{detail}", detail.backtrace end end # Find the file on disk, returning an instance of the model. def find(request) filename = rename_files_with_uppercase(path(request.key)) filename ? create_model(request.key, filename) : nil end # Save our file to disk. def save(request) path = path(request.key) dir = File.dirname(path) raise Puppet::Error.new("Cannot save #{request.key}; parent directory #{dir} does not exist") unless FileTest.directory?(dir) raise Puppet::Error.new("Cannot save #{request.key}; parent directory #{dir} is not writable") unless FileTest.writable?(dir) write(request.key, path) { |f| f.print request.instance.to_s } end # Search for more than one file. At this point, it just returns # an instance for every file in the directory. def search(request) dir = collection_directory Dir.entries(dir). select { |file| file =~ /\.pem$/ }. collect { |file| create_model(file.sub(/\.pem$/, ''), File.join(dir, file)) }. compact end private def create_model(name, path) result = model.new(name) result.read(path) result end # Demeterish pointers to class info. def collection_directory self.class.collection_directory end def file_location self.class.file_location end def ca_location self.class.ca_location end # A hack method to deal with files that exist with a different case. # Just renames it; doesn't read it in or anything. # LAK:NOTE This is a copy of the method in sslcertificates/support.rb, # which we'll be EOL'ing at some point. This method was added at 20080702 # and should be removed at some point. def rename_files_with_uppercase(file) return file if Puppet::FileSystem.exist?(file) dir, short = File.split(file) return nil unless Puppet::FileSystem.exist?(dir) raise ArgumentError, "Tried to fix SSL files to a file containing uppercase" unless short.downcase == short real_file = Dir.entries(dir).reject { |f| f =~ /^\./ }.find do |other| other.downcase == short end return nil unless real_file full_file = File.join(dir, real_file) Puppet.deprecation_warning "Automatic downcasing and renaming of ssl files is deprecated; please request the file using its correct case: #{full_file}" File.rename(full_file, file) file end # Yield a filehandle set up appropriately, either with our settings doing # the work or opening a filehandle manually. def write(name, path) if ca?(name) and ca_location Puppet.settings.setting(self.class.ca_setting).open('w') { |f| yield f } elsif file_location Puppet.settings.setting(self.class.file_setting).open('w') { |f| yield f } elsif setting = self.class.directory_setting begin Puppet.settings.setting(setting).open_file(path, 'w') { |f| yield f } rescue => detail raise Puppet::Error, "Could not write #{path} to #{setting}: #{detail}", detail.backtrace end else raise Puppet::DevError, "You must provide a setting to determine where the files are stored" end end end # LAK:NOTE This has to be at the end, because classes like SSL::Key use this # class, and this require statement loads those, which results in a load loop # and lots of failures. require 'puppet/ssl/host' puppet/indirector/code.rb000064400000000261147634041260011470 0ustar00require 'puppet/indirector/terminus' # Do nothing, requiring that the back-end terminus do all # of the work. class Puppet::Indirector::Code < Puppet::Indirector::Terminus end puppet/indirector/resource/ral.rb000064400000003604147634041260013167 0ustar00require 'puppet/indirector/resource/validator' class Puppet::Resource::Ral < Puppet::Indirector::Code include Puppet::Resource::Validator desc "Manipulate resources with the resource abstraction layer. Only used internally." def allow_remote_requests? Puppet.deprecation_warning("Accessing resources on the network is deprecated. See http://links.puppetlabs.com/deprecate-networked-resource") super end def find( request ) # find by name res = type(request).instances.find { |o| o.name == resource_name(request) } res ||= type(request).new(:name => resource_name(request), :audit => type(request).properties.collect { |s| s.name }) res.to_resource end def search( request ) conditions = request.options.dup conditions[:name] = resource_name(request) if resource_name(request) type(request).instances.map do |res| res.to_resource end.find_all do |res| conditions.all? {|property, value| res.to_resource[property].to_s == value.to_s} end.sort do |a,b| a.title <=> b.title end end def save( request ) # In RAL-land, to "save" means to actually try to change machine state res = request.instance ral_res = res.to_ral catalog = Puppet::Resource::Catalog.new(nil, request.environment) catalog.add_resource ral_res transaction = catalog.apply [ral_res.to_resource, transaction.report] end private # {type,resource}_name: the resource name may contain slashes: # File["/etc/hosts"]. To handle, assume the type name does # _not_ have any slashes in it, and split only on the first. def type_name( request ) request.key.split('/', 2)[0] end def resource_name( request ) name = request.key.split('/', 2)[1] name unless name == "" end def type( request ) Puppet::Type.type(type_name(request)) or raise Puppet::Error, "Could not find type #{type_name(request)}" end end puppet/indirector/resource/validator.rb000064400000000474147634041260014400 0ustar00module Puppet::Resource::Validator def validate_key(request) type, title = request.key.split('/', 2) unless type.downcase == request.instance.type.downcase and title == request.instance.title raise Puppet::Indirector::ValidationError, "Resource instance does not match request key" end end end puppet/indirector/resource/active_record.rb000064400000006171147634041260015224 0ustar00require 'puppet/indirector/active_record' require 'puppet/indirector/resource/validator' class Puppet::Resource::ActiveRecord < Puppet::Indirector::ActiveRecord include Puppet::Resource::Validator desc "A component of ActiveRecord storeconfigs. ActiveRecord-based storeconfigs and inventory are deprecated. See http://links.puppetlabs.com/activerecord-deprecation" def initialize Puppet.deprecation_warning "ActiveRecord-based storeconfigs and inventory are deprecated. See http://links.puppetlabs.com/activerecord-deprecation" super end def search(request) type = request_to_type_name(request) host = request.options[:host] filter = request.options[:filter] if filter and filter[1] =~ /^(and|or)$/i then raise Puppet::Error, "Complex search on StoreConfigs resources is not supported" end query = build_active_record_query(type, host, filter) Puppet::Rails::Resource.find(:all, query) end private def request_to_type_name(request) request.key.split('/', 2)[0] or raise "No key found in the request, failing: #{request.inspect}" end def filter_to_active_record(filter) # Don't call me if you don't have a filter, please. filter.is_a?(Array) or raise ArgumentError, "active record filters must be arrays" a, op, b = filter case op when /^(and|or)$/i then extra = [] first, args = filter_to_active_record a extra += args second, args = filter_to_active_record b extra += args return "(#{first}) #{op.upcase} (#{second})", extra when "==", "!=" then op = '=' if op == '==' # SQL, yayz! case a when "title" then return "title #{op} ?", [b] when "tag" then return "puppet_tags.name #{op} ?", [b] else return "param_names.name = ? AND param_values.value #{op} ?", [a, b] end else raise ArgumentError, "unknown operator #{op.inspect} in #{filter.inspect}" end end def build_active_record_query(type, host, filter) raise Puppet::DevError, "Cannot collect resources for a nil host" unless host search = "(exported=? AND restype=?)" arguments = [true, type] if filter then sql, values = filter_to_active_record(filter) search += " AND #{sql}" arguments += values end # note: we're not eagerly including any relations here because it can # create large numbers of objects that we will just throw out later. We # used to eagerly include param_names/values but the way the search filter # is built ruined those efforts and we were eagerly loading only the # searched parameter and not the other ones. query = {} case search when /puppet_tags/ query = { :joins => { :resource_tags => :puppet_tag } } when /param_name/ query = { :joins => { :param_values => :param_name } } end # We're going to collect objects from rails, but we don't want any # objects from this host. if host = Puppet::Rails::Host.find_by_name(host) search += " AND (host_id != ?)" arguments << host.id end query[:conditions] = [search, *arguments] query end end puppet/indirector/resource/store_configs.rb000064400000000744147634041260015257 0ustar00require 'puppet/indirector/store_configs' require 'puppet/indirector/resource/validator' class Puppet::Resource::StoreConfigs < Puppet::Indirector::StoreConfigs include Puppet::Resource::Validator desc %q{Part of the "storeconfigs" feature. Should not be directly set by end users.} def allow_remote_requests? Puppet.deprecation_warning("Accessing resources on the network is deprecated. See http://links.puppetlabs.com/deprecate-networked-resource") super end end puppet/indirector/resource/rest.rb000064400000001007147634041260013361 0ustar00require 'puppet/indirector/status' require 'puppet/indirector/rest' # @deprecated class Puppet::Resource::Rest < Puppet::Indirector::REST desc "Maniuplate resources remotely? Undocumented." private def deserialize_save(content_type, body) # Body is [ral_res.to_resource, transaction.report] format = Puppet::Network::FormatHandler.format_for(content_type) ary = format.intern(Array, body) [Puppet::Resource.from_data_hash(ary[0]), Puppet::Transaction::Report.from_data_hash(ary[1])] end end puppet/indirector/couch.rb000064400000003632147634041260011664 0ustar00class Puppet::Indirector::Couch < Puppet::Indirector::Terminus # The CouchRest database instance. One database instance per Puppet runtime # should be sufficient. # def self.db; @db ||= CouchRest.database! Puppet[:couchdb_url] end def db; self.class.db end def find(request) attributes_of get(request) end def initialize(*args) raise "Couch terminus not supported without couchrest gem" unless Puppet.features.couchdb? super end # Create or update the couchdb document with the request's data hash. # def save(request) raise ArgumentError, "PUT does not accept options" unless request.options.empty? update(request) || create(request) end private # RKH:TODO: Do not depend on error handling, check if the document exists # first. (Does couchrest support this?) # def get(request) db.get(id_for(request)) rescue RestClient::ResourceNotFound Puppet.debug "No couchdb document with id: #{id_for(request)}" return nil end def update(request) doc = get request return unless doc doc.merge!(hash_from(request)) doc.save true end def create(request) db.save_doc hash_from(request) end # The attributes hash that is serialized to CouchDB as JSON. It includes # metadata that is used to help aggregate data in couchdb. Add # model-specific attributes in subclasses. # def hash_from(request) { "_id" => id_for(request), "puppet_type" => document_type_for(request) } end # The couchdb response stripped of metadata, used to instantiate the model # instance that is returned by save. # def attributes_of(response) response && response.reject{|k,v| k =~ /^(_rev|puppet_)/ } end def document_type_for(request) request.indirection_name end # The id used to store the object in couchdb. Implemented in subclasses. # def id_for(request) raise NotImplementedError end end puppet/indirector/status/local.rb000064400000000403147634041260013171 0ustar00require 'puppet/indirector/status' class Puppet::Indirector::Status::Local < Puppet::Indirector::Code desc "Get status locally. Only used internally." def find( *anything ) status = model.new status.version= Puppet.version status end end puppet/indirector/status/rest.rb000064400000000415147634041260013057 0ustar00require 'puppet/indirector/status' require 'puppet/indirector/rest' class Puppet::Indirector::Status::Rest < Puppet::Indirector::REST desc "Get puppet master's status via REST. Useful because it tests the health of both the web server and the indirector." end puppet/indirector/exec.rb000064400000002233147634041260011503 0ustar00require 'puppet/indirector/terminus' require 'puppet/util' class Puppet::Indirector::Exec < Puppet::Indirector::Terminus # Look for external node definitions. def find(request) name = request.key external_command = command # Make sure it's an array raise Puppet::DevError, "Exec commands must be an array" unless external_command.is_a?(Array) # Make sure it's fully qualified. raise ArgumentError, "You must set the exec parameter to a fully qualified command" unless Puppet::Util.absolute_path?(external_command[0]) # Add our name to it. external_command << name begin output = execute(external_command, :failonfail => true, :combine => false) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Failed to find #{name} via exec: #{detail}", detail.backtrace end if output =~ /\A\s*\Z/ # all whitespace Puppet.debug "Empty response for #{name} from #{self.name} terminus" return nil else return output end end private # Proxy the execution, so it's easier to test. def execute(command, arguments) Puppet::Util::Execution.execute(command,arguments) end end puppet/indirector/certificate_revocation_list/disabled_ca.rb000064400000001330147634041260020534 0ustar00require 'puppet/indirector/code' require 'puppet/ssl/certificate_revocation_list' class Puppet::SSL::CertificateRevocationList::DisabledCa < Puppet::Indirector::Code desc "Manage SSL certificate revocation lists, but reject any remote access to the SSL data store. Used when a master has an explicitly disabled CA to prevent clients getting confusing 'success' behaviour." def initialize @file = Puppet::SSL::CertificateRevocationList.indirection.terminus(:file) end [:find, :head, :search, :save, :destroy].each do |name| define_method(name) do |request| if request.remote? raise Puppet::Error, "this master is not a CA" else @file.send(name, request) end end end end puppet/indirector/certificate_revocation_list/ca.rb000064400000000400147634041260016702 0ustar00require 'puppet/indirector/ssl_file' require 'puppet/ssl/certificate_revocation_list' class Puppet::SSL::CertificateRevocationList::Ca < Puppet::Indirector::SslFile desc "Manage the CA collection of certificate requests on disk." store_at :cacrl end puppet/indirector/certificate_revocation_list/file.rb000064400000000371147634041260017245 0ustar00require 'puppet/indirector/ssl_file' require 'puppet/ssl/certificate_revocation_list' class Puppet::SSL::CertificateRevocationList::File < Puppet::Indirector::SslFile desc "Manage the global certificate revocation list." store_at :hostcrl end puppet/indirector/certificate_revocation_list/rest.rb000064400000000503147634041260017300 0ustar00require 'puppet/ssl/certificate_revocation_list' require 'puppet/indirector/rest' class Puppet::SSL::CertificateRevocationList::Rest < Puppet::Indirector::REST desc "Find and save certificate revocation lists over HTTP via REST." use_server_setting(:ca_server) use_port_setting(:ca_port) use_srv_service(:ca) end puppet/indirector/file_metadata.rb000064400000000204147634041260013332 0ustar00# A stub class, so our constants work. class Puppet::Indirector::FileMetadata # :nodoc: end require 'puppet/file_serving/metadata' puppet/indirector/certificate/disabled_ca.rb000064400000001245147634041260015255 0ustar00require 'puppet/indirector/code' require 'puppet/ssl/certificate' class Puppet::SSL::Certificate::DisabledCa < Puppet::Indirector::Code desc "Manage SSL certificates on disk, but reject any remote access to the SSL data store. Used when a master has an explicitly disabled CA to prevent clients getting confusing 'success' behaviour." def initialize @file = Puppet::SSL::Certificate.indirection.terminus(:file) end [:find, :head, :search, :save, :destroy].each do |name| define_method(name) do |request| if request.remote? raise Puppet::Error, "this master is not a CA" else @file.send(name, request) end end end end puppet/indirector/certificate/ca.rb000064400000000377147634041260013433 0ustar00require 'puppet/indirector/ssl_file' require 'puppet/ssl/certificate' class Puppet::SSL::Certificate::Ca < Puppet::Indirector::SslFile desc "Manage the CA collection of signed SSL certificates on disk." store_in :signeddir store_ca_at :cacert end puppet/indirector/certificate/file.rb000064400000000350147634041260013756 0ustar00require 'puppet/indirector/ssl_file' require 'puppet/ssl/certificate' class Puppet::SSL::Certificate::File < Puppet::Indirector::SslFile desc "Manage SSL certificates on disk." store_in :certdir store_ca_at :localcacert end puppet/indirector/certificate/rest.rb000064400000000627147634041260014023 0ustar00require 'puppet/ssl/certificate' require 'puppet/indirector/rest' class Puppet::SSL::Certificate::Rest < Puppet::Indirector::REST desc "Find certificates over HTTP via REST." use_server_setting(:ca_server) use_port_setting(:ca_port) use_srv_service(:ca) def find(request) return nil unless result = super result.name = request.key unless result.name == request.key result end end puppet/indirector/active_record.rb000064400000001016147634041260013366 0ustar00require 'puppet/rails' require 'puppet/indirector' class Puppet::Indirector::ActiveRecord < Puppet::Indirector::Terminus class << self attr_accessor :ar_model end def self.use_ar_model(klass) self.ar_model = klass end def ar_model self.class.ar_model end def initialize Puppet::Rails.init end def find(request) return nil unless instance = ar_model.find_by_name(request.key) instance.to_puppet end def save(request) ar_model.from_puppet(request.instance).save end end puppet/indirector/certificate_request/disabled_ca.rb000064400000001302147634041260017017 0ustar00require 'puppet/indirector/code' require 'puppet/ssl/certificate_request' class Puppet::SSL::CertificateRequest::DisabledCa < Puppet::Indirector::Code desc "Manage SSL certificate requests on disk, but reject any remote access to the SSL data store. Used when a master has an explicitly disabled CA to prevent clients getting confusing 'success' behaviour." def initialize @file = Puppet::SSL::CertificateRequest.indirection.terminus(:file) end [:find, :head, :search, :save, :destroy].each do |name| define_method(name) do |request| if request.remote? raise Puppet::Error, "this master is not a CA" else @file.send(name, request) end end end end puppet/indirector/certificate_request/ca.rb000064400000001313147634041260015172 0ustar00require 'puppet/indirector/ssl_file' require 'puppet/ssl/certificate_request' class Puppet::SSL::CertificateRequest::Ca < Puppet::Indirector::SslFile desc "Manage the CA collection of certificate requests on disk." store_in :csrdir def save(request) if host = Puppet::SSL::Host.indirection.find(request.key) if Puppet[:allow_duplicate_certs] Puppet.notice "#{request.key} already has a #{host.state} certificate; new certificate will overwrite it" else raise "#{request.key} already has a #{host.state} certificate; ignoring certificate request" end end result = super Puppet.notice "#{request.key} has a waiting certificate request" result end end puppet/indirector/certificate_request/file.rb000064400000000365147634041260015534 0ustar00require 'puppet/indirector/ssl_file' require 'puppet/ssl/certificate_request' class Puppet::SSL::CertificateRequest::File < Puppet::Indirector::SslFile desc "Manage the collection of certificate requests on disk." store_in :requestdir end puppet/indirector/certificate_request/memory.rb000064400000000354147634041260016123 0ustar00require 'puppet/ssl/certificate_request' require 'puppet/indirector/memory' class Puppet::SSL::CertificateRequest::Memory < Puppet::Indirector::Memory desc "Store certificate requests in memory. This is used for testing puppet." end puppet/indirector/certificate_request/rest.rb000064400000000454147634041260015571 0ustar00require 'puppet/ssl/certificate_request' require 'puppet/indirector/rest' class Puppet::SSL::CertificateRequest::Rest < Puppet::Indirector::REST desc "Find and save certificate requests over HTTP via REST." use_server_setting(:ca_server) use_port_setting(:ca_port) use_srv_service(:ca) end puppet/indirector/instrumentation_listener.rb000064400000000135147634041260015726 0ustar00# A stub class, so our constants work. class Puppet::Indirector::InstrumentationListener end puppet/indirector/run/local.rb000064400000000451147634041260012455 0ustar00require 'puppet/run' require 'puppet/indirector/code' class Puppet::Run::Local < Puppet::Indirector::Code desc "Trigger a Puppet run locally. Only used internally." def save( request ) request.instance.run end def validate_key(request) # No key is necessary for kick end end puppet/indirector/run/rest.rb000064400000000374147634041260012344 0ustar00require 'puppet/run' require 'puppet/indirector/rest' class Puppet::Run::Rest < Puppet::Indirector::REST desc "Trigger Agent runs via REST." private def deserialize_save(content_type, body) model.convert_from(content_type, body) end end puppet/indirector/instrumentation_data/local.rb000064400000000773147634041260016114 0ustar00require 'puppet/indirector/instrumentation_data' class Puppet::Indirector::InstrumentationData::Local < Puppet::Indirector::Code desc "Undocumented." def find(request) model.new(request.key) end def search(request) raise Puppet::DevError, "You cannot search for instrumentation data" end def save(request) raise Puppet::DevError, "You cannot save instrumentation data" end def destroy(request) raise Puppet::DevError, "You cannot remove instrumentation data" end end puppet/indirector/instrumentation_data/rest.rb000064400000000277147634041260015776 0ustar00require 'puppet/indirector/rest' require 'puppet/indirector/instrumentation_data' class Puppet::Indirector::InstrumentationData::Rest < Puppet::Indirector::REST desc "Undocumented." end puppet/indirector/file_content.rb000064400000000202147634041260013222 0ustar00# A stub class, so our constants work. class Puppet::Indirector::FileContent # :nodoc: end require 'puppet/file_serving/content' puppet/indirector/store_configs.rb000064400000001117147634041260013423 0ustar00class Puppet::Indirector::StoreConfigs < Puppet::Indirector::Terminus def initialize super # This will raise if the indirection can't be found, so we can assume it # is always set to a valid instance from here on in. @target = indirection.terminus Puppet[:storeconfigs_backend] end attr_reader :target def head(request) target.head request end def find(request) target.find request end def search(request) target.search request end def save(request) target.save request end def destroy(request) target.save request end end puppet/indirector/node/write_only_yaml.rb000064400000002075147634041260014725 0ustar00require 'puppet/node' require 'puppet/indirector/yaml' # This is a WriteOnlyYaml terminus that exists only for the purpose of being able to write # node cache data that later can be read by the YAML terminus. # The use case this supports is to make it possible to search among the "current nodes" # when Puppet DB (recommended) or other central storage of information is not available. # # @see puppet issue 16753 # @see Puppet::Application::Master#setup_node_cache # @api private # class Puppet::Node::WriteOnlyYaml < Puppet::Indirector::Yaml desc "Store node information as flat files, serialized using YAML, does not deserialize (write only)." # Overridden to always return nil. This is a write only terminus. # @param [Object] request Ignored. # @return [nil] This implementation always return nil' # @api public def find(request) nil end # Overridden to always return nil. This is a write only terminus. # @param [Object] request Ignored. # @return [nil] This implementation always return nil # @api public def search(request) nil end end puppet/indirector/node/exec.rb000064400000003572147634041260012437 0ustar00require 'puppet/node' require 'puppet/indirector/exec' class Puppet::Node::Exec < Puppet::Indirector::Exec desc "Call an external program to get node information. See the [External Nodes](http://docs.puppetlabs.com/guides/external_nodes.html) page for more information." include Puppet::Util def command command = Puppet[:external_nodes] raise ArgumentError, "You must set the 'external_nodes' parameter to use the external node terminus" unless command != "none" command.split end # Look for external node definitions. def find(request) output = super or return nil # Translate the output to ruby. result = translate(request.key, output) # Set the requested environment if it wasn't overridden # If we don't do this it gets set to the local default result[:environment] ||= request.environment create_node(request.key, result) end private # Proxy the execution, so it's easier to test. def execute(command, arguments) Puppet::Util::Execution.execute(command,arguments) end # Turn our outputted objects into a Puppet::Node instance. def create_node(name, result) node = Puppet::Node.new(name) set = false [:parameters, :classes, :environment].each do |param| if value = result[param] node.send(param.to_s + "=", value) set = true end end node.fact_merge node end # Translate the yaml string into Ruby objects. def translate(name, output) YAML.load(output).inject({}) do |hash, data| case data[0] when String hash[data[0].intern] = data[1] when Symbol hash[data[0]] = data[1] else raise Puppet::Error, "key is a #{data[0].class}, not a string or symbol" end hash end rescue => detail raise Puppet::Error, "Could not load external node results for #{name}: #{detail}", detail.backtrace end end puppet/indirector/node/active_record.rb000064400000001245147634041260014317 0ustar00require 'puppet/rails/host' require 'puppet/indirector/active_record' require 'puppet/node' class Puppet::Node::ActiveRecord < Puppet::Indirector::ActiveRecord use_ar_model Puppet::Rails::Host desc "A component of ActiveRecord storeconfigs. ActiveRecord-based storeconfigs and inventory are deprecated. See http://links.puppetlabs.com/activerecord-deprecation" def initialize Puppet.deprecation_warning "ActiveRecord-based storeconfigs and inventory are deprecated. See http://links.puppetlabs.com/activerecord-deprecation" super end def find(request) node = super node.environment = request.environment node.fact_merge node end end puppet/indirector/node/store_configs.rb000064400000000343147634041260014350 0ustar00require 'puppet/indirector/store_configs' require 'puppet/node' class Puppet::Node::StoreConfigs < Puppet::Indirector::StoreConfigs desc %q{Part of the "storeconfigs" feature. Should not be directly set by end users.} end puppet/indirector/node/memory.rb000064400000000663147634041260013021 0ustar00require 'puppet/node' require 'puppet/indirector/memory' class Puppet::Node::Memory < Puppet::Indirector::Memory desc "Keep track of nodes in memory but nowhere else. This is used for one-time compiles, such as what the stand-alone `puppet` does. To use this terminus, you must load it with the data you want it to contain; it is only useful for developers and should generally not be chosen by a normal user." end puppet/indirector/node/plain.rb000064400000001175147634041260012613 0ustar00require 'puppet/node' require 'puppet/indirector/plain' class Puppet::Node::Plain < Puppet::Indirector::Plain desc "Always return an empty node object. Assumes you keep track of nodes in flat file manifests. You should use it when you don't have some other, functional source you want to use, as the compiler will not work without a valid node terminus. Note that class is responsible for merging the node's facts into the node instance before it is returned." # Just return an empty node. def find(request) node = super node.environment = request.environment node.fact_merge node end end puppet/indirector/node/rest.rb000064400000000341147634041260012457 0ustar00require 'puppet/node' require 'puppet/indirector/rest' class Puppet::Node::Rest < Puppet::Indirector::REST desc "Get a node via REST. Puppet agent uses this to allow the puppet master to override its environment." end puppet/indirector/node/yaml.rb000064400000001421147634041260012444 0ustar00require 'puppet/node' require 'puppet/indirector/yaml' class Puppet::Node::Yaml < Puppet::Indirector::Yaml desc "Store node information as flat files, serialized using YAML, or deserialize stored YAML nodes." protected def fix(object) # This looks very strange because when the object is read from disk the # environment is a string and by assigning it back onto the object it gets # converted to a Puppet::Node::Environment. # # The Puppet::Node class can't handle this itself because we are loading # with just straight YAML, which doesn't give the object a chance to modify # things as it is loaded. Instead YAML simply sets the instance variable # and leaves it at that. object.environment = object.environment object end end puppet/indirector/node/msgpack.rb000064400000000363147634041260013133 0ustar00require 'puppet/node' require 'puppet/indirector/msgpack' class Puppet::Node::Msgpack < Puppet::Indirector::Msgpack desc "Store node information as flat files, serialized using MessagePack, or deserialize stored MessagePack nodes." end puppet/indirector/node/ldap.rb000064400000016631147634041260012433 0ustar00require 'puppet/node' require 'puppet/indirector/ldap' class Puppet::Node::Ldap < Puppet::Indirector::Ldap desc "Search in LDAP for node configuration information. See the [LDAP Nodes](http://docs.puppetlabs.com/guides/ldap_nodes.html) page for more information. This will first search for whatever the certificate name is, then (if that name contains a `.`) for the short name, then `default`." # The attributes that Puppet class information is stored in. def class_attributes Puppet[:ldapclassattrs].split(/\s*,\s*/) end # Separate this out so it's relatively atomic. It's tempting to call # process instead of name2hash() here, but it ends up being # difficult to test because all exceptions get caught by ldapsearch. # LAK:NOTE Unfortunately, the ldap support is too stupid to throw anything # but LDAP::ResultError, even on bad connections, so we are rough-handed # with our error handling. def name2hash(name) info = nil ldapsearch(search_filter(name)) { |entry| info = entry2hash(entry) } info end # Look for our node in ldap. def find(request) names = [request.key] names << request.key.sub(/\..+/, '') if request.key.include?(".") # we assume it's an fqdn names << "default" node = nil names.each do |name| next unless info = name2hash(name) merge_parent(info) if info[:parent] info[:environment] ||= request.environment node = info2node(request.key, info) break end node end # Find more than one node. LAK:NOTE This is a bit of a clumsy API, because the 'search' # method currently *requires* a key. It seems appropriate in some cases but not others, # and I don't really know how to get rid of it as a requirement but allow it when desired. def search(request) if classes = request.options[:class] classes = [classes] unless classes.is_a?(Array) filter = "(&(objectclass=puppetClient)(puppetclass=" + classes.join(")(puppetclass=") + "))" else filter = "(objectclass=puppetClient)" end infos = [] ldapsearch(filter) { |entry| infos << entry2hash(entry, request.options[:fqdn]) } return infos.collect do |info| merge_parent(info) if info[:parent] info[:environment] ||= request.environment info2node(info[:name], info) end end # The parent attribute, if we have one. def parent_attribute if pattr = Puppet[:ldapparentattr] and ! pattr.empty? pattr else nil end end # The attributes that Puppet will stack as array over the full # hierarchy. def stacked_attributes(dummy_argument=:work_arround_for_ruby_GC_bug) Puppet[:ldapstackedattrs].split(/\s*,\s*/) end # Convert the found entry into a simple hash. def entry2hash(entry, fqdn = false) result = {} cn = entry.dn[ /cn\s*=\s*([^,\s]+)/i,1] dcs = entry.dn.scan(/dc\s*=\s*([^,\s]+)/i) result[:name] = fqdn ? ([cn]+dcs).join('.') : cn result[:parent] = get_parent_from_entry(entry) if parent_attribute result[:classes] = get_classes_from_entry(entry) result[:stacked] = get_stacked_values_from_entry(entry) result[:parameters] = get_parameters_from_entry(entry) result[:environment] = result[:parameters]["environment"] if result[:parameters]["environment"] result[:stacked_parameters] = {} if result[:stacked] result[:stacked].each do |value| param = value.split('=', 2) result[:stacked_parameters][param[0]] = param[1] end end if result[:stacked_parameters] result[:stacked_parameters].each do |param, value| result[:parameters][param] = value unless result[:parameters].include?(param) end end result[:parameters] = convert_parameters(result[:parameters]) result end # Default to all attributes. def search_attributes ldapattrs = Puppet[:ldapattrs] # results in everything getting returned return nil if ldapattrs == "all" search_attrs = class_attributes + ldapattrs.split(/\s*,\s*/) if pattr = parent_attribute search_attrs << pattr end search_attrs end # The ldap search filter to use. def search_filter(name) filter = Puppet[:ldapstring] if filter.include? "%s" # Don't replace the string in-line, since that would hard-code our node # info. filter = filter.gsub('%s', name) end filter end private # Add our hash of ldap information to the node instance. def add_to_node(node, information) node.classes = information[:classes].uniq unless information[:classes].nil? or information[:classes].empty? node.parameters = information[:parameters] unless information[:parameters].nil? or information[:parameters].empty? node.environment = information[:environment] if information[:environment] end def convert_parameters(parameters) result = {} parameters.each do |param, value| if value.is_a?(Array) result[param] = value.collect { |v| convert(v) } else result[param] = convert(value) end end result end # Convert any values if necessary. def convert(value) case value when Integer, Fixnum, Bignum; value when "true"; true when "false"; false else value end end # Find information for our parent and merge it into the current info. def find_and_merge_parent(parent, information) parent_info = name2hash(parent) || raise(Puppet::Error.new("Could not find parent node '#{parent}'")) information[:classes] += parent_info[:classes] parent_info[:parameters].each do |param, value| # Specifically test for whether it's set, so false values are handled correctly. information[:parameters][param] = value unless information[:parameters].include?(param) end information[:environment] ||= parent_info[:environment] parent_info[:parent] end # Take a name and a hash, and return a node instance. def info2node(name, info) node = Puppet::Node.new(name) add_to_node(node, info) node.fact_merge node end def merge_parent(info) parent = info[:parent] # Preload the parent array with the node name. parents = [info[:name]] while parent raise ArgumentError, "Found loop in LDAP node parents; #{parent} appears twice" if parents.include?(parent) parents << parent parent = find_and_merge_parent(parent, info) end info end def get_classes_from_entry(entry) result = class_attributes.inject([]) do |array, attr| if values = entry.vals(attr) values.each do |v| array << v end end array end result.uniq end def get_parameters_from_entry(entry) stacked_params = stacked_attributes entry.to_hash.inject({}) do |hash, ary| unless stacked_params.include?(ary[0]) # don't add our stacked parameters to the main param list if ary[1].length == 1 hash[ary[0]] = ary[1].shift else hash[ary[0]] = ary[1] end end hash end end def get_parent_from_entry(entry) pattr = parent_attribute return nil unless values = entry.vals(pattr) if values.length > 1 raise Puppet::Error, "Node entry #{entry.dn} specifies more than one parent: #{values.inspect}" end return(values.empty? ? nil : values.shift) end def get_stacked_values_from_entry(entry) stacked_attributes.inject([]) do |result, attr| if values = entry.vals(attr) result += values end result end end end puppet/indirector/none.rb000064400000000305147634041260011514 0ustar00require 'puppet/indirector/terminus' # A none terminus type, meant to always return nil class Puppet::Indirector::None < Puppet::Indirector::Terminus def find(request) return nil end end puppet/indirector/instrumentation_listener/local.rb000064400000001236147634041260017023 0ustar00require 'puppet/indirector/instrumentation_listener' class Puppet::Indirector::InstrumentationListener::Local < Puppet::Indirector::Code desc "Undocumented." def find(request) Puppet::Util::Instrumentation[request.key] end def search(request) Puppet::Util::Instrumentation.listeners end def save(request) res = request.instance Puppet::Util::Instrumentation[res.name] = res nil # don't leak the listener end def destroy(request) listener = Puppet::Util::Instrumentation[request.key] raise "Listener #{request.key} hasn't been subscribed" unless listener Puppet::Util::Instrumentation.unsubscribe(listener) end end puppet/indirector/instrumentation_listener/rest.rb000064400000000307147634041260016704 0ustar00require 'puppet/indirector/instrumentation_listener' require 'puppet/indirector/rest' class Puppet::Indirector::InstrumentationListener::Rest < Puppet::Indirector::REST desc "Undocumented." end puppet/indirector/key/disabled_ca.rb000064400000001206147634041260013560 0ustar00require 'puppet/indirector/code' require 'puppet/ssl/key' class Puppet::SSL::Key::DisabledCa < Puppet::Indirector::Code desc "Manage the CA private key, but reject any remote access to the SSL data store. Used when a master has an explicitly disabled CA to prevent clients getting confusing 'success' behaviour." def initialize @file = Puppet::SSL::Key.indirection.terminus(:file) end [:find, :head, :search, :save, :destroy].each do |name| define_method(name) do |request| if request.remote? raise Puppet::Error, "this master is not a CA" else @file.send(name, request) end end end end puppet/indirector/key/ca.rb000064400000000567147634041260011742 0ustar00require 'puppet/indirector/ssl_file' require 'puppet/ssl/key' class Puppet::SSL::Key::Ca < Puppet::Indirector::SslFile desc "Manage the CA's private on disk. This terminus *only* works with the CA key, because that's the only key that the CA ever interacts with." store_in :privatekeydir store_ca_at :cakey def allow_remote_requests? false end end puppet/indirector/key/file.rb000064400000002356147634041260012274 0ustar00require 'puppet/indirector/ssl_file' require 'puppet/ssl/key' class Puppet::SSL::Key::File < Puppet::Indirector::SslFile desc "Manage SSL private and public keys on disk." store_in :privatekeydir store_ca_at :cakey def allow_remote_requests? false end # Where should we store the public key? def public_key_path(name) if ca?(name) Puppet[:capub] else File.join(Puppet[:publickeydir], name.to_s + ".pem") end end # Remove the public key, in addition to the private key def destroy(request) super key_path = Puppet::FileSystem.pathname(public_key_path(request.key)) return unless Puppet::FileSystem.exist?(key_path) begin Puppet::FileSystem.unlink(key_path) rescue => detail raise Puppet::Error, "Could not remove #{request.key} public key: #{detail}", detail.backtrace end end # Save the public key, in addition to the private key. def save(request) super begin Puppet.settings.setting(:publickeydir).open_file(public_key_path(request.key), 'w') do |f| f.print request.instance.content.public_key.to_pem end rescue => detail raise Puppet::Error, "Could not write #{request.key}: #{detail}", detail.backtrace end end end puppet/indirector/key/memory.rb000064400000000275147634041260012663 0ustar00require 'puppet/ssl/key' require 'puppet/indirector/memory' class Puppet::SSL::Key::Memory < Puppet::Indirector::Memory desc "Store keys in memory. This is used for testing puppet." end puppet/indirector/facts/inventory_service.rb000064400000001134147634041260015433 0ustar00require 'puppet/node/facts' require 'puppet/indirector/rest' class Puppet::Node::Facts::InventoryService < Puppet::Indirector::REST desc "Find and save facts about nodes using a remote inventory service." use_server_setting(:inventory_server) use_port_setting(:inventory_port) # We don't want failing to upload to the inventory service to cause any # failures, so we just suppress them and warn. def save(request) begin super true rescue => e Puppet.warning "Could not upload facts for #{request.key} to inventory service: #{e.to_s}" false end end end puppet/indirector/facts/facter.rb000064400000004767147634041260013141 0ustar00require 'puppet/node/facts' require 'puppet/indirector/code' class Puppet::Node::Facts::Facter < Puppet::Indirector::Code desc "Retrieve facts from Facter. This provides a somewhat abstract interface between Puppet and Facter. It's only `somewhat` abstract because it always returns the local host's facts, regardless of what you attempt to find." def destroy(facts) raise Puppet::DevError, 'You cannot destroy facts in the code store; it is only used for getting facts from Facter' end def save(facts) raise Puppet::DevError, 'You cannot save facts to the code store; it is only used for getting facts from Facter' end # Lookup a host's facts up in Facter. def find(request) Facter.reset self.class.setup_external_search_paths(request) if Puppet.features.external_facts? self.class.setup_search_paths(request) result = Puppet::Node::Facts.new(request.key, Facter.to_hash) result.add_local_facts Puppet[:stringify_facts] ? result.stringify : result.sanitize result end private def self.setup_search_paths(request) # Add any per-module fact directories to facter's search path dirs = request.environment.modulepath.collect do |dir| ['lib', 'plugins'].map do |subdirectory| Dir.glob("#{dir}/*/#{subdirectory}/facter") end end.flatten + Puppet[:factpath].split(File::PATH_SEPARATOR) dirs = dirs.select do |dir| next false unless FileTest.directory?(dir) # Even through we no longer directly load facts in the terminus, # print out each .rb in the facts directory as module # developers may find that information useful for debugging purposes if Puppet::Util::Log.sendlevel?(:info) Puppet.info "Loading facts" Dir.glob("#{dir}/*.rb").each do |file| Puppet.debug "Loading facts from #{file}" end end true end Facter.search *dirs end def self.setup_external_search_paths(request) # Add any per-module external fact directories to facter's external search path dirs = [] request.environment.modules.each do |m| if m.has_external_facts? dir = m.plugin_fact_directory Puppet.debug "Loading external facts from #{dir}" dirs << dir end end # Add system external fact directory if it exists if FileTest.directory?(Puppet[:pluginfactdest]) dir = Puppet[:pluginfactdest] Puppet.debug "Loading external facts from #{dir}" dirs << dir end Facter.search_external dirs end end puppet/indirector/facts/couch.rb000064400000001676147634041260012772 0ustar00require 'puppet/node/facts' require 'puppet/indirector/couch' class Puppet::Node::Facts::Couch < Puppet::Indirector::Couch desc "DEPRECATED. This terminus will be removed in Puppet 4.0. Store facts in CouchDB. This should not be used with the inventory service; it is for more obscure custom integrations. If you are wondering whether you should use it, you shouldn't; use PuppetDB instead." # Return the facts object or nil if there is no document def find(request) doc = super doc ? model.new(doc['_id'], doc['facts']) : nil end private # Facts values are stored to the document's 'facts' attribute. Hostname is # stored to 'name' # def hash_from(request) super.merge('facts' => request.instance.values) end # Facts are stored to the 'node' document. def document_type_for(request) 'node' end # The id used to store the object in couchdb. def id_for(request) request.key.to_s end end puppet/indirector/facts/inventory_active_record.rb000064400000010156147634041260016610 0ustar00require 'puppet/rails' require 'puppet/rails/inventory_node' require 'puppet/rails/inventory_fact' require 'puppet/indirector/active_record' require 'puppet/util/retryaction' class Puppet::Node::Facts::InventoryActiveRecord < Puppet::Indirector::ActiveRecord desc "Medium-performance fact storage suitable for the inventory service. Most users should use PuppetDB instead. Note: ActiveRecord-based storeconfigs and inventory are deprecated. See http://links.puppetlabs.com/activerecord-deprecation" def initialize raise Puppet::Error, "ActiveRecords-based inventory is unsupported with Ruby 2 and Rails 3.0" if RUBY_VERSION[0] == '2' Puppet.deprecation_warning "ActiveRecord-based storeconfigs and inventory are deprecated. See http://links.puppetlabs.com/activerecord-deprecation" super end def find(request) node = Puppet::Rails::InventoryNode.find_by_name(request.key) return nil unless node facts = Puppet::Node::Facts.new(node.name, node.facts_to_hash) facts.timestamp = node.timestamp facts end def save(request) Puppet::Util::RetryAction.retry_action :retries => 4, :retry_exceptions => {ActiveRecord::StatementInvalid => 'MySQL Error. Retrying'} do facts = request.instance node = Puppet::Rails::InventoryNode.find_by_name(request.key) || Puppet::Rails::InventoryNode.create(:name => request.key, :timestamp => facts.timestamp) node.timestamp = facts.timestamp ActiveRecord::Base.transaction do Puppet::Rails::InventoryFact.delete_all(:node_id => node.id) # We don't want to save internal values as facts, because those are # metadata that belong on the node facts.values.each do |name,value| next if name.to_s =~ /^_/ node.facts.build(:name => name, :value => value) end node.save end end end def search(request) return [] unless request.options matching_nodes = [] fact_filters = Hash.new {|h,k| h[k] = []} meta_filters = Hash.new {|h,k| h[k] = []} request.options.each do |key,value| type, name, operator = key.to_s.split(".") operator ||= "eq" if type == "facts" fact_filters[operator] << [name,value] elsif type == "meta" and name == "timestamp" meta_filters[operator] << [name,value] end end matching_nodes = nodes_matching_fact_filters(fact_filters) + nodes_matching_meta_filters(meta_filters) # to_a because [].inject == nil matching_nodes.inject {|nodes,this_set| nodes & this_set}.to_a.sort end private def nodes_matching_fact_filters(fact_filters) node_sets = [] fact_filters['eq'].each do |name,value| node_sets << Puppet::Rails::InventoryNode.has_fact_with_value(name,value).map {|node| node.name} end fact_filters['ne'].each do |name,value| node_sets << Puppet::Rails::InventoryNode.has_fact_without_value(name,value).map {|node| node.name} end { 'gt' => '>', 'lt' => '<', 'ge' => '>=', 'le' => '<=' }.each do |operator_name,operator| fact_filters[operator_name].each do |name,value| facts = Puppet::Rails::InventoryFact.find_by_sql(["SELECT inventory_facts.value, inventory_nodes.name AS node_name FROM inventory_facts INNER JOIN inventory_nodes ON inventory_facts.node_id = inventory_nodes.id WHERE inventory_facts.name = ?", name]) node_sets << facts.select {|fact| fact.value.to_f.send(operator, value.to_f)}.map {|fact| fact.node_name} end end node_sets end def nodes_matching_meta_filters(meta_filters) node_sets = [] { 'eq' => '=', 'ne' => '!=', 'gt' => '>', 'lt' => '<', 'ge' => '>=', 'le' => '<=' }.each do |operator_name,operator| meta_filters[operator_name].each do |name,value| node_sets << Puppet::Rails::InventoryNode.find(:all, :select => "name", :conditions => ["timestamp #{operator} ?", value]).map {|node| node.name} end end node_sets end end puppet/indirector/facts/active_record.rb000064400000002642147634041260014474 0ustar00require 'puppet/rails/fact_name' require 'puppet/rails/fact_value' require 'puppet/rails/host' require 'puppet/indirector/active_record' class Puppet::Node::Facts::ActiveRecord < Puppet::Indirector::ActiveRecord use_ar_model Puppet::Rails::Host desc "A component of ActiveRecord storeconfigs and inventory. ActiveRecord-based storeconfigs and inventory are deprecated. See http://links.puppetlabs.com/activerecord-deprecation" def initialize Puppet.deprecation_warning "ActiveRecord-based storeconfigs and inventory are deprecated. See http://links.puppetlabs.com/activerecord-deprecation" super end # Find the Rails host and pull its facts as a Facts instance. def find(request) return nil unless host = ar_model.find_by_name(request.key, :include => {:fact_values => :fact_name}) facts = Puppet::Node::Facts.new(host.name) facts.values = host.get_facts_hash.inject({}) do |hash, ary| # Convert all single-member arrays into plain values. param = ary[0] values = ary[1].collect { |v| v.value } values = values[0] if values.length == 1 hash[param] = values hash end facts end # Save the values from a Facts instance as the facts on a Rails Host instance. def save(request) facts = request.instance host = ar_model.find_by_name(facts.name) || ar_model.create(:name => facts.name) host.merge_facts(facts.values) host.save end end puppet/indirector/facts/store_configs.rb000064400000000360147634041260014522 0ustar00require 'puppet/node/facts' require 'puppet/indirector/store_configs' class Puppet::Node::Facts::StoreConfigs < Puppet::Indirector::StoreConfigs desc %q{Part of the "storeconfigs" feature. Should not be directly set by end users.} end puppet/indirector/facts/memory.rb000064400000000546147634041260013174 0ustar00require 'puppet/node/facts' require 'puppet/indirector/memory' class Puppet::Node::Facts::Memory < Puppet::Indirector::Memory desc "Keep track of facts in memory but nowhere else. This is used for one-time compiles, such as what the stand-alone `puppet` does. To use this terminus, you must load it with the data you want it to contain." end puppet/indirector/facts/rest.rb000064400000000406147634041260012634 0ustar00require 'puppet/node/facts' require 'puppet/indirector/rest' class Puppet::Node::Facts::Rest < Puppet::Indirector::REST desc "Find and save facts about nodes over HTTP via REST." use_server_setting(:inventory_server) use_port_setting(:inventory_port) end puppet/indirector/facts/yaml.rb000064400000003567147634041260012634 0ustar00require 'puppet/node/facts' require 'puppet/indirector/yaml' class Puppet::Node::Facts::Yaml < Puppet::Indirector::Yaml desc "Store client facts as flat files, serialized using YAML, or return deserialized facts from disk." def search(request) node_names = [] Dir.glob(yaml_dir_path).each do |file| facts = YAML.load_file(file) node_names << facts.name if node_matches?(facts, request.options) end node_names end private # Return the path to a given node's file. def yaml_dir_path base = Puppet.run_mode.master? ? Puppet[:yamldir] : Puppet[:clientyamldir] File.join(base, 'facts', '*.yaml') end def node_matches?(facts, options) options.each do |key, value| type, name, operator = key.to_s.split(".") operator ||= 'eq' return false unless node_matches_option?(type, name, operator, value, facts) end return true end def node_matches_option?(type, name, operator, value, facts) case type when "meta" case name when "timestamp" compare_timestamp(operator, facts.timestamp, Time.parse(value)) end when "facts" compare_facts(operator, facts.values[name], value) end end def compare_facts(operator, value1, value2) return false unless value1 case operator when "eq" value1.to_s == value2.to_s when "le" value1.to_f <= value2.to_f when "ge" value1.to_f >= value2.to_f when "lt" value1.to_f < value2.to_f when "gt" value1.to_f > value2.to_f when "ne" value1.to_s != value2.to_s end end def compare_timestamp(operator, value1, value2) case operator when "eq" value1 == value2 when "le" value1 <= value2 when "ge" value1 >= value2 when "lt" value1 < value2 when "gt" value1 > value2 when "ne" value1 != value2 end end end puppet/indirector/facts/network_device.rb000064400000001404147634041260014666 0ustar00require 'puppet/node/facts' require 'puppet/indirector/code' class Puppet::Node::Facts::NetworkDevice < Puppet::Indirector::Code desc "Retrieve facts from a network device." # Look a device's facts up through the current device. def find(request) result = Puppet::Node::Facts.new(request.key, Puppet::Util::NetworkDevice.current.facts) result.add_local_facts Puppet[:stringify_facts] ? result.stringify : result.sanitize result end def destroy(facts) raise Puppet::DevError, "You cannot destroy facts in the code store; it is only used for getting facts from a remote device" end def save(facts) raise Puppet::DevError, "You cannot save facts to the code store; it is only used for getting facts from a remote device" end end puppet/indirector/report/processor.rb000064400000003113147634041260014107 0ustar00require 'puppet/transaction/report' require 'puppet/indirector/code' require 'puppet/reports' class Puppet::Transaction::Report::Processor < Puppet::Indirector::Code desc "Puppet's report processor. Processes the report with each of the report types listed in the 'reports' setting." def initialize Puppet.settings.use(:main, :reporting, :metrics) end def save(request) process(request.instance) end def destroy(request) processors do |mod| mod.destroy(request.key) if mod.respond_to?(:destroy) end end private # Process the report with each of the configured report types. # LAK:NOTE This isn't necessarily the best design, but it's backward # compatible and that's good enough for now. def process(report) Puppet.debug "Received report to process from #{report.host}" processors do |mod| Puppet.debug "Processing report from #{report.host} with processor #{mod}" # We have to use a dup because we're including a module in the # report. newrep = report.dup begin newrep.extend(mod) newrep.process rescue => detail Puppet.log_exception(detail, "Report #{name} failed: #{detail}") end end end # Handle the parsing of the reports attribute. def reports Puppet[:reports].gsub(/(^\s+)|(\s+$)/, '').split(/\s*,\s*/) end def processors(&blk) return [] if Puppet[:reports] == "none" reports.each do |name| if mod = Puppet::Reports.report(name) yield(mod) else Puppet.warning "No report named '#{name}'" end end end end puppet/indirector/report/rest.rb000064400000000632147634041260013050 0ustar00require 'puppet/indirector/rest' class Puppet::Transaction::Report::Rest < Puppet::Indirector::REST desc "Get server report over HTTP via REST." use_server_setting(:report_server) use_port_setting(:report_port) use_srv_service(:report) private def deserialize_save(content_type, body) format = Puppet::Network::FormatHandler.format_for(content_type) format.intern(Array, body) end end puppet/indirector/report/yaml.rb000064400000000461147634041260013035 0ustar00require 'puppet/transaction/report' require 'puppet/indirector/yaml' class Puppet::Transaction::Report::Yaml < Puppet::Indirector::Yaml desc "Store last report as a flat file, serialized using YAML." # Force report to be saved there def path(name,ext='.yaml') Puppet[:lastrunreport] end end puppet/indirector/report/msgpack.rb000064400000000504147634041260013516 0ustar00require 'puppet/transaction/report' require 'puppet/indirector/msgpack' class Puppet::Transaction::Report::Msgpack < Puppet::Indirector::Msgpack desc "Store last report as a flat file, serialized using MessagePack." # Force report to be saved there def path(name,ext='.msgpack') Puppet[:lastrunreport] end end puppet/indirector/file_content/file_server.rb000064400000000412147634041260015532 0ustar00require 'puppet/file_serving/content' require 'puppet/indirector/file_content' require 'puppet/indirector/file_server' class Puppet::Indirector::FileContent::FileServer < Puppet::Indirector::FileServer desc "Retrieve file contents using Puppet's fileserver." end puppet/indirector/file_content/selector.rb000064400000001332147634041260015047 0ustar00require 'puppet/file_serving/content' require 'puppet/indirector/file_content' require 'puppet/indirector/code' require 'puppet/file_serving/terminus_selector' class Puppet::Indirector::FileContent::Selector < Puppet::Indirector::Code desc "Select the terminus based on the request" include Puppet::FileServing::TerminusSelector def get_terminus(request) indirection.terminus(select(request)) end def find(request) get_terminus(request).find(request) end def search(request) get_terminus(request).search(request) end def authorized?(request) terminus = get_terminus(request) if terminus.respond_to?(:authorized?) terminus.authorized?(request) else true end end end puppet/indirector/file_content/file.rb000064400000000401147634041260014142 0ustar00require 'puppet/file_serving/content' require 'puppet/indirector/file_content' require 'puppet/indirector/direct_file_server' class Puppet::Indirector::FileContent::File < Puppet::Indirector::DirectFileServer desc "Retrieve file contents from disk." end puppet/indirector/file_content/rest.rb000064400000000427147634041260014210 0ustar00require 'puppet/file_serving/content' require 'puppet/indirector/file_content' require 'puppet/indirector/rest' class Puppet::Indirector::FileContent::Rest < Puppet::Indirector::REST desc "Retrieve file contents via a REST HTTP interface." use_srv_service(:fileserver) end puppet/indirector/json.rb000064400000004253147634041260011534 0ustar00require 'puppet/indirector/terminus' require 'puppet/util' # The base class for JSON indirection terminus implementations. # # This should generally be preferred to the YAML base for any future # implementations, since it is ~ three times faster despite being pure Ruby # rather than a C implementation. class Puppet::Indirector::JSON < Puppet::Indirector::Terminus def find(request) load_json_from_file(path(request.key), request.key) end def save(request) filename = path(request.key) FileUtils.mkdir_p(File.dirname(filename)) Puppet::Util.replace_file(filename, 0660) {|f| f.print to_json(request.instance) } rescue TypeError => detail Puppet.log_exception "Could not save #{self.name} #{request.key}: #{detail}" end def destroy(request) Puppet::FileSystem.unlink(path(request.key)) rescue => detail unless detail.is_a? Errno::ENOENT raise Puppet::Error, "Could not destroy #{self.name} #{request.key}: #{detail}", detail.backtrace end 1 # emulate success... end def search(request) Dir.glob(path(request.key)).collect do |file| load_json_from_file(file, request.key) end end # Return the path to a given node's file. def path(name, ext = '.json') if name =~ Puppet::Indirector::BadNameRegexp then Puppet.crit("directory traversal detected in #{self.class}: #{name.inspect}") raise ArgumentError, "invalid key" end base = Puppet.run_mode.master? ? Puppet[:server_datadir] : Puppet[:client_datadir] File.join(base, self.class.indirection_name.to_s, name.to_s + ext) end private def load_json_from_file(file, key) json = nil begin json = File.read(file) rescue Errno::ENOENT return nil rescue => detail raise Puppet::Error, "Could not read JSON data for #{indirection.name} #{key}: #{detail}", detail.backtrace end begin return from_json(json) rescue => detail raise Puppet::Error, "Could not parse JSON data for #{indirection.name} #{key}: #{detail}", detail.backtrace end end def from_json(text) model.convert_from('pson', text) end def to_json(object) object.render('pson') end end puppet/indirector/memory.rb000064400000001342147634041260012067 0ustar00require 'puppet/indirector/terminus' # Manage a memory-cached list of instances. class Puppet::Indirector::Memory < Puppet::Indirector::Terminus def initialize clear end def clear @instances = {} end def destroy(request) raise ArgumentError.new("Could not find #{request.key} to destroy") unless @instances.include?(request.key) @instances.delete(request.key) end def find(request) @instances[request.key] end def search(request) found_keys = @instances.keys.find_all { |key| key.include?(request.key) } found_keys.collect { |key| @instances[key] } end def head(request) not find(request).nil? end def save(request) @instances[request.key] = request.instance end end puppet/indirector/plain.rb000064400000000401147634041260011655 0ustar00require 'puppet/indirector/terminus' # An empty terminus type, meant to just return empty objects. class Puppet::Indirector::Plain < Puppet::Indirector::Terminus # Just return nothing. def find(request) indirection.model.new(request.key) end end puppet/indirector/envelope.rb000064400000000355147634041260012377 0ustar00require 'puppet/indirector' # Provide any attributes or functionality needed for indirected # instances. module Puppet::Indirector::Envelope attr_accessor :expiration def expired? expiration and expiration < Time.now end end puppet/indirector/rest.rb000064400000020055147634041260011536 0ustar00require 'net/http' require 'uri' require 'puppet/network/http' require 'puppet/network/http_pool' require 'puppet/network/http/api/v1' require 'puppet/network/http/compression' # Access objects via REST class Puppet::Indirector::REST < Puppet::Indirector::Terminus include Puppet::Network::HTTP::Compression.module class << self attr_reader :server_setting, :port_setting end # Specify the setting that we should use to get the server name. def self.use_server_setting(setting) @server_setting = setting end # Specify the setting that we should use to get the port. def self.use_port_setting(setting) @port_setting = setting end # Specify the service to use when doing SRV record lookup def self.use_srv_service(service) @srv_service = service end def self.srv_service @srv_service || :puppet end def self.server Puppet.settings[server_setting || :server] end def self.port Puppet.settings[port_setting || :masterport].to_i end # Provide appropriate headers. def headers add_accept_encoding({"Accept" => model.supported_formats.join(", ")}) end def add_profiling_header(headers) if (Puppet[:profile]) headers[Puppet::Network::HTTP::HEADER_ENABLE_PROFILING] = "true" end headers end def network(request) Puppet::Network::HttpPool.http_instance(request.server || self.class.server, request.port || self.class.port) end def http_get(request, path, headers = nil, *args) http_request(:get, request, path, add_profiling_header(headers), *args) end def http_post(request, path, data, headers = nil, *args) http_request(:post, request, path, data, add_profiling_header(headers), *args) end def http_head(request, path, headers = nil, *args) http_request(:head, request, path, add_profiling_header(headers), *args) end def http_delete(request, path, headers = nil, *args) http_request(:delete, request, path, add_profiling_header(headers), *args) end def http_put(request, path, data, headers = nil, *args) http_request(:put, request, path, data, add_profiling_header(headers), *args) end def http_request(method, request, *args) conn = network(request) conn.send(method, *args) end def find(request) uri, body = Puppet::Network::HTTP::API::V1.request_to_uri_and_body(request) uri_with_query_string = "#{uri}?#{body}" response = do_request(request) do |request| # WEBrick in Ruby 1.9.1 only supports up to 1024 character lines in an HTTP request # http://redmine.ruby-lang.org/issues/show/3991 if "GET #{uri_with_query_string} HTTP/1.1\r\n".length > 1024 http_post(request, uri, body, headers) else http_get(request, uri_with_query_string, headers) end end if is_http_200?(response) check_master_version(response) content_type, body = parse_response(response) result = deserialize_find(content_type, body) result.name = request.key if result.respond_to?(:name=) result elsif is_http_404?(response) return nil unless request.options[:fail_on_404] # 404 can get special treatment as the indirector API can not produce a meaningful # reason to why something is not found - it may not be the thing the user is # expecting to find that is missing, but something else (like the environment). # While this way of handling the issue is not perfect, there is at least an error # that makes a user aware of the reason for the failure. # content_type, body = parse_response(response) msg = "Find #{elide(uri_with_query_string, 100)} resulted in 404 with the message: #{body}" raise Puppet::Error, msg else nil end end def head(request) response = do_request(request) do |request| http_head(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), headers) end if is_http_200?(response) check_master_version(response) true else false end end def search(request) response = do_request(request) do |request| http_get(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), headers) end if is_http_200?(response) check_master_version(response) content_type, body = parse_response(response) deserialize_search(content_type, body) || [] else [] end end def destroy(request) raise ArgumentError, "DELETE does not accept options" unless request.options.empty? response = do_request(request) do |request| http_delete(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), headers) end if is_http_200?(response) check_master_version(response) content_type, body = parse_response(response) deserialize_destroy(content_type, body) else nil end end def save(request) raise ArgumentError, "PUT does not accept options" unless request.options.empty? response = do_request(request) do |request| http_put(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), request.instance.render, headers.merge({ "Content-Type" => request.instance.mime })) end if is_http_200?(response) check_master_version(response) content_type, body = parse_response(response) deserialize_save(content_type, body) else nil end end # Encapsulate call to request.do_request with the arguments from this class # Then yield to the code block that was called in # We certainly could have retained the full request.do_request(...) { |r| ... } # but this makes the code much cleaner and we only then actually make the call # to request.do_request from here, thus if we change what we pass or how we # get it, we only need to change it here. def do_request(request) request.do_request(self.class.srv_service, self.class.server, self.class.port) { |request| yield(request) } end def validate_key(request) # Validation happens on the remote end end private def is_http_200?(response) case response.code when "404" false when /^2/ true else # Raise the http error if we didn't get a 'success' of some kind. raise convert_to_http_error(response) end end def is_http_404?(response) response.code == "404" end def convert_to_http_error(response) message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}" Net::HTTPError.new(message, response) end def check_master_version response if !response[Puppet::Network::HTTP::HEADER_PUPPET_VERSION] && (Puppet[:legacy_query_parameter_serialization] == false || Puppet[:report_serialization_format] != "yaml") Puppet.notice "Using less secure serialization of reports and query parameters for compatibility" Puppet.notice "with older puppet master. To remove this notice, please upgrade your master(s) " Puppet.notice "to Puppet 3.3 or newer." Puppet.notice "See http://links.puppetlabs.com/deprecate_yaml_on_network for more information." Puppet[:legacy_query_parameter_serialization] = true Puppet[:report_serialization_format] = "yaml" end end # Returns the content_type, stripping any appended charset, and the # body, decompressed if necessary (content-encoding is checked inside # uncompress_body) def parse_response(response) if response['content-type'] [ response['content-type'].gsub(/\s*;.*$/,''), body = uncompress_body(response) ] else raise "No content type in http response; cannot parse" end end def deserialize_find(content_type, body) model.convert_from(content_type, body) end def deserialize_search(content_type, body) model.convert_from_multiple(content_type, body) end def deserialize_destroy(content_type, body) model.convert_from(content_type, body) end def deserialize_save(content_type, body) nil end def elide(string, length) if Puppet::Util::Log.level == :debug || string.length <= length string else string[0, length - 3] + "..." end end end puppet/indirector/certificate_status.rb000064400000000115147634041260014441 0ustar00require 'puppet/indirector' class Puppet::Indirector::CertificateStatus end puppet/indirector/resource_type.rb000064400000000174147634041260013451 0ustar00require 'puppet/resource/type' # A stub class, so our constants work. class Puppet::Indirector::ResourceType # :nodoc: end puppet/indirector/face.rb000064400000011105147634041260011453 0ustar00require 'puppet/face' class Puppet::Indirector::Face < Puppet::Face option "--terminus TERMINUS" do summary "The indirector terminus to use." description <<-EOT Indirector faces expose indirected subsystems of Puppet. These subsystems are each able to retrieve and alter a specific type of data (with the familiar actions of `find`, `search`, `save`, and `destroy`) from an arbitrary number of pluggable backends. In Puppet parlance, these backends are called terminuses. Almost all indirected subsystems have a `rest` terminus that interacts with the puppet master's data. Most of them have additional terminuses for various local data models, which are in turn used by the indirected subsystem on the puppet master whenever it receives a remote request. The terminus for an action is often determined by context, but occasionally needs to be set explicitly. See the "Notes" section of this face's manpage for more details. EOT before_action do |action, args, options| set_terminus(options[:terminus]) end after_action do |action, args, options| indirection.reset_terminus_class end end def self.indirections Puppet::Indirector::Indirection.instances.collect { |t| t.to_s }.sort end def self.terminus_classes(indirection) Puppet::Indirector::Terminus.terminus_classes(indirection.to_sym).collect { |t| t.to_s }.sort end def call_indirection_method(method, key, options) begin result = indirection.__send__(method, key, options) rescue => detail message = "Could not call '#{method}' on '#{indirection_name}': #{detail}" Puppet.log_exception(detail, message) raise RuntimeError, message, detail.backtrace end return result end option "--extra HASH" do summary "Extra arguments to pass to the indirection request" description <<-EOT A terminus can take additional arguments to refine the operation, which are passed as an arbitrary hash to the back-end. Anything passed as the extra value is just send direct to the back-end. EOT default_to do Hash.new end end action :destroy do summary "Delete an object." arguments "" when_invoked {|key, options| call_indirection_method :destroy, key, options[:extra] } end action :find do summary "Retrieve an object by name." arguments "" when_invoked {|key, options| call_indirection_method :find, key, options[:extra] } end action :save do summary "API only: create or overwrite an object." arguments "" description <<-EOT API only: create or overwrite an object. As the Faces framework does not currently accept data from STDIN, save actions cannot currently be invoked from the command line. EOT when_invoked {|key, options| call_indirection_method :save, key, options[:extra] } end action :search do summary "Search for an object or retrieve multiple objects." arguments "" when_invoked {|key, options| call_indirection_method :search, key, options[:extra] } end # Print the configuration for the current terminus class action :info do summary "Print the default terminus class for this face." description <<-EOT Prints the default terminus class for this subcommand. Note that different run modes may have different default termini; when in doubt, specify the run mode with the '--run_mode' option. EOT when_invoked do |options| if t = indirection.terminus_class "Run mode '#{Puppet.run_mode.name}': #{t}" else "No default terminus class for run mode '#{Puppet.run_mode.name}'" end end end attr_accessor :from def indirection_name @indirection_name || name.to_sym end # Here's your opportunity to override the indirection name. By default it # will be the same name as the face. def set_indirection_name(name) @indirection_name = name end # Return an indirection associated with a face, if one exists; # One usually does. def indirection unless @indirection @indirection = Puppet::Indirector::Indirection.instance(indirection_name) @indirection or raise "Could not find terminus for #{indirection_name}" end @indirection end def set_terminus(from) begin indirection.terminus_class = from rescue => detail msg = "Could not set '#{indirection.name}' terminus to '#{from}' (#{detail}); valid terminus types are #{self.class.terminus_classes(indirection.name).join(", ") }" raise detail, msg, detail.backtrace end end end puppet/indirector/yaml.rb000064400000003624147634041260011526 0ustar00require 'puppet/indirector/terminus' require 'puppet/util/yaml' # The base class for YAML indirection termini. class Puppet::Indirector::Yaml < Puppet::Indirector::Terminus # Read a given name's file in and convert it from YAML. def find(request) file = path(request.key) return nil unless Puppet::FileSystem.exist?(file) begin return fix(Puppet::Util::Yaml.load_file(file)) rescue Puppet::Util::Yaml::YamlLoadError => detail raise Puppet::Error, "Could not parse YAML data for #{indirection.name} #{request.key}: #{detail}", detail.backtrace end end # Convert our object to YAML and store it to the disk. def save(request) raise ArgumentError.new("You can only save objects that respond to :name") unless request.instance.respond_to?(:name) file = path(request.key) basedir = File.dirname(file) # This is quite likely a bad idea, since we're not managing ownership or modes. Dir.mkdir(basedir) unless Puppet::FileSystem.exist?(basedir) begin Puppet::Util::Yaml.dump(request.instance, file) rescue TypeError => detail Puppet.err "Could not save #{self.name} #{request.key}: #{detail}" end end # Return the path to a given node's file. def path(name,ext='.yaml') if name =~ Puppet::Indirector::BadNameRegexp then Puppet.crit("directory traversal detected in #{self.class}: #{name.inspect}") raise ArgumentError, "invalid key" end base = Puppet.run_mode.master? ? Puppet[:yamldir] : Puppet[:clientyamldir] File.join(base, self.class.indirection_name.to_s, name.to_s + ext) end def destroy(request) file_path = path(request.key) Puppet::FileSystem.unlink(file_path) if Puppet::FileSystem.exist?(file_path) end def search(request) Dir.glob(path(request.key,'')).collect do |file| fix(Puppet::Util::Yaml.load_file(file)) end end protected def fix(object) object end end puppet/indirector/request.rb000064400000021154147634041260012252 0ustar00require 'cgi' require 'uri' require 'puppet/indirector' require 'puppet/util/pson' require 'puppet/network/resolver' # This class encapsulates all of the information you need to make an # Indirection call, and as a result also handles REST calls. It's somewhat # analogous to an HTTP Request object, except tuned for our Indirector. class Puppet::Indirector::Request attr_accessor :key, :method, :options, :instance, :node, :ip, :authenticated, :ignore_cache, :ignore_terminus attr_accessor :server, :port, :uri, :protocol attr_reader :indirection_name # trusted_information is specifically left out because we can't serialize it # and keep it "trusted" OPTION_ATTRIBUTES = [:ip, :node, :authenticated, :ignore_terminus, :ignore_cache, :instance, :environment] ::PSON.register_document_type('IndirectorRequest',self) def self.from_data_hash(data) raise ArgumentError, "No indirection name provided in data" unless indirection_name = data['type'] raise ArgumentError, "No method name provided in data" unless method = data['method'] raise ArgumentError, "No key provided in data" unless key = data['key'] request = new(indirection_name, method, key, nil, data['attributes']) if instance = data['instance'] klass = Puppet::Indirector::Indirection.instance(request.indirection_name).model if instance.is_a?(klass) request.instance = instance else request.instance = klass.from_data_hash(instance) end end request end def self.from_pson(json) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(json) end def to_data_hash result = { 'type' => indirection_name, 'method' => method, 'key' => key } attributes = {} OPTION_ATTRIBUTES.each do |key| next unless value = send(key) attributes[key] = value end options.each do |opt, value| attributes[opt] = value end result['attributes'] = attributes unless attributes.empty? result['instance'] = instance if instance result end def to_pson_data_hash { 'document_type' => 'IndirectorRequest', 'data' => to_data_hash, } end def to_pson(*args) to_pson_data_hash.to_pson(*args) end # Is this an authenticated request? def authenticated? # Double negative, so we just get true or false ! ! authenticated end def environment # If environment has not been set directly, we should use the application's # current environment @environment ||= Puppet.lookup(:current_environment) end def environment=(env) @environment = if env.is_a?(Puppet::Node::Environment) env elsif (current_environment = Puppet.lookup(:current_environment)).name == env current_environment else Puppet.lookup(:environments).get!(env) end end def escaped_key URI.escape(key) end # LAK:NOTE This is a messy interface to the cache, and it's only # used by the Configurer class. I decided it was better to implement # it now and refactor later, when we have a better design, than # to spend another month coming up with a design now that might # not be any better. def ignore_cache? ignore_cache end def ignore_terminus? ignore_terminus end def initialize(indirection_name, method, key, instance, options = {}) @instance = instance options ||= {} self.indirection_name = indirection_name self.method = method options = options.inject({}) { |hash, ary| hash[ary[0].to_sym] = ary[1]; hash } set_attributes(options) @options = options if key # If the request key is a URI, then we need to treat it specially, # because it rewrites the key. We could otherwise strip server/port/etc # info out in the REST class, but it seemed bad design for the REST # class to rewrite the key. if key.to_s =~ /^\w+:\// and not Puppet::Util.absolute_path?(key.to_s) # it's a URI set_uri_key(key) else @key = key end end @key = @instance.name if ! @key and @instance end # Look up the indirection based on the name provided. def indirection Puppet::Indirector::Indirection.instance(indirection_name) end def indirection_name=(name) @indirection_name = name.to_sym end def model raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless i = indirection i.model end # Are we trying to interact with multiple resources, or just one? def plural? method == :search end # Create the query string, if options are present. def query_string return "" if options.nil? || options.empty? # For backward compatibility with older (pre-3.3) masters, # this puppet option allows serialization of query parameter # arrays as yaml. This can be removed when we remove yaml # support entirely. if Puppet.settings[:legacy_query_parameter_serialization] replace_arrays_with_yaml end "?" + encode_params(expand_into_parameters(options.to_a)) end def replace_arrays_with_yaml options.each do |key, value| case value when Array options[key] = YAML.dump(value) end end end def expand_into_parameters(data) data.inject([]) do |params, key_value| key, value = key_value expanded_value = case value when Array value.collect { |val| [key, val] } else [key_value] end params.concat(expand_primitive_types_into_parameters(expanded_value)) end end def expand_primitive_types_into_parameters(data) data.inject([]) do |params, key_value| key, value = key_value case value when nil params when true, false, String, Symbol, Fixnum, Bignum, Float params << [key, value] else raise ArgumentError, "HTTP REST queries cannot handle values of type '#{value.class}'" end end end def encode_params(params) params.collect do |key, value| "#{key}=#{CGI.escape(value.to_s)}" end.join("&") end def to_hash result = options.dup OPTION_ATTRIBUTES.each do |attribute| if value = send(attribute) result[attribute] = value end end result end def to_s return(uri ? uri : "/#{indirection_name}/#{key}") end def do_request(srv_service=:puppet, default_server=Puppet.settings[:server], default_port=Puppet.settings[:masterport], &block) # We were given a specific server to use, so just use that one. # This happens if someone does something like specifying a file # source using a puppet:// URI with a specific server. return yield(self) if !self.server.nil? if Puppet.settings[:use_srv_records] Puppet::Network::Resolver.each_srv_record(Puppet.settings[:srv_domain], srv_service) do |srv_server, srv_port| begin self.server = srv_server self.port = srv_port return yield(self) rescue SystemCallError => e Puppet.warning "Error connecting to #{srv_server}:#{srv_port}: #{e.message}" end end end # ... Fall back onto the default server. Puppet.debug "No more servers left, falling back to #{default_server}:#{default_port}" if Puppet.settings[:use_srv_records] self.server = default_server self.port = default_port return yield(self) end def remote? self.node or self.ip end private def set_attributes(options) OPTION_ATTRIBUTES.each do |attribute| if options.include?(attribute.to_sym) send(attribute.to_s + "=", options[attribute]) options.delete(attribute) end end end # Parse the key as a URI, setting attributes appropriately. def set_uri_key(key) @uri = key begin uri = URI.parse(URI.escape(key)) rescue => detail raise ArgumentError, "Could not understand URL #{key}: #{detail}", detail.backtrace end # Just short-circuit these to full paths if uri.scheme == "file" @key = Puppet::Util.uri_to_path(uri) return end @server = uri.host if uri.host # If the URI class can look up the scheme, it will provide a port, # otherwise it will default to '0'. if uri.port.to_i == 0 and uri.scheme == "puppet" @port = Puppet.settings[:masterport].to_i else @port = uri.port.to_i end @protocol = uri.scheme if uri.scheme == 'puppet' @key = URI.unescape(uri.path.sub(/^\//, '')) return end env, indirector, @key = URI.unescape(uri.path.sub(/^\//, '')).split('/',3) @key ||= '' self.environment = env unless env == '' end end puppet/indirector/file_metadata/file_server.rb000064400000000415147634041260015643 0ustar00require 'puppet/file_serving/metadata' require 'puppet/indirector/file_metadata' require 'puppet/indirector/file_server' class Puppet::Indirector::FileMetadata::FileServer < Puppet::Indirector::FileServer desc "Retrieve file metadata using Puppet's fileserver." end puppet/indirector/file_metadata/selector.rb000064400000001335147634041260015160 0ustar00require 'puppet/file_serving/metadata' require 'puppet/indirector/file_metadata' require 'puppet/indirector/code' require 'puppet/file_serving/terminus_selector' class Puppet::Indirector::FileMetadata::Selector < Puppet::Indirector::Code desc "Select the terminus based on the request" include Puppet::FileServing::TerminusSelector def get_terminus(request) indirection.terminus(select(request)) end def find(request) get_terminus(request).find(request) end def search(request) get_terminus(request).search(request) end def authorized?(request) terminus = get_terminus(request) if terminus.respond_to?(:authorized?) terminus.authorized?(request) else true end end end puppet/indirector/file_metadata/file.rb000064400000001023147634041260014251 0ustar00require 'puppet/file_serving/metadata' require 'puppet/indirector/file_metadata' require 'puppet/indirector/direct_file_server' class Puppet::Indirector::FileMetadata::File < Puppet::Indirector::DirectFileServer desc "Retrieve file metadata directly from the local filesystem." def find(request) return unless data = super data.collect(request.options[:source_permissions]) data end def search(request) return unless result = super result.each { |instance| instance.collect } result end end puppet/indirector/file_metadata/rest.rb000064400000000432147634041260014312 0ustar00require 'puppet/file_serving/metadata' require 'puppet/indirector/file_metadata' require 'puppet/indirector/rest' class Puppet::Indirector::FileMetadata::Rest < Puppet::Indirector::REST desc "Retrieve file metadata via a REST HTTP interface." use_srv_service(:fileserver) end puppet/indirector/msgpack.rb000064400000004511147634041260012205 0ustar00require 'puppet/indirector/terminus' require 'puppet/util' # The base class for MessagePack indirection terminus implementations. # # This should generally be preferred to the PSON base for any future # implementations, since it is ~ 30 times faster class Puppet::Indirector::Msgpack < Puppet::Indirector::Terminus def initialize(*args) if ! Puppet.features.msgpack? raise "MessagePack terminus not supported without msgpack library" end super end def find(request) load_msgpack_from_file(path(request.key), request.key) end def save(request) filename = path(request.key) FileUtils.mkdir_p(File.dirname(filename)) Puppet::Util.replace_file(filename, 0660) {|f| f.print to_msgpack(request.instance) } rescue TypeError => detail Puppet.log_exception "Could not save #{self.name} #{request.key}: #{detail}" end def destroy(request) Puppet::FileSystem.unlink(path(request.key)) rescue => detail unless detail.is_a? Errno::ENOENT raise Puppet::Error, "Could not destroy #{self.name} #{request.key}: #{detail}", detail.backtrace end 1 # emulate success... end def search(request) Dir.glob(path(request.key)).collect do |file| load_msgpack_from_file(file, request.key) end end # Return the path to a given node's file. def path(name, ext = '.msgpack') if name =~ Puppet::Indirector::BadNameRegexp then Puppet.crit("directory traversal detected in #{self.class}: #{name.inspect}") raise ArgumentError, "invalid key" end base = Puppet.run_mode.master? ? Puppet[:server_datadir] : Puppet[:client_datadir] File.join(base, self.class.indirection_name.to_s, name.to_s + ext) end private def load_msgpack_from_file(file, key) msgpack = nil begin msgpack = File.read(file) rescue Errno::ENOENT return nil rescue => detail raise Puppet::Error, "Could not read MessagePack data for #{indirection.name} #{key}: #{detail}", detail.backtrace end begin return from_msgpack(msgpack) rescue => detail raise Puppet::Error, "Could not parse MessagePack data for #{indirection.name} #{key}: #{detail}", detail.backtrace end end def from_msgpack(text) model.convert_from('msgpack', text) end def to_msgpack(object) object.render('msgpack') end end puppet/indirector/certificate_status/file.rb000064400000005553147634041260015373 0ustar00require 'puppet' require 'puppet/indirector/certificate_status' require 'puppet/ssl/certificate' require 'puppet/ssl/certificate_authority' require 'puppet/ssl/certificate_request' require 'puppet/ssl/host' require 'puppet/ssl/key' class Puppet::Indirector::CertificateStatus::File < Puppet::Indirector::Code desc "Manipulate certificate status on the local filesystem. Only functional on the CA." def ca raise ArgumentError, "This process is not configured as a certificate authority" unless Puppet::SSL::CertificateAuthority.ca? Puppet::SSL::CertificateAuthority.new end def destroy(request) deleted = [] [ Puppet::SSL::Certificate, Puppet::SSL::CertificateRequest, Puppet::SSL::Key, ].collect do |part| if part.indirection.destroy(request.key) deleted << "#{part}" end end return "Nothing was deleted" if deleted.empty? "Deleted for #{request.key}: #{deleted.join(", ")}" end def save(request) if request.instance.desired_state == "signed" certificate_request = Puppet::SSL::CertificateRequest.indirection.find(request.key) raise Puppet::Error, "Cannot sign for host #{request.key} without a certificate request" unless certificate_request ca.sign(request.key) elsif request.instance.desired_state == "revoked" certificate = Puppet::SSL::Certificate.indirection.find(request.key) raise Puppet::Error, "Cannot revoke host #{request.key} because has it doesn't have a signed certificate" unless certificate ca.revoke(request.key) else raise Puppet::Error, "State #{request.instance.desired_state} invalid; Must specify desired state of 'signed' or 'revoked' for host #{request.key}" end end def search(request) # Support historic interface wherein users provide classes to filter # the search. When used via the REST API, the arguments must be # a Symbol or an Array containing Symbol objects. klasses = case request.options[:for] when Class [request.options[:for]] when nil [ Puppet::SSL::Certificate, Puppet::SSL::CertificateRequest, Puppet::SSL::Key, ] else [request.options[:for]].flatten.map do |klassname| indirection.class.model(klassname.to_sym) end end klasses.collect do |klass| klass.indirection.search(request.key, request.options) end.flatten.collect do |result| result.name end.uniq.collect &Puppet::SSL::Host.method(:new) end def find(request) ssl_host = Puppet::SSL::Host.new(request.key) public_key = Puppet::SSL::Certificate.indirection.find(request.key) if ssl_host.certificate_request || public_key ssl_host else nil end end def validate_key(request) # We only use desired_state from the instance and use request.key # otherwise, so the name does not need to match end end puppet/indirector/certificate_status/rest.rb000064400000000555147634041260015426 0ustar00require 'puppet/ssl/host' require 'puppet/indirector/rest' require 'puppet/indirector/certificate_status' class Puppet::Indirector::CertificateStatus::Rest < Puppet::Indirector::REST desc "Sign, revoke, search for, or clean certificates & certificate requests over HTTP." use_server_setting(:ca_server) use_port_setting(:ca_port) use_srv_service(:ca) end puppet/indirector/instrumentation_probe.rb000064400000000132147634041260015205 0ustar00# A stub class, so our constants work. class Puppet::Indirector::InstrumentationProbe end puppet/indirector/ldap.rb000064400000004236147634041260011504 0ustar00require 'puppet/indirector/terminus' require 'puppet/util/ldap/connection' class Puppet::Indirector::Ldap < Puppet::Indirector::Terminus # Perform our ldap search and process the result. def find(request) ldapsearch(search_filter(request.key)) { |entry| return process(entry) } || nil end # Process the found entry. We assume that we don't just want the # ldap object. def process(entry) raise Puppet::DevError, "The 'process' method has not been overridden for the LDAP terminus for #{self.name}" end # Default to all attributes. def search_attributes nil end def search_base Puppet[:ldapbase] end # The ldap search filter to use. def search_filter(name) raise Puppet::DevError, "No search string set for LDAP terminus for #{self.name}" end # Find the ldap node, return the class list and parent node specially, # and everything else in a parameter hash. def ldapsearch(filter) raise ArgumentError.new("You must pass a block to ldapsearch") unless block_given? found = false count = 0 begin connection.search(search_base, 2, filter, search_attributes) do |entry| found = true yield entry end rescue SystemExit,NoMemoryError raise rescue Exception => detail if count == 0 # Try reconnecting to ldap if we get an exception and we haven't yet retried. count += 1 @connection = nil Puppet.warning "Retrying LDAP connection" retry else error = Puppet::Error.new("LDAP Search failed") error.set_backtrace(detail.backtrace) raise error end end found end # Create an ldap connection. def connection unless @connection raise Puppet::Error, "Could not set up LDAP Connection: Missing ruby/ldap libraries" unless Puppet.features.ldap? begin conn = Puppet::Util::Ldap::Connection.instance conn.start @connection = conn.connection rescue => detail message = "Could not connect to LDAP: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message, detail.backtrace end end @connection end end puppet/indirector/file_bucket_file/selector.rb000064400000002002147634041260015644 0ustar00require 'puppet/indirector/code' module Puppet::FileBucketFile class Selector < Puppet::Indirector::Code desc "Select the terminus based on the request" def select(request) if request.protocol == 'https' :rest else :file end end def get_terminus(request) indirection.terminus(select(request)) end def head(request) get_terminus(request).head(request) end def find(request) get_terminus(request).find(request) end def save(request) get_terminus(request).save(request) end def search(request) get_terminus(request).search(request) end def destroy(request) get_terminus(request).destroy(request) end def authorized?(request) terminus = get_terminus(request) if terminus.respond_to?(:authorized?) terminus.authorized?(request) else true end end def validate_key(request) get_terminus(request).validate(request) end end end puppet/indirector/file_bucket_file/file.rb000064400000012371147634041260014755 0ustar00require 'puppet/indirector/code' require 'puppet/file_bucket/file' require 'puppet/util/checksums' require 'fileutils' module Puppet::FileBucketFile class File < Puppet::Indirector::Code include Puppet::Util::Checksums desc "Store files in a directory set based on their checksums." def find(request) checksum, files_original_path = request_to_checksum_and_path(request) contents_file = path_for(request.options[:bucket_path], checksum, 'contents') paths_file = path_for(request.options[:bucket_path], checksum, 'paths') if Puppet::FileSystem.exist?(contents_file) && matches(paths_file, files_original_path) if request.options[:diff_with] other_contents_file = path_for(request.options[:bucket_path], request.options[:diff_with], 'contents') raise "could not find diff_with #{request.options[:diff_with]}" unless Puppet::FileSystem.exist?(other_contents_file) return `diff #{Puppet::FileSystem.path_string(contents_file).inspect} #{Puppet::FileSystem.path_string(other_contents_file).inspect}` else Puppet.info "FileBucket read #{checksum}" model.new(Puppet::FileSystem.binread(contents_file)) end else nil end end def head(request) checksum, files_original_path = request_to_checksum_and_path(request) contents_file = path_for(request.options[:bucket_path], checksum, 'contents') paths_file = path_for(request.options[:bucket_path], checksum, 'paths') Puppet::FileSystem.exist?(contents_file) && matches(paths_file, files_original_path) end def save(request) instance = request.instance _, files_original_path = request_to_checksum_and_path(request) contents_file = path_for(instance.bucket_path, instance.checksum_data, 'contents') paths_file = path_for(instance.bucket_path, instance.checksum_data, 'paths') save_to_disk(instance, files_original_path, contents_file, paths_file) # don't echo the request content back to the agent model.new('') end def validate_key(request) # There are no ACLs on filebucket files so validating key is not important end private # @param paths_file [Object] Opaque file path # @param files_original_path [String] # def matches(paths_file, files_original_path) Puppet::FileSystem.open(paths_file, 0640, 'a+') do |f| path_match(f, files_original_path) end end def path_match(file_handle, files_original_path) return true unless files_original_path # if no path was provided, it's a match file_handle.rewind file_handle.each_line do |line| return true if line.chomp == files_original_path end return false end # @param contents_file [Object] Opaque file path # @param paths_file [Object] Opaque file path # def save_to_disk(bucket_file, files_original_path, contents_file, paths_file) Puppet::Util.withumask(0007) do unless Puppet::FileSystem.dir_exist?(paths_file) Puppet::FileSystem.dir_mkpath(paths_file) end Puppet::FileSystem.exclusive_open(paths_file, 0640, 'a+') do |f| if Puppet::FileSystem.exist?(contents_file) verify_identical_file!(contents_file, bucket_file) Puppet::FileSystem.touch(contents_file) else Puppet::FileSystem.open(contents_file, 0440, 'wb') do |of| # PUP-1044 writes all of the contents bucket_file.stream() do |src| FileUtils.copy_stream(src, of) end end end unless path_match(f, files_original_path) f.seek(0, IO::SEEK_END) f.puts(files_original_path) end end end end def request_to_checksum_and_path(request) checksum_type, checksum, path = request.key.split(/\//, 3) if path == '' # Treat "md5//" like "md5/" path = nil end raise ArgumentError, "Unsupported checksum type #{checksum_type.inspect}" if checksum_type != Puppet[:digest_algorithm] expected = method(checksum_type + "_hex_length").call raise "Invalid checksum #{checksum.inspect}" if checksum !~ /^[0-9a-f]{#{expected}}$/ [checksum, path] end # @return [Object] Opaque path as constructed by the Puppet::FileSystem # def path_for(bucket_path, digest, subfile = nil) bucket_path ||= Puppet[:bucketdir] dir = ::File.join(digest[0..7].split("")) basedir = ::File.join(bucket_path, dir, digest) Puppet::FileSystem.pathname(subfile ? ::File.join(basedir, subfile) : basedir) end # @param contents_file [Object] Opaque file path # @param bucket_file [IO] def verify_identical_file!(contents_file, bucket_file) if bucket_file.size == Puppet::FileSystem.size(contents_file) if bucket_file.stream() {|s| Puppet::FileSystem.compare_stream(contents_file, s) } Puppet.info "FileBucket got a duplicate file #{bucket_file.checksum}" return end end # If the contents or sizes don't match, then we've found a conflict. # Unlikely, but quite bad. raise Puppet::FileBucket::BucketError, "Got passed new contents for sum #{bucket_file.checksum}" end end end puppet/indirector/file_bucket_file/rest.rb000064400000000353147634041260015010 0ustar00require 'puppet/indirector/rest' require 'puppet/file_bucket/file' module Puppet::FileBucketFile class Rest < Puppet::Indirector::REST desc "This is a REST based mechanism to send/retrieve file to/from the filebucket" end end puppet/indirector/terminus.rb000064400000012007147634041260012425 0ustar00require 'puppet/indirector' require 'puppet/indirector/errors' require 'puppet/indirector/indirection' require 'puppet/util/instance_loader' # A simple class that can function as the base class for indirected types. class Puppet::Indirector::Terminus require 'puppet/util/docs' extend Puppet::Util::Docs class << self include Puppet::Util::InstanceLoader attr_accessor :name, :terminus_type attr_reader :abstract_terminus, :indirection # Are we an abstract terminus type, rather than an instance with an # associated indirection? def abstract_terminus? abstract_terminus end # Convert a constant to a short name. def const2name(const) const.sub(/^[A-Z]/) { |i| i.downcase }.gsub(/[A-Z]/) { |i| "_#{i.downcase}" }.intern end # Look up the indirection if we were only provided a name. def indirection=(name) if name.is_a?(Puppet::Indirector::Indirection) @indirection = name elsif ind = Puppet::Indirector::Indirection.instance(name) @indirection = ind else raise ArgumentError, "Could not find indirection instance #{name} for #{self.name}" end end def indirection_name @indirection.name end # Register our subclass with the appropriate indirection. # This follows the convention that our terminus is named after the # indirection. def inherited(subclass) longname = subclass.to_s if longname =~ /# 0 indirection_name = names.pop.sub(/^[A-Z]/) { |i| i.downcase }.gsub(/[A-Z]/) { |i| "_#{i.downcase}" }.intern if indirection_name == "" or indirection_name.nil? raise Puppet::DevError, "Could not discern indirection model from class constant" end # This will throw an exception if the indirection instance cannot be found. # Do this last, because it also registers the terminus type with the indirection, # which needs the above information. subclass.indirection = indirection_name # And add this instance to the instance hash. Puppet::Indirector::Terminus.register_terminus_class(subclass) end # Mark that this instance is abstract. def mark_as_abstract_terminus @abstract_terminus = true end def model indirection.model end # Convert a short name to a constant. def name2const(name) name.to_s.capitalize.sub(/_(.)/) { |i| $1.upcase } end # Register a class, probably autoloaded. def register_terminus_class(klass) setup_instance_loading klass.indirection_name instance_hash(klass.indirection_name)[klass.name] = klass end # Return a terminus by name, using the autoloader. def terminus_class(indirection_name, terminus_type) setup_instance_loading indirection_name loaded_instance(indirection_name, terminus_type) end # Return all terminus classes for a given indirection. def terminus_classes(indirection_name) setup_instance_loading indirection_name instance_loader(indirection_name).files_to_load.map do |file| File.basename(file).chomp(".rb").intern end end private def setup_instance_loading(type) instance_load type, "puppet/indirector/#{type}" unless instance_loading?(type) end end def indirection self.class.indirection end def initialize raise Puppet::DevError, "Cannot create instances of abstract terminus types" if self.class.abstract_terminus? end def model self.class.model end def name self.class.name end def allow_remote_requests? true end def terminus_type self.class.terminus_type end def validate(request) if request.instance validate_model(request) validate_key(request) end end def validate_key(request) unless request.key == request.instance.name raise Puppet::Indirector::ValidationError, "Instance name #{request.instance.name.inspect} does not match requested key #{request.key.inspect}" end end def validate_model(request) unless model === request.instance raise Puppet::Indirector::ValidationError, "Invalid instance type #{request.instance.class.inspect}, expected #{model.inspect}" end end end puppet/indirector/indirection.rb000064400000025021147634041260013066 0ustar00require 'puppet/util/docs' require 'puppet/util/profiler' require 'puppet/util/methodhelper' require 'puppet/indirector/envelope' require 'puppet/indirector/request' require 'puppet/util/instrumentation/instrumentable' # The class that connects functional classes with their different collection # back-ends. Each indirection has a set of associated terminus classes, # each of which is a subclass of Puppet::Indirector::Terminus. class Puppet::Indirector::Indirection include Puppet::Util::MethodHelper include Puppet::Util::Docs extend Puppet::Util::Instrumentation::Instrumentable attr_accessor :name, :model attr_reader :termini probe :find, :label => Proc.new { |parent, key, *args| "find_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} probe :save, :label => Proc.new { |parent, key, *args| "save_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} probe :search, :label => Proc.new { |parent, key, *args| "search_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} probe :destroy, :label => Proc.new { |parent, key, *args| "destroy_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }} @@indirections = [] # Find an indirection by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.instance(name) @@indirections.find { |i| i.name == name } end # Return a list of all known indirections. Used to generate the # reference. def self.instances @@indirections.collect { |i| i.name } end # Find an indirected model by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.model(name) return nil unless match = @@indirections.find { |i| i.name == name } match.model end # Create and return our cache terminus. def cache raise(Puppet::DevError, "Tried to cache when no cache class was set") unless cache_class terminus(cache_class) end # Should we use a cache? def cache? cache_class ? true : false end attr_reader :cache_class # Define a terminus class to be used for caching. def cache_class=(class_name) validate_terminus_class(class_name) if class_name @cache_class = class_name end # This is only used for testing. def delete @@indirections.delete(self) if @@indirections.include?(self) end # Set the time-to-live for instances created through this indirection. def ttl=(value) raise ArgumentError, "Indirection TTL must be an integer" unless value.is_a?(Fixnum) @ttl = value end # Default to the runinterval for the ttl. def ttl @ttl ||= Puppet[:runinterval] end # Calculate the expiration date for a returned instance. def expiration Time.now + ttl end # Generate the full doc string. def doc text = "" text << scrub(@doc) << "\n\n" if @doc text << "* **Indirected Class**: `#{@indirected_class}`\n"; if terminus_setting text << "* **Terminus Setting**: #{terminus_setting}\n" end text end def initialize(model, name, options = {}) @model = model @name = name @termini = {} @cache_class = nil @terminus_class = nil raise(ArgumentError, "Indirection #{@name} is already defined") if @@indirections.find { |i| i.name == @name } @@indirections << self @indirected_class = options.delete(:indirected_class) if mod = options[:extend] extend(mod) options.delete(:extend) end # This is currently only used for cache_class and terminus_class. set_options(options) end # Set up our request object. def request(*args) Puppet::Indirector::Request.new(self.name, *args) end # Return the singleton terminus for this indirection. def terminus(terminus_name = nil) # Get the name of the terminus. raise Puppet::DevError, "No terminus specified for #{self.name}; cannot redirect" unless terminus_name ||= terminus_class termini[terminus_name] ||= make_terminus(terminus_name) end # This can be used to select the terminus class. attr_accessor :terminus_setting # Determine the terminus class. def terminus_class unless @terminus_class if setting = self.terminus_setting self.terminus_class = Puppet.settings[setting] else raise Puppet::DevError, "No terminus class nor terminus setting was provided for indirection #{self.name}" end end @terminus_class end def reset_terminus_class @terminus_class = nil end # Specify the terminus class to use. def terminus_class=(klass) validate_terminus_class(klass) @terminus_class = klass end # This is used by terminus_class= and cache=. def validate_terminus_class(terminus_class) raise ArgumentError, "Invalid terminus name #{terminus_class.inspect}" unless terminus_class and terminus_class.to_s != "" unless Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class) raise ArgumentError, "Could not find terminus #{terminus_class} for indirection #{self.name}" end end # Expire a cached object, if one is cached. Note that we don't actually # remove it, we expire it and write it back out to disk. This way people # can still use the expired object if they want. def expire(key, options={}) request = request(:expire, key, nil, options) return nil unless cache? return nil unless instance = cache.find(request(:find, key, nil, options)) Puppet.info "Expiring the #{self.name} cache of #{instance.name}" # Set an expiration date in the past instance.expiration = Time.now - 60 cache.save(request(:save, nil, instance, options)) end def allow_remote_requests? terminus.allow_remote_requests? end # Search for an instance in the appropriate terminus, caching the # results if caching is configured.. def find(key, options={}) request = request(:find, key, nil, options) terminus = prepare(request) result = find_in_cache(request) if not result.nil? result elsif request.ignore_terminus? nil else # Otherwise, return the result from the terminus, caching if # appropriate. result = terminus.find(request) if not result.nil? result.expiration ||= self.expiration if result.respond_to?(:expiration) if cache? Puppet.info "Caching #{self.name} for #{request.key}" cache.save request(:save, key, result, options) end filtered = result if terminus.respond_to?(:filter) Puppet::Util::Profiler.profile("Filtered result for #{self.name} #{request.key}", [:indirector, :filter, self.name, request.key]) do filtered = terminus.filter(result) end end filtered end end end # Search for an instance in the appropriate terminus, and return a # boolean indicating whether the instance was found. def head(key, options={}) request = request(:head, key, nil, options) terminus = prepare(request) # Look in the cache first, then in the terminus. Force the result # to be a boolean. !!(find_in_cache(request) || terminus.head(request)) end def find_in_cache(request) # See if our instance is in the cache and up to date. return nil unless cache? and ! request.ignore_cache? and cached = cache.find(request) if cached.expired? Puppet.info "Not using expired #{self.name} for #{request.key} from cache; expired at #{cached.expiration}" return nil end Puppet.debug "Using cached #{self.name} for #{request.key}" cached rescue => detail Puppet.log_exception(detail, "Cached #{self.name} for #{request.key} failed: #{detail}") nil end # Remove something via the terminus. def destroy(key, options={}) request = request(:destroy, key, nil, options) terminus = prepare(request) result = terminus.destroy(request) if cache? and cache.find(request(:find, key, nil, options)) # Reuse the existing request, since it's equivalent. cache.destroy(request) end result end # Search for more than one instance. Should always return an array. def search(key, options={}) request = request(:search, key, nil, options) terminus = prepare(request) if result = terminus.search(request) raise Puppet::DevError, "Search results from terminus #{terminus.name} are not an array" unless result.is_a?(Array) result.each do |instance| next unless instance.respond_to? :expiration instance.expiration ||= self.expiration end return result end end # Save the instance in the appropriate terminus. This method is # normally an instance method on the indirected class. def save(instance, key = nil, options={}) request = request(:save, key, instance, options) terminus = prepare(request) result = terminus.save(request) # If caching is enabled, save our document there cache.save(request) if cache? result end private # Check authorization if there's a hook available; fail if there is one # and it returns false. def check_authorization(request, terminus) # At this point, we're assuming authorization makes no sense without # client information. return unless request.node # This is only to authorize via a terminus-specific authorization hook. return unless terminus.respond_to?(:authorized?) unless terminus.authorized?(request) msg = "Not authorized to call #{request.method} on #{request}" msg += " with #{request.options.inspect}" unless request.options.empty? raise ArgumentError, msg end end # Setup a request, pick the appropriate terminus, check the request's authorization, and return it. def prepare(request) # Pick our terminus. if respond_to?(:select_terminus) unless terminus_name = select_terminus(request) raise ArgumentError, "Could not determine appropriate terminus for #{request}" end else terminus_name = terminus_class end dest_terminus = terminus(terminus_name) check_authorization(request, dest_terminus) dest_terminus.validate(request) dest_terminus end # Create a new terminus instance. def make_terminus(terminus_class) # Load our terminus class. unless klass = Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class) raise ArgumentError, "Could not find terminus #{terminus_class} for indirection #{self.name}" end klass.new end end puppet/indirector/resource_type/parser.rb000064400000007207147634041260014751 0ustar00require 'puppet/resource/type' require 'puppet/indirector/code' require 'puppet/indirector/resource_type' # The main terminus for Puppet::Resource::Type # # This exposes the known resource types from within Puppet. Only find # and search are supported. When a request is received, Puppet will # attempt to load all resource types (by parsing manifests and modules) and # returns a description of the resource types found. The format of these # objects is documented at {Puppet::Resource::Type}. # # @api public class Puppet::Indirector::ResourceType::Parser < Puppet::Indirector::Code desc "Return the data-form of a resource type." # Find will return the first resource_type with the given name. It is # not possible to specify the kind of the resource type. # # @param request [Puppet::Indirector::Request] The request object. # The only parameters used from the request are `environment` and # `key`, which corresponds to the resource type's `name` field. # @return [Puppet::Resource::Type, nil] # @api public def find(request) Puppet.override(:squelch_parse_errors => true) do krt = resource_types_in(request.environment) # This is a bit ugly. [:hostclass, :definition, :node].each do |type| # We have to us 'find_' here because it will # load any missing types from disk, whereas the plain # '' method only returns from memory. if r = krt.send("find_#{type}", [""], request.key) return r end end nil end end # Search for resource types using a regular expression. Unlike `find`, this # allows you to filter the results by the "kind" of the resource type # ("class", "defined_type", or "node"). All three are searched if no # `kind` filter is given. This also accepts the special string "`*`" # to return all resource type objects. # # @param request [Puppet::Indirector::Request] The request object. The # `key` field holds the regular expression used to search, and # `options[:kind]` holds the kind query parameter to filter the # result as described above. The `environment` field specifies the # environment used to load resources. # # @return [Array, nil] # # @api public def search(request) Puppet.override(:squelch_parse_errors => true) do krt = resource_types_in(request.environment) # Make sure we've got all of the types loaded. krt.loader.import_all result_candidates = case request.options[:kind] when "class" krt.hostclasses.values when "defined_type" krt.definitions.values when "node" krt.nodes.values when nil result_candidates = [krt.hostclasses.values, krt.definitions.values, krt.nodes.values] else raise ArgumentError, "Unrecognized kind filter: " + "'#{request.options[:kind]}', expected one " + " of 'class', 'defined_type', or 'node'." end result = result_candidates.flatten.reject { |t| t.name == "" } return nil if result.empty? return result if request.key == "*" # Strip the regex of any wrapping slashes that might exist key = request.key.sub(/^\//, '').sub(/\/$/, '') begin regex = Regexp.new(key) rescue => detail raise ArgumentError, "Invalid regex '#{request.key}': #{detail}", detail.backtrace end result.reject! { |t| t.name.to_s !~ regex } return nil if result.empty? result end end def resource_types_in(environment) environment.check_for_reparse environment.known_resource_types end end puppet/indirector/resource_type/rest.rb000064400000000363147634041260014426 0ustar00require 'puppet/resource/type' require 'puppet/indirector/rest' require 'puppet/indirector/resource_type' class Puppet::Indirector::ResourceType::Rest < Puppet::Indirector::REST desc "Retrieve resource types via a REST HTTP interface." end puppet/indirector/status.rb000064400000000114147634041260012076 0ustar00# A stub class, so our constants work. class Puppet::Indirector::Status end puppet/indirector/data_binding/hiera.rb000064400000000237147634041260014254 0ustar00require 'puppet/indirector/hiera' require 'hiera/scope' class Puppet::DataBinding::Hiera < Puppet::Indirector::Hiera desc "Retrieve data using Hiera." end puppet/indirector/data_binding/none.rb000064400000000245147634041260014122 0ustar00require 'puppet/indirector/none' class Puppet::DataBinding::None < Puppet::Indirector::None desc "A Dummy terminus that always returns nil for data lookups." end puppet/reference/type.rb000064400000006504147634041260011341 0ustar00Puppet::Util::Reference.newreference :type, :doc => "All Puppet resource types and all their details" do types = {} Puppet::Type.loadall Puppet::Type.eachtype { |type| next if type.name == :puppet next if type.name == :component next if type.name == :whit types[type.name] = type } str = %{ ## Resource Types - The *namevar* is the parameter used to uniquely identify a type instance. This is the parameter that gets assigned when a string is provided before the colon in a type declaration. In general, only developers will need to worry about which parameter is the `namevar`. In the following code: file { "/etc/passwd": owner => root, group => root, mode => 644 } `/etc/passwd` is considered the title of the file object (used for things like dependency handling), and because `path` is the namevar for `file`, that string is assigned to the `path` parameter. - *Parameters* determine the specific configuration of the instance. They either directly modify the system (internally, these are called properties) or they affect how the instance behaves (e.g., adding a search path for `exec` instances or determining recursion on `file` instances). - *Providers* provide low-level functionality for a given resource type. This is usually in the form of calling out to external commands. When required binaries are specified for providers, fully qualifed paths indicate that the binary must exist at that specific path and unqualified binaries indicate that Puppet will search for the binary using the shell path. - *Features* are abilities that some providers might not support. You can use the list of supported features to determine how a given provider can be used. Resource types define features they can use, and providers can be tested to see which features they provide. } types.sort { |a,b| a.to_s <=> b.to_s }.each { |name,type| str << " ---------------- " str << markdown_header(name, 3) str << scrub(type.doc) + "\n\n" # Handle the feature docs. if featuredocs = type.featuredocs str << markdown_header("Features", 4) str << featuredocs end docs = {} type.validproperties.sort { |a,b| a.to_s <=> b.to_s }.reject { |sname| property = type.propertybyname(sname) property.nodoc }.each { |sname| property = type.propertybyname(sname) raise "Could not retrieve property #{sname} on type #{type.name}" unless property doc = nil unless doc = property.doc $stderr.puts "No docs for #{type}[#{sname}]" next end doc = doc.dup tmp = doc tmp = scrub(tmp) docs[sname] = tmp } str << markdown_header("Parameters", 4) + "\n" type.parameters.sort { |a,b| a.to_s <=> b.to_s }.each { |name,param| #docs[name] = indent(scrub(type.paramdoc(name)), $tab) docs[name] = scrub(type.paramdoc(name)) } additional_key_attributes = type.key_attributes - [:name] docs.sort { |a, b| a[0].to_s <=> b[0].to_s }.each { |name, doc| if additional_key_attributes.include?(name) doc = "(**Namevar:** If omitted, this parameter's value defaults to the resource's title.)\n\n" + doc end str << markdown_definitionlist(name, doc) } str << "\n" } str end puppet/reference/metaparameter.rb000064400000002225147634041260013203 0ustar00Puppet::Util::Reference.newreference :metaparameter, :doc => "All Puppet metaparameters and all their details" do types = {} Puppet::Type.loadall Puppet::Type.eachtype { |type| next if type.name == :puppet next if type.name == :component types[type.name] = type } str = %{ Metaparameters are attributes that work with any resource type, including custom types and defined types. In general, they affect _Puppet's_ behavior rather than the desired state of the resource. Metaparameters do things like add metadata to a resource (`alias`, `tag`), set limits on when the resource should be synced (`require`, `schedule`, etc.), prevent Puppet from making changes (`noop`), and change logging verbosity (`loglevel`). ## Available Metaparameters } begin params = [] Puppet::Type.eachmetaparam { |param| params << param } params.sort { |a,b| a.to_s <=> b.to_s }.each { |param| str << markdown_header(param.to_s, 3) str << scrub(Puppet::Type.metaparamdoc(param)) str << "\n\n" } rescue => detail Puppet.log_exception(detail, "incorrect metaparams: #{detail}") exit(1) end str end puppet/reference/configuration.rb000064400000004567147634041260013236 0ustar00config = Puppet::Util::Reference.newreference(:configuration, :depth => 1, :doc => "A reference for all settings") do docs = {} Puppet.settings.each do |name, object| docs[name] = object end str = "" docs.sort { |a, b| a[0].to_s <=> b[0].to_s }.each do |name, object| # Make each name an anchor header = name.to_s str << markdown_header(header, 3) # Print the doc string itself begin str << Puppet::Util::Docs.scrub(object.desc) rescue => detail Puppet.log_exception(detail) end str << "\n\n" # Now print the data about the item. val = object.default if name.to_s == "vardir" val = "/var/lib/puppet" elsif name.to_s == "confdir" val = "/etc/puppet" end # Leave out the section information; it was apparently confusing people. #str << "- **Section**: #{object.section}\n" unless val == "" str << "- *Default*: #{val}\n" end str << "\n" end return str end config.header = < "All functions available in the parser" do Puppet::Parser::Functions.functiondocs end function.header = " There are two types of functions in Puppet: Statements and rvalues. Statements stand on their own and do not return arguments; they are used for performing stand-alone work like importing. Rvalues return values and can only be used in a statement requiring a value, such as an assignment or a case statement. Functions execute on the Puppet master. They do not execute on the Puppet agent. Hence they only have access to the commands and data available on the Puppet master host. Here are the functions available in Puppet: " puppet/reference/providers.rb000064400000007417147634041260012401 0ustar00# This doesn't get stored in trac, since it changes every time. providers = Puppet::Util::Reference.newreference :providers, :title => "Provider Suitability Report", :depth => 1, :dynamic => true, :doc => "Which providers are valid for this machine" do types = [] Puppet::Type.loadall Puppet::Type.eachtype do |klass| next unless klass.providers.length > 0 types << klass end types.sort! { |a,b| a.name.to_s <=> b.name.to_s } command_line = Puppet::Util::CommandLine.new types.reject! { |type| ! command_line.args.include?(type.name.to_s) } unless command_line.args.empty? ret = "Details about this host:\n\n" # Throw some facts in there, so we know where the report is from. ["Ruby Version", "Puppet Version", "Operating System", "Operating System Release"].each do |label| name = label.gsub(/\s+/, '') value = Facter.value(name) ret << option(label, value) end ret << "\n" count = 1 # Produce output for each type. types.each do |type| features = type.features ret << "\n" # add a trailing newline # Now build up a table of provider suitability. headers = %w{Provider Suitable?} + features.collect { |f| f.to_s }.sort table_data = {} functional = false notes = [] default = type.defaultprovider ? type.defaultprovider.name : 'none' type.providers.sort { |a,b| a.to_s <=> b.to_s }.each do |pname| data = [] table_data[pname] = data provider = type.provider(pname) # Add the suitability note if missing = provider.suitable?(false) and missing.empty? data << "*X*" suit = true functional = true else data << "[#{count}]_" # A pointer to the appropriate footnote suit = false end # Add a footnote with the details about why this provider is unsuitable, if that's the case unless suit details = ".. [#{count}]\n" missing.each do |test, values| case test when :exists details << " - Missing files #{values.join(", ")}\n" when :variable values.each do |name, facts| if Puppet.settings.valid?(name) details << " - Setting #{name} (currently #{Puppet.settings.value(name).inspect}) not in list #{facts.join(", ")}\n" else details << " - Fact #{name} (currently #{Facter.value(name).inspect}) not in list #{facts.join(", ")}\n" end end when :true details << " - Got #{values} true tests that should have been false\n" when :false details << " - Got #{values} false tests that should have been true\n" when :feature details << " - Missing features #{values.collect { |f| f.to_s }.join(",")}\n" end end notes << details count += 1 end # Add a note for every feature features.each do |feature| if provider.features.include?(feature) data << "*X*" else data << "" end end end ret << markdown_header(type.name.to_s + "_", 2) ret << "[#{type.name}](#{"http://docs.puppetlabs.com/references/stable/type.html##{type.name}"})\n\n" ret << option("Default provider", default) ret << doctable(headers, table_data) notes.each do |note| ret << note + "\n" end ret << "\n" end ret << "\n" ret end providers.header = " Puppet resource types are usually backed by multiple implementations called `providers`, which handle variance between platforms and tools. Different providers are suitable or unsuitable on different platforms based on things like the presence of a given tool. Here are all of the provider-backed types and their different providers. Any unmentioned types do not use providers yet. " puppet/reference/report.rb000064400000001676147634041260011700 0ustar00require 'puppet/reports' report = Puppet::Util::Reference.newreference :report, :doc => "All available transaction reports" do Puppet::Reports.reportdocs end report.header = " Puppet clients can report back to the server after each transaction. This transaction report is sent as a YAML dump of the `Puppet::Transaction::Report` class and includes every log message that was generated during the transaction along with as many metrics as Puppet knows how to collect. See [Reports and Reporting](http://docs.puppetlabs.com/guides/reporting.html) for more information on how to use reports. Currently, clients default to not sending in reports; you can enable reporting by setting the `report` parameter to true. To use a report, set the `reports` parameter on the server; multiple reports must be comma-separated. You can also specify `none` to disable reports entirely. Puppet provides multiple report handlers that will process client reports: " puppet/reference/indirection.rb000064400000007004147634041260012663 0ustar00require 'puppet/indirector/indirection' require 'puppet/util/checksums' require 'puppet/file_serving/content' require 'puppet/file_serving/metadata' reference = Puppet::Util::Reference.newreference :indirection, :doc => "Indirection types and their terminus classes" do text = "" Puppet::Indirector::Indirection.instances.sort { |a,b| a.to_s <=> b.to_s }.each do |indirection| ind = Puppet::Indirector::Indirection.instance(indirection) name = indirection.to_s.capitalize text << "## " + indirection.to_s + "\n\n" text << Puppet::Util::Docs.scrub(ind.doc) + "\n\n" Puppet::Indirector::Terminus.terminus_classes(ind.name).sort { |a,b| a.to_s <=> b.to_s }.each do |terminus| terminus_name = terminus.to_s term_class = Puppet::Indirector::Terminus.terminus_class(ind.name, terminus) if term_class terminus_doc = Puppet::Util::Docs.scrub(term_class.doc) text << markdown_header("`#{terminus_name}` terminus", 3) << terminus_doc << "\n\n" else Puppet.warning "Could not build docs for indirector #{name.inspect}, terminus #{terminus_name.inspect}: could not locate terminus." end end end text end reference.header = <
String}] Map of name to command that the provider will # be executing on the system. Each command is specified with a name and the path of the executable. # @return [void] # @see optional_commands # @api public # def self.commands(command_specs) command_specs.each do |name, path| has_command(name, path) end end # Defines optional commands. # Since Puppet 2.7.8 this is typically not needed as evaluation of provider suitability # is lazy (when a resource is evaluated) and the absence of commands # that will be present after other resources have been applied no longer needs to be specified as # optional. # @param [Hash{String => String}] hash Named commands that the provider will # be executing on the system. Each command is specified with a name and the path of the executable. # (@see #has_command) # @see commands # @api public def self.optional_commands(hash) hash.each do |name, target| has_command(name, target) do is_optional end end end # Creates a convenience method for invocation of a command. # # This generates a Provider method that allows easy execution of the command. The generated # method may take arguments that will be passed through to the executable as the command line arguments # when it is invoked. # # @example Use it like this: # has_command(:echo, "/bin/echo") # def some_method # echo("arg 1", "arg 2") # end # @comment the . . . below is intentional to avoid the three dots to become an illegible ellipsis char. # @example . . . or like this # has_command(:echo, "/bin/echo") do # is_optional # environment :HOME => "/var/tmp", :PWD => "/tmp" # end # # @param name [Symbol] The name of the command (will become the name of the generated method that executes the command) # @param path [String] The path to the executable for the command # @yield [ ] A block that configures the command (see {Puppet::Provider::Command}) # @comment a yield [ ] produces {|| ...} in the signature, do not remove the space. # @note the name ´has_command´ looks odd in an API context, but makes more sense when seen in the internal # DSL context where a Provider is declaratively defined. # @api public # def self.has_command(name, path, &block) name = name.intern command = CommandDefiner.define(name, path, self, &block) @commands[name] = command.executable # Now define the class and instance methods. create_class_and_instance_method(name) do |*args| return command.execute(*args) end end # Internal helper class when creating commands - undocumented. # @api private class CommandDefiner private_class_method :new def self.define(name, path, confiner, &block) definer = new(name, path, confiner) definer.instance_eval(&block) if block definer.command end def initialize(name, path, confiner) @name = name @path = path @optional = false @confiner = confiner @custom_environment = {} end def is_optional @optional = true end def environment(env) @custom_environment = @custom_environment.merge(env) end def command if not @optional @confiner.confine :exists => @path, :for_binary => true end Puppet::Provider::Command.new(@name, @path, Puppet::Util, Puppet::Util::Execution, { :failonfail => true, :combine => true, :custom_environment => @custom_environment }) end end # @return [Boolean] Return whether the given feature has been declared or not. def self.declared_feature?(name) defined?(@declared_features) and @declared_features.include?(name) end # @return [Boolean] Returns whether this implementation satisfies all of the default requirements or not. # Returns false if there is no matching defaultfor # @see Provider.defaultfor # def self.default? default_match ? true : false end # Look through the array of defaultfor hashes and return the first match. # @return [Hash<{String => Object}>] the matching hash specified by a defaultfor # @see Provider.defaultfor # @api private def self.default_match @defaults.find do |default| default.all? do |key, values| case key when :feature feature_match(values) else fact_match(key, values) end end end end def self.fact_match(fact, values) values = [values] unless values.is_a? Array values.map! { |v| v.to_s.downcase.intern } if fval = Facter.value(fact).to_s and fval != "" fval = fval.to_s.downcase.intern values.include?(fval) else false end end def self.feature_match(value) Puppet.features.send(value.to_s + "?") end # Sets a facts filter that determine which of several suitable providers should be picked by default. # This selection only kicks in if there is more than one suitable provider. # To filter on multiple facts the given hash may contain more than one fact name/value entry. # The filter picks the provider if all the fact/value entries match the current set of facts. (In case # there are still more than one provider after this filtering, the first found is picked). # @param hash [Hash<{String => Object}>] hash of fact name to fact value. # @return [void] # def self.defaultfor(hash) @defaults << hash end # @return [Integer] Returns a numeric specificity for this provider based on how many requirements it has # and number of _ancestors_. The higher the number the more specific the provider. # The number of requirements is based on the hash size of the matching {Provider.defaultfor}. # # The _ancestors_ is the Ruby Module::ancestors method and the number of classes returned is used # to boost the score. The intent is that if two providers are equal, but one is more "derived" than the other # (i.e. includes more classes), it should win because it is more specific). # @note Because of how this value is # calculated there could be surprising side effects if a provider included an excessive amount of classes. # def self.specificity # This strange piece of logic attempts to figure out how many parent providers there # are to increase the score. What is will actually do is count all classes that Ruby Module::ancestors # returns (which can be other classes than those the parent chain) - in a way, an odd measure of the # complexity of a provider). match = default_match length = match ? match.length : 0 (length * 100) + ancestors.select { |a| a.is_a? Class }.length end # Initializes defaults and commands (i.e. clears them). # @return [void] def self.initvars @defaults = [] @commands = {} end # Returns a list of system resources (entities) this provider may/can manage. # This is a query mechanism that lists entities that the provider may manage on a given system. It is # is directly used in query services, but is also the foundation for other services; prefetching, and # purging. # # As an example, a package provider lists all installed packages. (In contrast, the File provider does # not list all files on the file-system as that would make execution incredibly slow). An implementation # of this method should be made if it is possible to quickly (with a single system call) provide all # instances. # # An implementation of this method should only cache the values of properties # if they are discovered as part of the process for finding existing resources. # Resource properties that require additional commands (than those used to determine existence/identity) # should be implemented in their respective getter method. (This is important from a performance perspective; # it may be expensive to compute, as well as wasteful as all discovered resources may perhaps not be managed). # # An implementation may return an empty list (naturally with the effect that it is not possible to query # for manageable entities). # # By implementing this method, it is possible to use the `resources´ resource type to specify purging # of all non managed entities. # # @note The returned instances are instance of some subclass of Provider, not resources. # @return [Array] a list of providers referencing the system entities # @abstract this method must be implemented by a subclass and this super method should never be called as it raises an exception. # @raise [Puppet::DevError] Error indicating that the method should have been implemented by subclass. # @see prefetch def self.instances raise Puppet::DevError, "Provider #{self.name} has not defined the 'instances' class method" end # Creates the methods for a given command. # @api private # @deprecated Use {commands}, {optional_commands}, or {has_command} instead. This was not meant to be part of a public API def self.make_command_methods(name) Puppet.deprecation_warning "Provider.make_command_methods is deprecated; use Provider.commands or Provider.optional_commands instead for creating command methods" # Now define a method for that command unless singleton_class.method_defined?(name) meta_def(name) do |*args| # This might throw an ExecutionFailure, but the system above # will catch it, if so. command = Puppet::Provider::Command.new(name, command(name), Puppet::Util, Puppet::Util::Execution) return command.execute(*args) end # And then define an instance method that just calls the class method. # We need both, so both instances and classes can easily run the commands. unless method_defined?(name) define_method(name) do |*args| self.class.send(name, *args) end end end end # Creates getter- and setter- methods for each property supported by the resource type. # Call this method to generate simple accessors for all properties supported by the # resource type. These simple accessors lookup and sets values in the property hash. # The generated methods may be overridden by more advanced implementations if something # else than a straight forward getter/setter pair of methods is required. # (i.e. define such overriding methods after this method has been called) # # An implementor of a provider that makes use of `prefetch` and `flush` can use this method since it uses # the internal `@property_hash` variable to store values. An implementation would then update the system # state on a call to `flush` based on the current values in the `@property_hash`. # # @return [void] # def self.mk_resource_methods [resource_type.validproperties, resource_type.parameters].flatten.each do |attr| attr = attr.intern next if attr == :name define_method(attr) do if @property_hash[attr].nil? :absent else @property_hash[attr] end end define_method(attr.to_s + "=") do |val| @property_hash[attr] = val end end end self.initvars # This method is used to generate a method for a command. # @return [void] # @api private # def self.create_class_and_instance_method(name, &block) unless singleton_class.method_defined?(name) meta_def(name, &block) end unless method_defined?(name) define_method(name) do |*args| self.class.send(name, *args) end end end private_class_method :create_class_and_instance_method # @return [String] Returns the data source, which is the provider name if no other source has been set. # @todo Unclear what "the source" is used for? def self.source @source ||= self.name end # Returns true if the given attribute/parameter is supported by the provider. # The check is made that the parameter is a valid parameter for the resource type, and then # if all its required features (if any) are supported by the provider. # # @param param [Class, Puppet::Parameter] the parameter class, or a parameter instance # @return [Boolean] Returns whether this provider supports the given parameter or not. # @raise [Puppet::DevError] if the given parameter is not valid for the resource type # def self.supports_parameter?(param) if param.is_a?(Class) klass = param else unless klass = resource_type.attrclass(param) raise Puppet::DevError, "'#{param}' is not a valid parameter for #{resource_type.name}" end end return true unless features = klass.required_features !!satisfies?(*features) end # def self.to_s # unless defined?(@str) # if self.resource_type # @str = "#{resource_type.name} provider #{self.name}" # else # @str = "unattached provider #{self.name}" # end # end # @str # end dochook(:defaults) do if @defaults.length > 0 return @defaults.collect do |d| "Default for " + d.collect do |f, v| "`#{f}` == `#{[v].flatten.join(', ')}`" end.sort.join(" and ") + "." end.join(" ") end end dochook(:commands) do if @commands.length > 0 return "Required binaries: " + @commands.collect do |n, c| "`#{c}`" end.sort.join(", ") + "." end end dochook(:features) do if features.length > 0 return "Supported features: " + features.collect do |f| "`#{f}`" end.sort.join(", ") + "." end end # Clears this provider instance to allow GC to clean up. def clear @resource = nil @model = nil end # (see command) def command(name) self.class.command(name) end # Returns the value of a parameter value, or `:absent` if it is not defined. # @param param [Puppet::Parameter] the parameter to obtain the value of # @return [Object] the value of the parameter or `:absent` if not defined. # def get(param) @property_hash[param.intern] || :absent end # Creates a new provider that is optionally initialized from a resource or a hash of properties. # If no argument is specified, a new non specific provider is initialized. If a resource is given # it is remembered for further operations. If a hash is used it becomes the internal `@property_hash` # structure of the provider - this hash holds the current state property values of system entities # as they are being discovered by querying or other operations (typically getters). # # @todo The use of a hash as a parameter needs a better exaplanation; why is this done? What is the intent? # @param resource [Puppet::Resource, Hash] optional resource or hash # def initialize(resource = nil) if resource.is_a?(Hash) # We don't use a duplicate here, because some providers (ParsedFile, at least) # use the hash here for later events. @property_hash = resource elsif resource @resource = resource # LAK 2007-05-09: Keep the model stuff around for backward compatibility @model = resource @property_hash = {} else @property_hash = {} end end # Returns the name of the resource this provider is operating on. # @return [String] the name of the resource instance (e.g. the file path of a File). # @raise [Puppet::DevError] if no resource is set, or no name defined. # def name if n = @property_hash[:name] return n elsif self.resource resource.name else raise Puppet::DevError, "No resource and no name in property hash in #{self.class.name} instance" end end # Sets the given parameters values as the current values for those parameters. # Other parameters are unchanged. # @param [Array] params the parameters with values that should be set # @return [void] # def set(params) params.each do |param, value| @property_hash[param.intern] = value end end # @return [String] Returns a human readable string with information about the resource and the provider. def to_s "#{@resource}(provider=#{self.class.name})" end # Makes providers comparable. include Comparable # Compares this provider against another provider. # Comparison is only possible with another provider (no other class). # The ordering is based on the class name of the two providers. # # @return [-1,0,+1, nil] A comparison result -1, 0, +1 if this is before other, equal or after other. Returns # nil oif not comparable to other. # @see Comparable def <=>(other) # We can only have ordering against other providers. return nil unless other.is_a? Puppet::Provider # Otherwise, order by the providers class name. return self.class.name <=> other.class.name end # @comment Document prefetch here as it does not exist anywhere else (called from transaction if implemented) # @!method self.prefetch(resource_hash) # @abstract A subclass may implement this - it is not implemented in the Provider class # This method may be implemented by a provider in order to pre-fetch resource properties. # If implemented it should set the provider instance of the managed resources to a provider with the # fetched state (i.e. what is returned from the {instances} method). # @param resources_hash [Hash<{String => Puppet::Resource}>] map from name to resource of resources to prefetch # @return [void] # @api public # @comment Document post_resource_eval here as it does not exist anywhere else (called from transaction if implemented) # @!method self.post_resource_eval() # @since 3.4.0 # @api public # @abstract A subclass may implement this - it is not implemented in the Provider class # This method may be implemented by a provider in order to perform any # cleanup actions needed. It will be called at the end of the transaction if # any resources in the catalog make use of the provider, regardless of # whether the resources are changed or not and even if resource failures occur. # @return [void] # @comment Document flush here as it does not exist anywhere (called from transaction if implemented) # @!method flush() # @abstract A subclass may implement this - it is not implemented in the Provider class # This method may be implemented by a provider in order to flush properties that has not been individually # applied to the managed entity's current state. # @return [void] # @api public end puppet/relationship.rb000064400000004277147634041260011130 0ustar00# objects react to an event require 'puppet/util/pson' # This is Puppet's class for modeling edges in its configuration graph. # It used to be a subclass of GRATR::Edge, but that class has weird hash # overrides that dramatically slow down the graphing. class Puppet::Relationship extend Puppet::Util::Pson attr_accessor :source, :target, :callback attr_reader :event def self.from_data_hash(data) source = data["source"] target = data["target"] args = {} if event = data["event"] args[:event] = event end if callback = data["callback"] args[:callback] = callback end new(source, target, args) end def self.from_pson(pson) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(pson) end def event=(event) raise ArgumentError, "You must pass a callback for non-NONE events" if event != :NONE and ! callback @event = event end def initialize(source, target, options = {}) @source, @target = source, target options = (options || {}).inject({}) { |h,a| h[a[0].to_sym] = a[1]; h } [:callback, :event].each do |option| if value = options[option] send(option.to_s + "=", value) end end end # Does the passed event match our event? This is where the meaning # of :NONE comes from. def match?(event) if self.event.nil? or event == :NONE or self.event == :NONE return false elsif self.event == :ALL_EVENTS or event == self.event return true else return false end end def label result = {} result[:callback] = callback if callback result[:event] = event if event result end def ref "#{source} => #{target}" end def inspect "{ #{source} => #{target} }" end def to_data_hash data = { 'source' => source.to_s, 'target' => target.to_s } ["event", "callback"].each do |attr| next unless value = send(attr) data[attr] = value end data end # This doesn't include document type as it is part of a catalog def to_pson_data_hash to_data_hash end def to_pson(*args) to_data_hash.to_pson(*args) end def to_s ref end end puppet/resource.rb000064400000043725147634041260010257 0ustar00require 'puppet' require 'puppet/util/tagging' require 'puppet/util/pson' require 'puppet/parameter' # The simplest resource class. Eventually it will function as the # base class for all resource-like behaviour. # # @api public class Puppet::Resource # This stub class is only needed for serialization compatibility with 0.25.x. # Specifically, it exists to provide a compatibility API when using YAML # serialized objects loaded from StoreConfigs. Reference = Puppet::Resource include Puppet::Util::Tagging extend Puppet::Util::Pson include Enumerable attr_accessor :file, :line, :catalog, :exported, :virtual, :validate_parameters, :strict attr_reader :type, :title require 'puppet/indirector' extend Puppet::Indirector indirects :resource, :terminus_class => :ral ATTRIBUTES = [:file, :line, :exported] def self.from_data_hash(data) raise ArgumentError, "No resource type provided in serialized data" unless type = data['type'] raise ArgumentError, "No resource title provided in serialized data" unless title = data['title'] resource = new(type, title) if params = data['parameters'] params.each { |param, value| resource[param] = value } end if tags = data['tags'] tags.each { |tag| resource.tag(tag) } end ATTRIBUTES.each do |a| if value = data[a.to_s] resource.send(a.to_s + "=", value) end end resource end def self.from_pson(pson) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(pson) end def inspect "#{@type}[#{@title}]#{to_hash.inspect}" end def to_data_hash data = ([:type, :title, :tags] + ATTRIBUTES).inject({}) do |hash, param| next hash unless value = self.send(param) hash[param.to_s] = value hash end data["exported"] ||= false params = self.to_hash.inject({}) do |hash, ary| param, value = ary # Don't duplicate the title as the namevar next hash if param == namevar and value == title hash[param] = Puppet::Resource.value_to_pson_data(value) hash end data["parameters"] = params unless params.empty? data end # This doesn't include document type as it is part of a catalog def to_pson_data_hash to_data_hash end def self.value_to_pson_data(value) if value.is_a? Array value.map{|v| value_to_pson_data(v) } elsif value.is_a? Puppet::Resource value.to_s else value end end def yaml_property_munge(x) case x when Hash x.inject({}) { |h,kv| k,v = kv h[k] = self.class.value_to_pson_data(v) h } else self.class.value_to_pson_data(x) end end YAML_ATTRIBUTES = [:@file, :@line, :@exported, :@type, :@title, :@tags, :@parameters] # Explicitly list the instance variables that should be serialized when # converting to YAML. # # @api private # @return [Array] The intersection of our explicit variable list and # all of the instance variables defined on this class. def to_yaml_properties YAML_ATTRIBUTES & super end def to_pson(*args) to_data_hash.to_pson(*args) end # Proxy these methods to the parameters hash. It's likely they'll # be overridden at some point, but this works for now. %w{has_key? keys length delete empty? <<}.each do |method| define_method(method) do |*args| parameters.send(method, *args) end end # Set a given parameter. Converts all passed names # to lower-case symbols. def []=(param, value) validate_parameter(param) if validate_parameters parameters[parameter_name(param)] = value end # Return a given parameter's value. Converts all passed names # to lower-case symbols. def [](param) parameters[parameter_name(param)] end def ==(other) return false unless other.respond_to?(:title) and self.type == other.type and self.title == other.title return false unless to_hash == other.to_hash true end # Compatibility method. def builtin? builtin_type? end # Is this a builtin resource type? def builtin_type? resource_type.is_a?(Class) end # Iterate over each param/value pair, as required for Enumerable. def each parameters.each { |p,v| yield p, v } end def include?(parameter) super || parameters.keys.include?( parameter_name(parameter) ) end %w{exported virtual strict}.each do |m| define_method(m+"?") do self.send(m) end end def class? @is_class ||= @type == "Class" end def stage? @is_stage ||= @type.to_s.downcase == "stage" end # Construct a resource from data. # # Constructs a resource instance with the given `type` and `title`. Multiple # type signatures are possible for these arguments and most will result in an # expensive call to {Puppet::Node::Environment#known_resource_types} in order # to resolve `String` and `Symbol` Types to actual Ruby classes. # # @param type [Symbol, String] The name of the Puppet Type, as a string or # symbol. The actual Type will be looked up using # {Puppet::Node::Environment#known_resource_types}. This lookup is expensive. # @param type [String] The full resource name in the form of # `"Type[Title]"`. This method of calling should only be used when # `title` is `nil`. # @param type [nil] If a `nil` is passed, the title argument must be a string # of the form `"Type[Title]"`. # @param type [Class] A class that inherits from `Puppet::Type`. This method # of construction is much more efficient as it skips calls to # {Puppet::Node::Environment#known_resource_types}. # # @param title [String, :main, nil] The title of the resource. If type is `nil`, may also # be the full resource name in the form of `"Type[Title]"`. # # @api public def initialize(type, title = nil, attributes = {}) @parameters = {} environment = attributes[:environment] if type.is_a?(Class) && type < Puppet::Type # Set the resource type to avoid an expensive `known_resource_types` # lookup. self.resource_type = type # From this point on, the constructor behaves the same as if `type` had # been passed as a symbol. type = type.name end # Set things like strictness first. attributes.each do |attr, value| next if attr == :parameters send(attr.to_s + "=", value) end @type, @title = extract_type_and_title(type, title) @type = munge_type_name(@type) if self.class? @title = :main if @title == "" @title = munge_type_name(@title) end if params = attributes[:parameters] extract_parameters(params) end if resource_type && resource_type.respond_to?(:deprecate_params) resource_type.deprecate_params(title, attributes[:parameters]) end tag(self.type) tag(self.title) if valid_tag?(self.title) @reference = self # for serialization compatibility with 0.25.x if strict? and ! resource_type if self.class? raise ArgumentError, "Could not find declared class #{title}" else raise ArgumentError, "Invalid resource type #{type}" end end end def ref to_s end # Find our resource. def resolve catalog ? catalog.resource(to_s) : nil end # The resource's type implementation # @return [Puppet::Type, Puppet::Resource::Type] # @api private def resource_type @rstype ||= case type when "Class"; environment.known_resource_types.hostclass(title == :main ? "" : title) when "Node"; environment.known_resource_types.node(title) else Puppet::Type.type(type) || environment.known_resource_types.definition(type) end end # Set the resource's type implementation # @param type [Puppet::Type, Puppet::Resource::Type] # @api private def resource_type=(type) @rstype = type end def environment @environment ||= if catalog catalog.environment_instance else Puppet.lookup(:current_environment) { Puppet::Node::Environment::NONE } end end def environment=(environment) @environment = environment end # Produce a simple hash of our parameters. def to_hash parse_title.merge parameters end def to_s "#{type}[#{title}]" end def uniqueness_key # Temporary kludge to deal with inconsistant use patters h = self.to_hash h[namevar] ||= h[:name] h[:name] ||= h[namevar] h.values_at(*key_attributes.sort_by { |k| k.to_s }) end def key_attributes resource_type.respond_to?(:key_attributes) ? resource_type.key_attributes : [:name] end # Convert our resource to Puppet code. def to_manifest # Collect list of attributes to align => and move ensure first attr = parameters.keys attr_max = attr.inject(0) { |max,k| k.to_s.length > max ? k.to_s.length : max } attr.sort! if attr.first != :ensure && attr.include?(:ensure) attr.delete(:ensure) attr.unshift(:ensure) end attributes = attr.collect { |k| v = parameters[k] " %-#{attr_max}s => %s,\n" % [k, Puppet::Parameter.format_value_for_display(v)] }.join "%s { '%s':\n%s}" % [self.type.to_s.downcase, self.title, attributes] end def to_ref ref end # Convert our resource to a RAL resource instance. Creates component # instances for resource types that don't exist. def to_ral typeklass = Puppet::Type.type(self.type) || Puppet::Type.type(:component) typeklass.new(self) end def name # this is potential namespace conflict # between the notion of an "indirector name" # and a "resource name" [ type, title ].join('/') end def missing_arguments resource_type.arguments.select do |param, default| the_param = parameters[param.to_sym] the_param.nil? || the_param.value.nil? || the_param.value == :undef end end private :missing_arguments # Consult external data bindings for class parameter values which must be # namespaced in the backend. # # Example: # # class foo($port=0){ ... } # # We make a request to the backend for the key 'foo::port' not 'foo' # def lookup_external_default_for(param, scope) # Only lookup parameters for host classes return nil unless resource_type.type == :hostclass name = "#{resource_type.name}::#{param}" lookup_with_databinding(name, scope) end private :lookup_external_default_for def lookup_with_databinding(name, scope) begin Puppet::DataBinding.indirection.find( name, :environment => scope.environment, :variables => scope) rescue Puppet::DataBinding::LookupError => e raise Puppet::Error.new("Error from DataBinding '#{Puppet[:data_binding_terminus]}' while looking up '#{name}': #{e.message}", e) end end private :lookup_with_databinding def set_default_parameters(scope) return [] unless resource_type and resource_type.respond_to?(:arguments) unless is_a?(Puppet::Parser::Resource) fail Puppet::DevError, "Cannot evaluate default parameters for #{self} - not a parser resource" end missing_arguments.collect do |param, default| external_value = lookup_external_default_for(param, scope) if external_value.nil? && default.nil? next elsif external_value.nil? value = default.safeevaluate(scope) else value = external_value end self[param.to_sym] = value param end.compact end def copy_as_resource result = Puppet::Resource.new(type, title) result.file = self.file result.line = self.line result.exported = self.exported result.virtual = self.virtual result.tag(*self.tags) result.environment = environment result.instance_variable_set(:@rstype, resource_type) future_parser_not_in_use = !Puppet.future_parser?(result.environment) to_hash.each do |p, v| if v.is_a?(Puppet::Resource) v = Puppet::Resource.new(v.type, v.title) elsif v.is_a?(Array) # flatten resource references arrays v = v.flatten if v.flatten.find { |av| av.is_a?(Puppet::Resource) } v = v.collect do |av| av = Puppet::Resource.new(av.type, av.title) if av.is_a?(Puppet::Resource) av end end if future_parser_not_in_use # !Puppet.future_parser? # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. # # This behavior is not done in the future parser, but we can't issue a # deprecation warning either since there isn't anything that a user can # do about it. result[p] = if v.is_a?(Array) and v.length == 1 v[0] else v end else result[p] = v end end result end def valid_parameter?(name) resource_type.valid_parameter?(name) end # Verify that all required arguments are either present or # have been provided with defaults. # Must be called after 'set_default_parameters'. We can't join the methods # because Type#set_parameters needs specifically ordered behavior. def validate_complete return unless resource_type and resource_type.respond_to?(:arguments) resource_type.arguments.each do |param, default| param = param.to_sym fail Puppet::ParseError, "Must pass #{param} to #{self}" unless parameters.include?(param) end # Perform optional type checking if Puppet.future_parser? # Perform type checking arg_types = resource_type.argument_types # Parameters is a map from name, to parameter, and the parameter again has name and value parameters.each do |name, value| next unless t = arg_types[name.to_s] # untyped, and parameters are symbols here (aargh, strings in the type) unless Puppet::Pops::Types::TypeCalculator.instance?(t, value.value) inferred_type = Puppet::Pops::Types::TypeCalculator.infer_set(value.value) actual = Puppet::Pops::Types::TypeCalculator.generalize!(inferred_type) fail Puppet::ParseError, "Expected parameter '#{name}' of '#{self}' to have type #{t.to_s}, got #{actual.to_s}" end end end end def validate_parameter(name) raise ArgumentError, "Invalid parameter #{name}" unless valid_parameter?(name) end def prune_parameters(options = {}) properties = resource_type.properties.map(&:name) dup.collect do |attribute, value| if value.to_s.empty? or Array(value).empty? delete(attribute) elsif value.to_s == "absent" and attribute.to_s != "ensure" delete(attribute) end parameters_to_include = options[:parameters_to_include] || [] delete(attribute) unless properties.include?(attribute) || parameters_to_include.include?(attribute) end self end private # Produce a canonical method name. def parameter_name(param) param = param.to_s.downcase.to_sym if param == :name and namevar param = namevar end param end # The namevar for our resource type. If the type doesn't exist, # always use :name. def namevar if builtin_type? and t = resource_type and t.key_attributes.length == 1 t.key_attributes.first else :name end end def extract_parameters(params) params.each do |param, value| validate_parameter(param) if strict? self[param] = value end end def extract_type_and_title(argtype, argtitle) if (argtype.nil? || argtype == :component || argtype == :whit) && argtitle =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ] elsif argtitle.nil? && argtype =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ] elsif argtitle then [ argtype, argtitle ] elsif argtype.is_a?(Puppet::Type) then [ argtype.class.name, argtype.title ] elsif argtype.is_a?(Hash) then raise ArgumentError, "Puppet::Resource.new does not take a hash as the first argument. "+ "Did you mean (#{(argtype[:type] || argtype["type"]).inspect}, #{(argtype[:title] || argtype["title"]).inspect }) ?" else raise ArgumentError, "No title provided and #{argtype.inspect} is not a valid resource reference" end end def munge_type_name(value) return :main if value == :main return "Class" if value == "" or value.nil? or value.to_s.downcase == "component" value.to_s.split("::").collect { |s| s.capitalize }.join("::") end def parse_title h = {} type = resource_type if type.respond_to? :title_patterns type.title_patterns.each { |regexp, symbols_and_lambdas| if captures = regexp.match(title.to_s) symbols_and_lambdas.zip(captures[1..-1]).each do |symbol_and_lambda,capture| symbol, proc = symbol_and_lambda # Many types pass "identity" as the proc; we might as well give # them a shortcut to delivering that without the extra cost. # # Especially because the global type defines title_patterns and # uses the identity patterns. # # This was worth about 8MB of memory allocation saved in my # testing, so is worth the complexity for the API. if proc then h[symbol] = proc.call(capture) else h[symbol] = capture end end return h end } # If we've gotten this far, then none of the provided title patterns # matched. Since there's no way to determine the title then the # resource should fail here. raise Puppet::Error, "No set of title patterns matched the title \"#{title}\"." else return { :name => title.to_s } end end def parameters # @parameters could have been loaded from YAML, causing it to be nil (by # bypassing initialize). @parameters ||= {} end end puppet/external/nagios.rb000064400000001462147634041260011522 0ustar00# A script to retrieve hosts from ldap and create an importable # cfservd file from them require 'digest/md5' #require 'ldap' require 'puppet/external/nagios/parser.rb' require 'puppet/external/nagios/base.rb' module Nagios NAGIOSVERSION = '1.1' # yay colors PINK = "" GREEN = "" YELLOW = "" SLATE = "" ORANGE = "" BLUE = "" NOCOLOR = "" RESET = "" def self.version NAGIOSVERSION end class Config def Config.import(config) text = String.new File.open(config) { |file| file.each { |line| text += line } } parser = Nagios::Parser.new parser.parse(text) end def Config.each Nagios::Object.objects.each { |object| yield object } end end end puppet/external/pson/common.rb000064400000031766147634041260012523 0ustar00require 'puppet/external/pson/version' module PSON class << self # If _object_ is string-like parse the string and return the parsed result # as a Ruby data structure. Otherwise generate a PSON text from the Ruby # data structure object and return it. # # The _opts_ argument is passed through to generate/parse respectively, see # generate and parse for their documentation. def [](object, opts = {}) if object.respond_to? :to_str PSON.parse(object.to_str, opts => {}) else PSON.generate(object, opts => {}) end end # Returns the PSON parser class, that is used by PSON. This might be either # PSON::Ext::Parser or PSON::Pure::Parser. attr_reader :parser # Set the PSON parser class _parser_ to be used by PSON. def parser=(parser) # :nodoc: @parser = parser remove_const :Parser if const_defined? :Parser const_set :Parser, parser end def registered_document_types @registered_document_types ||= {} end # Register a class-constant for deserializaion. def register_document_type(name,klass) registered_document_types[name.to_s] = klass end # Return the constant located at _path_. # Anything may be registered as a path by calling register_path, above. # Otherwise, the format of _path_ has to be either ::A::B::C or A::B::C. # In either of these cases A has to be defined in Object (e.g. the path # must be an absolute namespace path. If the constant doesn't exist at # the given path, an ArgumentError is raised. def deep_const_get(path) # :nodoc: path = path.to_s registered_document_types[path] || path.split(/::/).inject(Object) do |p, c| case when c.empty? then p when p.const_defined?(c) then p.const_get(c) else raise ArgumentError, "can't find const for unregistered document type #{path}" end end end # Set the module _generator_ to be used by PSON. def generator=(generator) # :nodoc: @generator = generator generator_methods = generator::GeneratorMethods for const in generator_methods.constants klass = deep_const_get(const) modul = generator_methods.const_get(const) klass.class_eval do instance_methods(false).each do |m| m.to_s == 'to_pson' and remove_method m end include modul end end self.state = generator::State const_set :State, self.state end # Returns the PSON generator modul, that is used by PSON. This might be # either PSON::Ext::Generator or PSON::Pure::Generator. attr_reader :generator # Returns the PSON generator state class, that is used by PSON. This might # be either PSON::Ext::Generator::State or PSON::Pure::Generator::State. attr_accessor :state # This is create identifier, that is used to decide, if the _pson_create_ # hook of a class should be called. It defaults to 'document_type'. attr_accessor :create_id end self.create_id = 'document_type' NaN = (-1.0) ** 0.5 Infinity = 1.0/0 MinusInfinity = -Infinity # The base exception for PSON errors. class PSONError < StandardError; end # This exception is raised, if a parser error occurs. class ParserError < PSONError; end # This exception is raised, if the nesting of parsed datastructures is too # deep. class NestingError < ParserError; end # This exception is raised, if a generator or unparser error occurs. class GeneratorError < PSONError; end # For backwards compatibility UnparserError = GeneratorError # If a circular data structure is encountered while unparsing # this exception is raised. class CircularDatastructure < GeneratorError; end # This exception is raised, if the required unicode support is missing on the # system. Usually this means, that the iconv library is not installed. class MissingUnicodeSupport < PSONError; end module_function # Parse the PSON string _source_ into a Ruby data structure and return it. # # _opts_ can have the following # keys: # * *max_nesting*: The maximum depth of nesting allowed in the parsed data # structures. Disable depth checking with :max_nesting => false, it defaults # to 19. # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in # defiance of RFC 4627 to be parsed by the Parser. This option defaults # to false. # * *create_additions*: If set to false, the Parser doesn't create # additions even if a matching class and create_id was found. This option # defaults to true. def parse(source, opts = {}) PSON.parser.new(source, opts).parse end # Parse the PSON string _source_ into a Ruby data structure and return it. # The bang version of the parse method, defaults to the more dangerous values # for the _opts_ hash, so be sure only to parse trusted _source_ strings. # # _opts_ can have the following keys: # * *max_nesting*: The maximum depth of nesting allowed in the parsed data # structures. Enable depth checking with :max_nesting => anInteger. The parse! # methods defaults to not doing max depth checking: This can be dangerous, # if someone wants to fill up your stack. # * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in # defiance of RFC 4627 to be parsed by the Parser. This option defaults # to true. # * *create_additions*: If set to false, the Parser doesn't create # additions even if a matching class and create_id was found. This option # defaults to true. def parse!(source, opts = {}) opts = { :max_nesting => false, :allow_nan => true }.update(opts) PSON.parser.new(source, opts).parse end # Unparse the Ruby data structure _obj_ into a single line PSON string and # return it. _state_ is # * a PSON::State object, # * or a Hash like object (responding to to_hash), # * an object convertible into a hash by a to_h method, # that is used as or to configure a State object. # # It defaults to a state object, that creates the shortest possible PSON text # in one line, checks for circular data structures and doesn't allow NaN, # Infinity, and -Infinity. # # A _state_ hash can have the following keys: # * *indent*: a string used to indent levels (default: ''), # * *space*: a string that is put after, a : or , delimiter (default: ''), # * *space_before*: a string that is put before a : pair delimiter (default: ''), # * *object_nl*: a string that is put at the end of a PSON object (default: ''), # * *array_nl*: a string that is put at the end of a PSON array (default: ''), # * *check_circular*: true if checking for circular data structures # should be done (the default), false otherwise. # * *allow_nan*: true if NaN, Infinity, and -Infinity should be # generated, otherwise an exception is thrown, if these values are # encountered. This options defaults to false. # * *max_nesting*: The maximum depth of nesting allowed in the data # structures from which PSON is to be generated. Disable depth checking # with :max_nesting => false, it defaults to 19. # # See also the fast_generate for the fastest creation method with the least # amount of sanity checks, and the pretty_generate method for some # defaults for a pretty output. def generate(obj, state = nil) if state state = State.from_state(state) else state = State.new end obj.to_pson(state) end # :stopdoc: # I want to deprecate these later, so I'll first be silent about them, and # later delete them. alias unparse generate module_function :unparse # :startdoc: # Unparse the Ruby data structure _obj_ into a single line PSON string and # return it. This method disables the checks for circles in Ruby objects, and # also generates NaN, Infinity, and, -Infinity float values. # # *WARNING*: Be careful not to pass any Ruby data structures with circles as # _obj_ argument, because this will cause PSON to go into an infinite loop. def fast_generate(obj) obj.to_pson(nil) end # :stopdoc: # I want to deprecate these later, so I'll first be silent about them, and later delete them. alias fast_unparse fast_generate module_function :fast_unparse # :startdoc: # Unparse the Ruby data structure _obj_ into a PSON string and return it. The # returned string is a prettier form of the string returned by #unparse. # # The _opts_ argument can be used to configure the generator, see the # generate method for a more detailed explanation. def pretty_generate(obj, opts = nil) state = PSON.state.new( :indent => ' ', :space => ' ', :object_nl => "\n", :array_nl => "\n", :check_circular => true ) if opts if opts.respond_to? :to_hash opts = opts.to_hash elsif opts.respond_to? :to_h opts = opts.to_h else raise TypeError, "can't convert #{opts.class} into Hash" end state.configure(opts) end obj.to_pson(state) end # :stopdoc: # I want to deprecate these later, so I'll first be silent about them, and later delete them. alias pretty_unparse pretty_generate module_function :pretty_unparse # :startdoc: # Load a ruby data structure from a PSON _source_ and return it. A source can # either be a string-like object, an IO like object, or an object responding # to the read method. If _proc_ was given, it will be called with any nested # Ruby object as an argument recursively in depth first order. # # This method is part of the implementation of the load/dump interface of # Marshal and YAML. def load(source, proc = nil) if source.respond_to? :to_str source = source.to_str elsif source.respond_to? :to_io source = source.to_io.read else source = source.read end result = parse(source, :max_nesting => false, :allow_nan => true) recurse_proc(result, &proc) if proc result end def recurse_proc(result, &proc) case result when Array result.each { |x| recurse_proc x, &proc } proc.call result when Hash result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc } proc.call result else proc.call result end end private :recurse_proc module_function :recurse_proc alias restore load module_function :restore # Dumps _obj_ as a PSON string, i.e. calls generate on the object and returns # the result. # # If anIO (an IO like object or an object that responds to the write method) # was given, the resulting PSON is written to it. # # If the number of nested arrays or objects exceeds _limit_ an ArgumentError # exception is raised. This argument is similar (but not exactly the # same!) to the _limit_ argument in Marshal.dump. # # This method is part of the implementation of the load/dump interface of # Marshal and YAML. def dump(obj, anIO = nil, limit = nil) if anIO and limit.nil? anIO = anIO.to_io if anIO.respond_to?(:to_io) unless anIO.respond_to?(:write) limit = anIO anIO = nil end end limit ||= 0 result = generate(obj, :allow_nan => true, :max_nesting => limit) if anIO anIO.write result anIO else result end rescue PSON::NestingError raise ArgumentError, "exceed depth limit", $!.backtrace end # Provide a smarter wrapper for changing string encoding that works with # both Ruby 1.8 (iconv) and 1.9 (String#encode). Thankfully they seem to # have compatible input syntax, at least for the encodings we touch. if String.method_defined?("encode") def encode(to, from, string) string.encode(to, from) end else require 'iconv' def encode(to, from, string) Iconv.conv(to, from, string) end end end module ::Kernel private # Outputs _objs_ to STDOUT as PSON strings in the shortest form, that is in # one line. def j(*objs) objs.each do |obj| puts PSON::generate(obj, :allow_nan => true, :max_nesting => false) end nil end # Ouputs _objs_ to STDOUT as PSON strings in a pretty format, with # indentation and over many lines. def jj(*objs) objs.each do |obj| puts PSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false) end nil end # If _object_ is string-like parse the string and return the parsed result as # a Ruby data structure. Otherwise generate a PSON text from the Ruby data # structure object and return it. # # The _opts_ argument is passed through to generate/parse respectively, see # generate and parse for their documentation. def PSON(object, opts = {}) if object.respond_to? :to_str PSON.parse(object.to_str, opts) else PSON.generate(object, opts) end end end class ::Class # Returns true, if this class can be used to create an instance # from a serialised PSON string. The class has to implement a class # method _pson_create_ that expects a hash as first parameter, which includes # the required data. def pson_creatable? respond_to?(:pson_create) end end puppet/external/pson/pure.rb000064400000000611147634041260012167 0ustar00require 'puppet/external/pson/common' require 'puppet/external/pson/pure/parser' require 'puppet/external/pson/pure/generator' module PSON # This module holds all the modules/classes that implement PSON's # functionality in pure ruby. module Pure $DEBUG and warn "Using pure library for PSON." PSON.parser = Parser PSON.generator = Generator end PSON_LOADED = true end puppet/external/pson/pure/parser.rb000064400000025013147634041260013466 0ustar00require 'strscan' module PSON module Pure # This class implements the PSON parser that is used to parse a PSON string # into a Ruby data structure. class Parser < StringScanner STRING = /" ((?:[^\x0-\x1f"\\] | # escaped special characters: \\["\\\/bfnrt] | \\u[0-9a-fA-F]{4} | # match all but escaped special characters: \\[\x20-\x21\x23-\x2e\x30-\x5b\x5d-\x61\x63-\x65\x67-\x6d\x6f-\x71\x73\x75-\xff])*) "/nx INTEGER = /(-?0|-?[1-9]\d*)/ FLOAT = /(-? (?:0|[1-9]\d*) (?: \.\d+(?i:e[+-]?\d+) | \.\d+ | (?i:e[+-]?\d+) ) )/x NAN = /NaN/ INFINITY = /Infinity/ MINUS_INFINITY = /-Infinity/ OBJECT_OPEN = /\{/ OBJECT_CLOSE = /\}/ ARRAY_OPEN = /\[/ ARRAY_CLOSE = /\]/ PAIR_DELIMITER = /:/ COLLECTION_DELIMITER = /,/ TRUE = /true/ FALSE = /false/ NULL = /null/ IGNORE = %r( (?: //[^\n\r]*[\n\r]| # line comments /\* # c-style comments (?: [^*/]| # normal chars /[^*]| # slashes that do not start a nested comment \*[^/]| # asterisks that do not end this comment /(?=\*/) # single slash before this comment's end )* \*/ # the End of this comment |[ \t\r\n]+ # whitespaces: space, horicontal tab, lf, cr )+ )mx UNPARSED = Object.new # Creates a new PSON::Pure::Parser instance for the string _source_. # # It will be configured by the _opts_ hash. _opts_ can have the following # keys: # * *max_nesting*: The maximum depth of nesting allowed in the parsed data # structures. Disable depth checking with :max_nesting => false|nil|0, # it defaults to 19. # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in # defiance of RFC 4627 to be parsed by the Parser. This option defaults # to false. # * *create_additions*: If set to false, the Parser doesn't create # additions even if a matching class and create_id was found. This option # defaults to true. # * *object_class*: Defaults to Hash # * *array_class*: Defaults to Array def initialize(source, opts = {}) source = convert_encoding source super source if !opts.key?(:max_nesting) # defaults to 19 @max_nesting = 19 elsif opts[:max_nesting] @max_nesting = opts[:max_nesting] else @max_nesting = 0 end @allow_nan = !!opts[:allow_nan] ca = true ca = opts[:create_additions] if opts.key?(:create_additions) @create_id = ca ? PSON.create_id : nil @object_class = opts[:object_class] || Hash @array_class = opts[:array_class] || Array end alias source string # Parses the current PSON string _source_ and returns the complete data # structure as a result. def parse reset obj = nil until eos? case when scan(OBJECT_OPEN) obj and raise ParserError, "source '#{peek(20)}' not in PSON!" @current_nesting = 1 obj = parse_object when scan(ARRAY_OPEN) obj and raise ParserError, "source '#{peek(20)}' not in PSON!" @current_nesting = 1 obj = parse_array when skip(IGNORE) ; else raise ParserError, "source '#{peek(20)}' not in PSON!" end end obj or raise ParserError, "source did not contain any PSON!" obj end private def convert_encoding(source) if source.respond_to?(:to_str) source = source.to_str else raise TypeError, "#{source.inspect} is not like a string" end if supports_encodings?(source) if source.encoding == ::Encoding::ASCII_8BIT b = source[0, 4].bytes.to_a source = case when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0 source.dup.force_encoding(::Encoding::UTF_32BE).encode!(::Encoding::UTF_8) when b.size >= 4 && b[0] == 0 && b[2] == 0 source.dup.force_encoding(::Encoding::UTF_16BE).encode!(::Encoding::UTF_8) when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0 source.dup.force_encoding(::Encoding::UTF_32LE).encode!(::Encoding::UTF_8) when b.size >= 4 && b[1] == 0 && b[3] == 0 source.dup.force_encoding(::Encoding::UTF_16LE).encode!(::Encoding::UTF_8) else source.dup end else source = source.encode(::Encoding::UTF_8) end source.force_encoding(::Encoding::ASCII_8BIT) else b = source source = case when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0 PSON.encode('utf-8', 'utf-32be', b) when b.size >= 4 && b[0] == 0 && b[2] == 0 PSON.encode('utf-8', 'utf-16be', b) when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0 PSON.encode('utf-8', 'utf-32le', b) when b.size >= 4 && b[1] == 0 && b[3] == 0 PSON.encode('utf-8', 'utf-16le', b) else b end end source end def supports_encodings?(string) # Some modules, such as REXML on 1.8.7 (see #22804) can actually create # a top-level Encoding constant when they are misused. Therefore # checking for just that constant is not enough, so we'll be a bit more # robust about if we can actually support encoding transformations. string.respond_to?(:encoding) && defined?(::Encoding) end # Unescape characters in strings. UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr } UNESCAPE_MAP.update( { ?" => '"', ?\\ => '\\', ?/ => '/', ?b => "\b", ?f => "\f", ?n => "\n", ?r => "\r", ?t => "\t", ?u => nil, }) def parse_string if scan(STRING) return '' if self[1].empty? string = self[1].gsub(%r{(?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff])}n) do |c| if u = UNESCAPE_MAP[$MATCH[1]] u else # \uXXXX bytes = '' i = 0 while c[6 * i] == ?\\ && c[6 * i + 1] == ?u bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16) i += 1 end PSON.encode('utf-8', 'utf-16be', bytes) end end string.force_encoding(Encoding::UTF_8) if string.respond_to?(:force_encoding) string else UNPARSED end rescue => e raise GeneratorError, "Caught #{e.class}: #{e}", e.backtrace end def parse_value case when scan(FLOAT) Float(self[1]) when scan(INTEGER) Integer(self[1]) when scan(TRUE) true when scan(FALSE) false when scan(NULL) nil when (string = parse_string) != UNPARSED string when scan(ARRAY_OPEN) @current_nesting += 1 ary = parse_array @current_nesting -= 1 ary when scan(OBJECT_OPEN) @current_nesting += 1 obj = parse_object @current_nesting -= 1 obj when @allow_nan && scan(NAN) NaN when @allow_nan && scan(INFINITY) Infinity when @allow_nan && scan(MINUS_INFINITY) MinusInfinity else UNPARSED end end def parse_array raise NestingError, "nesting of #@current_nesting is too deep" if @max_nesting.nonzero? && @current_nesting > @max_nesting result = @array_class.new delim = false until eos? case when (value = parse_value) != UNPARSED delim = false result << value skip(IGNORE) if scan(COLLECTION_DELIMITER) delim = true elsif match?(ARRAY_CLOSE) ; else raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!" end when scan(ARRAY_CLOSE) raise ParserError, "expected next element in array at '#{peek(20)}'!" if delim break when skip(IGNORE) ; else raise ParserError, "unexpected token in array at '#{peek(20)}'!" end end result end def parse_object raise NestingError, "nesting of #@current_nesting is too deep" if @max_nesting.nonzero? && @current_nesting > @max_nesting result = @object_class.new delim = false until eos? case when (string = parse_string) != UNPARSED skip(IGNORE) raise ParserError, "expected ':' in object at '#{peek(20)}'!" unless scan(PAIR_DELIMITER) skip(IGNORE) unless (value = parse_value).equal? UNPARSED result[string] = value delim = false skip(IGNORE) if scan(COLLECTION_DELIMITER) delim = true elsif match?(OBJECT_CLOSE) ; else raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!" end else raise ParserError, "expected value in object at '#{peek(20)}'!" end when scan(OBJECT_CLOSE) raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!" if delim if @create_id and klassname = result[@create_id] klass = PSON.deep_const_get klassname break unless klass and klass.pson_creatable? result = klass.pson_create(result) end break when skip(IGNORE) ; else raise ParserError, "unexpected token in object at '#{peek(20)}'!" end end result end end end end puppet/external/pson/pure/generator.rb000064400000031727147634041260014171 0ustar00module PSON MAP = { "\x0" => '\u0000', "\x1" => '\u0001', "\x2" => '\u0002', "\x3" => '\u0003', "\x4" => '\u0004', "\x5" => '\u0005', "\x6" => '\u0006', "\x7" => '\u0007', "\b" => '\b', "\t" => '\t', "\n" => '\n', "\xb" => '\u000b', "\f" => '\f', "\r" => '\r', "\xe" => '\u000e', "\xf" => '\u000f', "\x10" => '\u0010', "\x11" => '\u0011', "\x12" => '\u0012', "\x13" => '\u0013', "\x14" => '\u0014', "\x15" => '\u0015', "\x16" => '\u0016', "\x17" => '\u0017', "\x18" => '\u0018', "\x19" => '\u0019', "\x1a" => '\u001a', "\x1b" => '\u001b', "\x1c" => '\u001c', "\x1d" => '\u001d', "\x1e" => '\u001e', "\x1f" => '\u001f', '"' => '\"', '\\' => '\\\\', } # :nodoc: # Convert a UTF8 encoded Ruby string _string_ to a PSON string, encoded with # UTF16 big endian characters as \u????, and return it. if String.method_defined?(:force_encoding) def utf8_to_pson(string) # :nodoc: string = string.dup string << '' # XXX workaround: avoid buffer sharing string.force_encoding(Encoding::ASCII_8BIT) string.gsub!(/["\\\x0-\x1f]/) { MAP[$MATCH] } string rescue => e raise GeneratorError, "Caught #{e.class}: #{e}", e.backtrace end else def utf8_to_pson(string) # :nodoc: string.gsub(/["\\\x0-\x1f]/n) { MAP[$MATCH] } end end module_function :utf8_to_pson module Pure module Generator # This class is used to create State instances, that are use to hold data # while generating a PSON text from a Ruby data structure. class State # Creates a State object from _opts_, which ought to be Hash to create # a new State instance configured by _opts_, something else to create # an unconfigured instance. If _opts_ is a State object, it is just # returned. def self.from_state(opts) case opts when self opts when Hash new(opts) else new end end # Instantiates a new State object, configured by _opts_. # # _opts_ can have the following keys: # # * *indent*: a string used to indent levels (default: ''), # * *space*: a string that is put after, a : or , delimiter (default: ''), # * *space_before*: a string that is put before a : pair delimiter (default: ''), # * *object_nl*: a string that is put at the end of a PSON object (default: ''), # * *array_nl*: a string that is put at the end of a PSON array (default: ''), # * *check_circular*: true if checking for circular data structures # should be done (the default), false otherwise. # * *check_circular*: true if checking for circular data structures # should be done, false (the default) otherwise. # * *allow_nan*: true if NaN, Infinity, and -Infinity should be # generated, otherwise an exception is thrown, if these values are # encountered. This options defaults to false. def initialize(opts = {}) @seen = {} @indent = '' @space = '' @space_before = '' @object_nl = '' @array_nl = '' @check_circular = true @allow_nan = false configure opts end # This string is used to indent levels in the PSON text. attr_accessor :indent # This string is used to insert a space between the tokens in a PSON # string. attr_accessor :space # This string is used to insert a space before the ':' in PSON objects. attr_accessor :space_before # This string is put at the end of a line that holds a PSON object (or # Hash). attr_accessor :object_nl # This string is put at the end of a line that holds a PSON array. attr_accessor :array_nl # This integer returns the maximum level of data structure nesting in # the generated PSON, max_nesting = 0 if no maximum is checked. attr_accessor :max_nesting def check_max_nesting(depth) # :nodoc: return if @max_nesting.zero? current_nesting = depth + 1 current_nesting > @max_nesting and raise NestingError, "nesting of #{current_nesting} is too deep" end # Returns true, if circular data structures should be checked, # otherwise returns false. def check_circular? @check_circular end # Returns true if NaN, Infinity, and -Infinity should be considered as # valid PSON and output. def allow_nan? @allow_nan end # Returns _true_, if _object_ was already seen during this generating # run. def seen?(object) @seen.key?(object.__id__) end # Remember _object_, to find out if it was already encountered (if a # cyclic data structure is if a cyclic data structure is rendered). def remember(object) @seen[object.__id__] = true end # Forget _object_ for this generating run. def forget(object) @seen.delete object.__id__ end # Configure this State instance with the Hash _opts_, and return # itself. def configure(opts) @indent = opts[:indent] if opts.key?(:indent) @space = opts[:space] if opts.key?(:space) @space_before = opts[:space_before] if opts.key?(:space_before) @object_nl = opts[:object_nl] if opts.key?(:object_nl) @array_nl = opts[:array_nl] if opts.key?(:array_nl) @check_circular = !!opts[:check_circular] if opts.key?(:check_circular) @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan) if !opts.key?(:max_nesting) # defaults to 19 @max_nesting = 19 elsif opts[:max_nesting] @max_nesting = opts[:max_nesting] else @max_nesting = 0 end self end # Returns the configuration instance variables as a hash, that can be # passed to the configure method. def to_h result = {} for iv in %w{indent space space_before object_nl array_nl check_circular allow_nan max_nesting} result[iv.intern] = instance_variable_get("@#{iv}") end result end end module GeneratorMethods module Object # Converts this object to a string (calling #to_s), converts # it to a PSON string, and returns the result. This is a fallback, if no # special method #to_pson was defined for some object. def to_pson(*) to_s.to_pson end end module Hash # Returns a PSON string containing a PSON object, that is unparsed from # this Hash instance. # _state_ is a PSON::State object, that can also be used to configure the # produced PSON string output further. # _depth_ is used to find out nesting depth, to indent accordingly. def to_pson(state = nil, depth = 0, *) if state state = PSON.state.from_state(state) state.check_max_nesting(depth) pson_check_circular(state) { pson_transform(state, depth) } else pson_transform(state, depth) end end private def pson_check_circular(state) if state and state.check_circular? state.seen?(self) and raise PSON::CircularDatastructure, "circular data structures not supported!" state.remember self end yield ensure state and state.forget self end def pson_shift(state, depth) state and not state.object_nl.empty? or return '' state.indent * depth end def pson_transform(state, depth) delim = ',' if state delim << state.object_nl result = '{' result << state.object_nl result << map { |key,value| s = pson_shift(state, depth + 1) s << key.to_s.to_pson(state, depth + 1) s << state.space_before s << ':' s << state.space s << value.to_pson(state, depth + 1) }.join(delim) result << state.object_nl result << pson_shift(state, depth) result << '}' else result = '{' result << map { |key,value| key.to_s.to_pson << ':' << value.to_pson }.join(delim) result << '}' end result end end module Array # Returns a PSON string containing a PSON array, that is unparsed from # this Array instance. # _state_ is a PSON::State object, that can also be used to configure the # produced PSON string output further. # _depth_ is used to find out nesting depth, to indent accordingly. def to_pson(state = nil, depth = 0, *) if state state = PSON.state.from_state(state) state.check_max_nesting(depth) pson_check_circular(state) { pson_transform(state, depth) } else pson_transform(state, depth) end end private def pson_check_circular(state) if state and state.check_circular? state.seen?(self) and raise PSON::CircularDatastructure, "circular data structures not supported!" state.remember self end yield ensure state and state.forget self end def pson_shift(state, depth) state and not state.array_nl.empty? or return '' state.indent * depth end def pson_transform(state, depth) delim = ',' if state delim << state.array_nl result = '[' result << state.array_nl result << map { |value| pson_shift(state, depth + 1) << value.to_pson(state, depth + 1) }.join(delim) result << state.array_nl result << pson_shift(state, depth) result << ']' else '[' << map { |value| value.to_pson }.join(delim) << ']' end end end module Integer # Returns a PSON string representation for this Integer number. def to_pson(*) to_s end end module Float # Returns a PSON string representation for this Float number. def to_pson(state = nil, *) if infinite? || nan? if !state || state.allow_nan? to_s else raise GeneratorError, "#{self} not allowed in PSON" end else to_s end end end module String # This string should be encoded with UTF-8 A call to this method # returns a PSON string encoded with UTF16 big endian characters as # \u????. def to_pson(*) '"' << PSON.utf8_to_pson(self) << '"' end # Module that holds the extinding methods if, the String module is # included. module Extend # Raw Strings are PSON Objects (the raw bytes are stored in an array for the # key "raw"). The Ruby String can be created by this module method. def pson_create(o) o['raw'].pack('C*') end end # Extends _modul_ with the String::Extend module. def self.included(modul) modul.extend Extend end # This method creates a raw object hash, that can be nested into # other data structures and will be unparsed as a raw string. This # method should be used, if you want to convert raw strings to PSON # instead of UTF-8 strings, e.g. binary data. def to_pson_raw_object { PSON.create_id => self.class.name, 'raw' => self.unpack('C*'), } end # This method creates a PSON text from the result of # a call to to_pson_raw_object of this String. def to_pson_raw(*args) to_pson_raw_object.to_pson(*args) end end module TrueClass # Returns a PSON string for true: 'true'. def to_pson(*) 'true' end end module FalseClass # Returns a PSON string for false: 'false'. def to_pson(*) 'false' end end module NilClass # Returns a PSON string for nil: 'null'. def to_pson(*) 'null' end end end end end end puppet/external/pson/version.rb000064400000000417147634041260012705 0ustar00module PSON # PSON version VERSION = '1.1.9' VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc: VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc: VERSION_MINOR = VERSION_ARRAY[1] # :nodoc: VERSION_BUILD = VERSION_ARRAY[2] # :nodoc: end puppet/external/nagios/parser.rb000064400000021724147634041260013021 0ustar00# # DO NOT MODIFY!!!! # This file is automatically generated by Racc 1.4.9 # from Racc grammer file "". # require 'racc/parser.rb' module Nagios class Parser < Racc::Parser module_eval(<<'...end grammar.ry/module_eval...', 'grammar.ry', 50) require 'strscan' class ::Nagios::Parser::SyntaxError < RuntimeError; end def parse(src) if src.respond_to?("force_encoding") then src.force_encoding("ASCII-8BIT") end @ss = StringScanner.new(src) # state variables @in_parameter_value = false @in_object_definition = false @done = false @line = 1 @yydebug = true do_parse end # This tokenizes the outside of object definitions, # and detects when we start defining an object. # We ignore whitespaces, comments and inline comments. # We yield when finding newlines, the "define" keyword, # the object name and the opening curly bracket. def tokenize_outside_definitions case when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ ; when (text = @ss.scan(/\#.*$/)) # ignore comments ; when (text = @ss.scan(/;.*$/)) # ignore inline comments ; when (text = @ss.scan(/\n/)) # newline [:RETURN, text] when (text = @ss.scan(/\b(define)\b/)) # the "define" keyword [:DEFINE, text] when (text = @ss.scan(/[^{ \t\n]+/)) # the name of the object being defined (everything not an opening curly bracket or a separator) [:NAME, text] when (text = @ss.scan(/\{/)) # the opening curly bracket - we enter object definition @in_object_definition = true [:LCURLY, text] else text = @ss.string[@ss.pos .. -1] raise ScanError, "can not match: '#{text}'" end # case end # This tokenizes until we find the parameter name. def tokenize_parameter_name case when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ ; when (text = @ss.scan(/\#.*$/)) # ignore comments ; when (text = @ss.scan(/;.*$/)) # ignore inline comments ; when (text = @ss.scan(/\n/)) # newline [:RETURN, text] when (text = @ss.scan(/\}/)) # closing curly bracket : end of definition @in_object_definition = false [:RCURLY, text] when (not @in_parameter_value and (text = @ss.scan(/\S+/))) # This is the name of the parameter @in_parameter_value = true [:PARAM, text] else text = @ss.string[@ss.pos .. -1] raise ScanError, "can not match: '#{text}'" end # case end # This tokenizes the parameter value. # There is a special handling for lines containing semicolons : # - unescaped semicolons are line comments (and should stop parsing of the line) # - escaped (with backslash \) semicolons should be kept in the parameter value (without the backslash) def tokenize_parameter_value case when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ ; when (text = @ss.scan(/\#.*$/)) # ignore comments ; when (text = @ss.scan(/\n/)) # newline @in_parameter_value = false [:RETURN, text] when (text = @ss.scan(/.+$/)) # Value of parameter @in_parameter_value = false # Special handling of inline comments (;) and escaped semicolons (\;) # We split the string on escaped semicolons (\;), # Then we rebuild it as long as there are no inline comments (;) # We join the rebuilt string with unescaped semicolons (on purpose) array = text.split('\;', 0) text = "" array.each do |elt| # Now we split at inline comments. If we have more than 1 element in the array # it means we have an inline comment, so we are able to stop parsing # However we still want to reconstruct the string with its first part (before the comment) linearray = elt.split(';', 0) # Let's reconstruct the string with a (unescaped) semicolon if text != "" then text += ';' end text += linearray[0] # Now we can stop if linearray.length > 1 then break end end # We strip the text to remove spaces between end of string and beginning of inline comment [:VALUE, text.strip] else text = @ss.string[@ss.pos .. -1] raise ScanError, "can not match: '#{text}'" end # case end # This tokenizes inside an object definition. # Two cases : parameter name and parameter value def tokenize_inside_definitions if @in_parameter_value tokenize_parameter_value else tokenize_parameter_name end end # The lexer. Very simple. def token text = @ss.peek(1) @line += 1 if text == "\n" token = if @in_object_definition tokenize_inside_definitions else tokenize_outside_definitions end token end def next_token return if @ss.eos? # skips empty actions until _next_token = token or @ss.eos?; end _next_token end def yydebug 1 end def yywrap 0 end def on_error(token, value, vstack ) # text = @ss.string[@ss.pos .. -1] text = @ss.peek(20) msg = "" unless value.nil? msg = "line #{@line}: syntax error at value '#{value}' : #{text}" else msg = "line #{@line}: syntax error at token '#{token}' : #{text}" end if @ss.eos? msg = "line #{@line}: Unexpected end of file" end if token == '$end'.intern puts "okay, this is silly" else raise ::Nagios::Parser::SyntaxError, msg end end ...end grammar.ry/module_eval... ##### State transition tables begin ### racc_action_table = [ 8, 3, 3, 14, 12, 18, 10, 4, 4, 20, 12, 14, 12, 9, 6, 12 ] racc_action_check = [ 5, 0, 5, 13, 9, 13, 8, 0, 5, 14, 14, 11, 12, 6, 3, 20 ] racc_action_pointer = [ -1, nil, nil, 11, nil, 0, 8, nil, 6, -4, nil, 7, 4, -1, 2, nil, nil, nil, nil, nil, 7, nil ] racc_action_default = [ -12, -1, -3, -12, -4, -12, -12, -2, -12, -12, 22, -12, -10, -12, -12, -6, -11, -7, -5, -9, -12, -8 ] racc_goto_table = [ 11, 1, 15, 16, 17, 19, 7, 13, 5, nil, nil, 21 ] racc_goto_check = [ 4, 2, 6, 4, 6, 4, 2, 5, 1, nil, nil, 4 ] racc_goto_pointer = [ nil, 8, 1, nil, -9, -4, -9 ] racc_goto_default = [ nil, nil, nil, 2, nil, nil, nil ] racc_reduce_table = [ 0, 0, :racc_error, 1, 10, :_reduce_1, 2, 10, :_reduce_2, 1, 11, :_reduce_3, 1, 11, :_reduce_4, 6, 12, :_reduce_5, 1, 14, :_reduce_none, 2, 14, :_reduce_7, 3, 15, :_reduce_8, 2, 15, :_reduce_9, 1, 13, :_reduce_none, 2, 13, :_reduce_none ] racc_reduce_n = 12 racc_shift_n = 22 racc_token_table = { false => 0, :error => 1, :DEFINE => 2, :NAME => 3, :PARAM => 4, :LCURLY => 5, :RCURLY => 6, :VALUE => 7, :RETURN => 8 } racc_nt_base = 9 racc_use_result_var = true Racc_arg = [ racc_action_table, racc_action_check, racc_action_default, racc_action_pointer, racc_goto_table, racc_goto_check, racc_goto_default, racc_goto_pointer, racc_nt_base, racc_reduce_table, racc_token_table, racc_shift_n, racc_reduce_n, racc_use_result_var ] Racc_token_to_s_table = [ "$end", "error", "DEFINE", "NAME", "PARAM", "LCURLY", "RCURLY", "VALUE", "RETURN", "$start", "decls", "decl", "object", "returns", "vars", "var" ] Racc_debug_parser = false ##### State transition tables end ##### # reduce 0 omitted module_eval(<<'.,.,', 'grammar.ry', 6) def _reduce_1(val, _values, result) return val[0] if val[0] result end .,., module_eval(<<'.,.,', 'grammar.ry', 8) def _reduce_2(val, _values, result) if val[1].nil? result = val[0] else if val[0].nil? result = val[1] else result = [ val[0], val[1] ].flatten end end result end .,., module_eval(<<'.,.,', 'grammar.ry', 20) def _reduce_3(val, _values, result) result = [val[0]] result end .,., module_eval(<<'.,.,', 'grammar.ry', 21) def _reduce_4(val, _values, result) result = nil result end .,., module_eval(<<'.,.,', 'grammar.ry', 25) def _reduce_5(val, _values, result) result = Nagios::Base.create(val[1],val[4]) result end .,., # reduce 6 omitted module_eval(<<'.,.,', 'grammar.ry', 31) def _reduce_7(val, _values, result) val[1].each {|p,v| val[0][p] = v } result = val[0] result end .,., module_eval(<<'.,.,', 'grammar.ry', 38) def _reduce_8(val, _values, result) result = {val[0] => val[1]} result end .,., module_eval(<<'.,.,', 'grammar.ry', 39) def _reduce_9(val, _values, result) result = {val[0] => "" } result end .,., # reduce 10 omitted # reduce 11 omitted def _reduce_none(val, _values, result) val[0] end end # class Parser end # module Nagios puppet/external/nagios/base.rb000064400000030053147634041260012432 0ustar00# The base class for all of our Nagios object types. Everything else # is mostly just data. class Nagios::Base class UnknownNagiosType < RuntimeError # When an unknown type is asked for by name. end include Enumerable class << self attr_accessor :parameters, :derivatives, :ocs, :name, :att attr_accessor :ldapbase attr_writer :namevar attr_reader :superior end # Attach one class to another. def self.attach(hash) @attach ||= {} hash.each do |n, v| @attach[n] = v end end # Convert a parameter to camelcase def self.camelcase(param) param.gsub(/_./) do |match| match.sub(/_/,'').capitalize end end # Uncamelcase a parameter. def self.decamelcase(param) param.gsub(/[A-Z]/) do |match| "_#{match.downcase}" end end # Create a new instance of a given class. def self.create(name, args = {}) name = name.intern if name.is_a? String if @types.include?(name) @types[name].new(args) else raise UnknownNagiosType, "Unknown type #{name}" end end # Yield each type in turn. def self.eachtype @types.each do |name, type| yield [name, type] end end # Create a mapping. def self.map(hash) @map ||= {} hash.each do |n, v| @map[n] = v end end # Return a mapping (or nil) for a param def self.mapping(name) name = name.intern if name.is_a? String if defined?(@map) @map[name] else nil end end # Return the namevar for the canonical name. def self.namevar if defined?(@namevar) return @namevar else if parameter?(:name) return :name elsif tmp = (self.name.to_s + "_name").intern and parameter?(tmp) @namevar = tmp return @namevar else raise "Type #{self.name} has no name var" end end end # Create a new type. def self.newtype(name, &block) name = name.intern if name.is_a? String @types ||= {} # Create the class, with the correct name. t = Class.new(self) t.name = name # Everyone gets this. There should probably be a better way, and I # should probably hack the attribute system to look things up based on # this "use" setting, but, eh. t.parameters = [:use] const_set(name.to_s.capitalize,t) # Evaluate the passed block. This should usually define all of the work. t.class_eval(&block) @types[name] = t end # Define both the normal case and camelcase method for a parameter def self.paramattr(name) camel = camelcase(name) param = name [name, camel].each do |method| define_method(method) do @parameters[param] end define_method(method.to_s + "=") do |value| @parameters[param] = value end end end # Is the specified name a valid parameter? def self.parameter?(name) name = name.intern if name.is_a? String @parameters.include?(name) end # Manually set the namevar def self.setnamevar(name) name = name.intern if name.is_a? String @namevar = name end # Set the valid parameters for this class def self.setparameters(*array) @parameters += array end # Set the superior ldap object class. Seems silly to include this # in this class, but, eh. def self.setsuperior(name) @superior = name end # Parameters to suppress in output. def self.suppress(name) @suppress ||= [] @suppress << name end # Whether a given parameter is suppressed. def self.suppress?(name) defined?(@suppress) and @suppress.include?(name) end # Return our name as the string. def self.to_s self.name.to_s end # Return a type by name. def self.type(name) name = name.intern if name.is_a? String @types[name] end # Convenience methods. def [](param) send(param) end # Convenience methods. def []=(param,value) send(param.to_s + "=", value) end # Iterate across all ofour set parameters. def each @parameters.each { |param,value| yield(param,value) } end # Initialize our object, optionally with a list of parameters. def initialize(args = {}) @parameters = {} args.each { |param,value| self[param] = value } if @namevar == :_naginator_name self['_naginator_name'] = self['name'] end end # Handle parameters like attributes. def method_missing(mname, *args) pname = mname.to_s pname.sub!(/=/, '') if self.class.parameter?(pname) if pname =~ /A-Z/ pname = self.class.decamelcase(pname) end self.class.paramattr(pname) # Now access the parameters directly, to make it at least less # likely we'll end up in an infinite recursion. if mname.to_s =~ /=$/ @parameters[pname] = args.first else return @parameters[mname] end else super end end # Retrieve our name, through a bit of redirection. def name send(self.class.namevar) end # This is probably a bad idea. def name=(value) unless self.class.namevar.to_s == "name" send(self.class.namevar.to_s + "=", value) end end def namevar (self.type + "_name").intern end def parammap(param) unless defined?(@map) map = { self.namevar => "cn" } map.update(self.class.map) if self.class.map end if map.include?(param) return map[param] else return "nagios-" + param.id2name.gsub(/_/,'-') end end def parent unless defined?(self.class.attached) puts "Duh, you called parent on an unattached class" return end klass,param = self.class.attached unless @parameters.include?(param) puts "Huh, no attachment param" return end klass[@parameters[param]] end # okay, this sucks # how do i get my list of ocs? def to_ldif str = self.dn + "\n" ocs = Array.new if self.class.ocs # i'm storing an array, so i have to flatten it and stuff kocs = self.class.ocs ocs.push(*kocs) end ocs.push "top" oc = self.class.to_s oc.sub!(/Nagios/,'nagios') oc.sub!(/::/,'') ocs.push oc ocs.each { |oc| str += "objectclass: #{oc}\n" } @parameters.each { |name,value| next if self.class.suppress.include?(name) ldapname = self.parammap(name) str += ldapname + ": #{value}\n" } str += "\n" end def to_s str = "define #{self.type} {\n" @parameters.keys.sort.each { |param| value = @parameters[param] str += %{\t%-30s %s\n} % [ param, if value.is_a? Array value.join(",").sub(';', '\;') else value.to_s.sub(';', '\;') end ] } str += "}\n" str end # The type of object we are. def type self.class.name end # object types newtype :host do setparameters :host_name, :alias, :display_name, :address, :parents, :hostgroups, :check_command, :initial_state, :max_check_attempts, :check_interval, :retry_interval, :active_checks_enabled, :passive_checks_enabled, :check_period, :obsess_over_host, :check_freshness, :freshness_threshold, :event_handler, :event_handler_enabled, :low_flap_threshold, :high_flap_threshold, :flap_detection_enabled, :flap_detection_options, :failure_prediction_enabled, :process_perf_data, :retain_status_information, :retain_nonstatus_information, :contacts, :contact_groups, :notification_interval, :first_notification_delay, :notification_period, :notification_options, :notifications_enabled, :stalking_options, :notes, :notes_url, :action_url, :icon_image, :icon_image_alt, :vrml_image, :statusmap_image, "2d_coords".intern, "3d_coords".intern, :register, :use, :realm, :poller_tag, :business_impact setsuperior "person" map :address => "ipHostNumber" end newtype :hostgroup do setparameters :hostgroup_name, :alias, :members, :hostgroup_members, :notes, :notes_url, :action_url, :register, :use, :realm end newtype :service do attach :host => :host_name setparameters :host_name, :hostgroup_name, :service_description, :display_name, :servicegroups, :is_volatile, :check_command, :initial_state, :max_check_attempts, :check_interval, :retry_interval, :normal_check_interval, :retry_check_interval, :active_checks_enabled, :passive_checks_enabled, :parallelize_check, :check_period, :obsess_over_service, :check_freshness, :freshness_threshold, :event_handler, :event_handler_enabled, :low_flap_threshold, :high_flap_threshold, :flap_detection_enabled,:flap_detection_options, :process_perf_data, :failure_prediction_enabled, :retain_status_information, :retain_nonstatus_information, :notification_interval, :first_notification_delay, :notification_period, :notification_options, :notifications_enabled, :contacts, :contact_groups, :stalking_options, :notes, :notes_url, :action_url, :icon_image, :icon_image_alt, :register, :use, :_naginator_name, :poller_tag, :business_impact suppress :host_name setnamevar :_naginator_name end newtype :servicegroup do setparameters :servicegroup_name, :alias, :members, :servicegroup_members, :notes, :notes_url, :action_url, :register, :use end newtype :contact do setparameters :contact_name, :alias, :contactgroups, :host_notifications_enabled, :service_notifications_enabled, :host_notification_period, :service_notification_period, :host_notification_options, :service_notification_options, :host_notification_commands, :service_notification_commands, :email, :pager, :address1, :address2, :address3, :address4, :address5, :address6, :can_submit_commands, :retain_status_information, :retain_nonstatus_information, :register, :use setsuperior "person" end newtype :contactgroup do setparameters :contactgroup_name, :alias, :members, :contactgroup_members, :register, :use end # TODO - We should support generic time periods here eg "day 1 - 15" newtype :timeperiod do setparameters :timeperiod_name, :alias, :sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :exclude, :register, :use end newtype :command do setparameters :command_name, :command_line, :poller_tag end newtype :servicedependency do setparameters :dependent_host_name, :dependent_hostgroup_name, :dependent_service_description, :host_name, :hostgroup_name, :service_description, :inherits_parent, :execution_failure_criteria, :notification_failure_criteria, :dependency_period, :register, :use, :_naginator_name setnamevar :_naginator_name end newtype :serviceescalation do setparameters :host_name, :hostgroup_name, :servicegroup_name, :service_description, :contacts, :contact_groups, :first_notification, :last_notification, :notification_interval, :escalation_period, :escalation_options, :register, :use, :_naginator_name setnamevar :_naginator_name end newtype :hostdependency do setparameters :dependent_host_name, :dependent_hostgroup_name, :host_name, :hostgroup_name, :inherits_parent, :execution_failure_criteria, :notification_failure_criteria, :dependency_period, :register, :use, :_naginator_name setnamevar :_naginator_name end newtype :hostescalation do setparameters :host_name, :hostgroup_name, :contacts, :contact_groups, :first_notification, :last_notification, :notification_interval, :escalation_period, :escalation_options, :register, :use, :_naginator_name setnamevar :_naginator_name end newtype :hostextinfo do setparameters :host_name, :notes, :notes_url, :icon_image, :icon_image_alt, :vrml_image, :statusmap_image, "2d_coords".intern, "3d_coords".intern, :register, :use setnamevar :host_name end newtype :serviceextinfo do setparameters :host_name, :service_description, :notes, :notes_url, :action_url, :icon_image, :icon_image_alt, :register, :use, :_naginator_name setnamevar :_naginator_name end end puppet/external/nagios/makefile000064400000000247147634041260012675 0ustar00all: parser.rb debug: parser.rb setdebug parser.rb: grammar.ry racc -oparser.rb grammar.ry setdebug: perl -pi -e 's{\@yydebug =.*$$}{\@yydebug = true}' parser.rb puppet/external/nagios/grammar.ry000064400000013736147634041260013206 0ustar00# vim: syntax=ruby class Nagios::Parser token DEFINE NAME PARAM LCURLY RCURLY VALUE RETURN rule decls: decl { return val[0] if val[0] } | decls decl { if val[1].nil? result = val[0] else if val[0].nil? result = val[1] else result = [ val[0], val[1] ].flatten end end } ; decl: object { result = [val[0]] } | RETURN { result = nil } ; object: DEFINE NAME LCURLY returns vars RCURLY { result = Nagios::Base.create(val[1],val[4]) } ; vars: var | vars var { val[1].each {|p,v| val[0][p] = v } result = val[0] } ; var: PARAM VALUE returns { result = {val[0] => val[1]} } | PARAM returns { result = {val[0] => "" } } ; returns: RETURN | RETURN returns ; end ----inner require 'strscan' class ::Nagios::Parser::SyntaxError < RuntimeError; end def parse(src) if src.respond_to?("force_encoding") then src.force_encoding("ASCII-8BIT") end @ss = StringScanner.new(src) # state variables @in_parameter_value = false @in_object_definition = false @done = false @line = 1 @yydebug = true do_parse end # This tokenizes the outside of object definitions, # and detects when we start defining an object. # We ignore whitespaces, comments and inline comments. # We yield when finding newlines, the "define" keyword, # the object name and the opening curly bracket. def tokenize_outside_definitions case when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ ; when (text = @ss.scan(/\#.*$/)) # ignore comments ; when (text = @ss.scan(/;.*$/)) # ignore inline comments ; when (text = @ss.scan(/\n/)) # newline [:RETURN, text] when (text = @ss.scan(/\b(define)\b/)) # the "define" keyword [:DEFINE, text] when (text = @ss.scan(/[^{ \t\n]+/)) # the name of the object being defined (everything not an opening curly bracket or a separator) [:NAME, text] when (text = @ss.scan(/\{/)) # the opening curly bracket - we enter object definition @in_object_definition = true [:LCURLY, text] else text = @ss.string[@ss.pos .. -1] raise ScanError, "can not match: '#{text}'" end # case end # This tokenizes until we find the parameter name. def tokenize_parameter_name case when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ ; when (text = @ss.scan(/\#.*$/)) # ignore comments ; when (text = @ss.scan(/;.*$/)) # ignore inline comments ; when (text = @ss.scan(/\n/)) # newline [:RETURN, text] when (text = @ss.scan(/\}/)) # closing curly bracket : end of definition @in_object_definition = false [:RCURLY, text] when (not @in_parameter_value and (text = @ss.scan(/\S+/))) # This is the name of the parameter @in_parameter_value = true [:PARAM, text] else text = @ss.string[@ss.pos .. -1] raise ScanError, "can not match: '#{text}'" end # case end # This tokenizes the parameter value. # There is a special handling for lines containing semicolons : # - unescaped semicolons are line comments (and should stop parsing of the line) # - escaped (with backslash \) semicolons should be kept in the parameter value (without the backslash) def tokenize_parameter_value case when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ ; when (text = @ss.scan(/\#.*$/)) # ignore comments ; when (text = @ss.scan(/\n/)) # newline @in_parameter_value = false [:RETURN, text] when (text = @ss.scan(/.+$/)) # Value of parameter @in_parameter_value = false # Special handling of inline comments (;) and escaped semicolons (\;) # We split the string on escaped semicolons (\;), # Then we rebuild it as long as there are no inline comments (;) # We join the rebuilt string with unescaped semicolons (on purpose) array = text.split('\;', 0) text = "" array.each do |elt| # Now we split at inline comments. If we have more than 1 element in the array # it means we have an inline comment, so we are able to stop parsing # However we still want to reconstruct the string with its first part (before the comment) linearray = elt.split(';', 0) # Let's reconstruct the string with a (unescaped) semicolon if text != "" then text += ';' end text += linearray[0] # Now we can stop if linearray.length > 1 then break end end # We strip the text to remove spaces between end of string and beginning of inline comment [:VALUE, text.strip] else text = @ss.string[@ss.pos .. -1] raise ScanError, "can not match: '#{text}'" end # case end # This tokenizes inside an object definition. # Two cases : parameter name and parameter value def tokenize_inside_definitions if @in_parameter_value tokenize_parameter_value else tokenize_parameter_name end end # The lexer. Very simple. def token text = @ss.peek(1) @line += 1 if text == "\n" token = if @in_object_definition tokenize_inside_definitions else tokenize_outside_definitions end token end def next_token return if @ss.eos? # skips empty actions until _next_token = token or @ss.eos?; end _next_token end def yydebug 1 end def yywrap 0 end def on_error(token, value, vstack ) # text = @ss.string[@ss.pos .. -1] text = @ss.peek(20) msg = "" unless value.nil? msg = "line #{@line}: syntax error at value '#{value}' : #{text}" else msg = "line #{@line}: syntax error at token '#{token}' : #{text}" end if @ss.eos? msg = "line #{@line}: Unexpected end of file" end if token == '$end'.intern puts "okay, this is silly" else raise ::Nagios::Parser::SyntaxError, msg end end puppet/external/dot.rb000064400000022247147634041260011034 0ustar00# rdot.rb # # # This is a modified version of dot.rb from Dave Thomas's rdoc project. I [Horst Duchene] # renamed it to rdot.rb to avoid collision with an installed rdoc/dot. # # It also supports undirected edges. module DOT # These glogal vars are used to make nice graph source. $tab = ' ' $tab2 = $tab * 2 # if we don't like 4 spaces, we can change it any time def change_tab (t) $tab = t $tab2 = t * 2 end # options for node declaration NODE_OPTS = [ # attributes due to # http://www.graphviz.org/Documentation/dotguide.pdf # March, 26, 2005 'bottomlabel', # auxiliary label for nodes of shape M* 'color', # default: black; node shape color 'comment', # any string (format-dependent) 'distortion', # default: 0.0; node distortion for shape=polygon 'fillcolor', # default: lightgrey/black; node fill color 'fixedsize', # default: false; label text has no affect on node size 'fontcolor', # default: black; type face color 'fontname', # default: Times-Roman; font family 'fontsize', # default: 14; point size of label 'group', # name of node's group 'height', # default: .5; height in inches 'label', # default: node name; any string 'layer', # default: overlay range; all, id or id:id 'orientation', # default: 0.0; node rotation angle 'peripheries', # shape-dependent number of node boundaries 'regular', # default: false; force polygon to be regular 'shape', # default: ellipse; node shape; see Section 2.1 and Appendix E 'shapefile', # external EPSF or SVG custom shape file 'sides', # default: 4; number of sides for shape=polygon 'skew' , # default: 0.0; skewing of node for shape=polygon 'style', # graphics options, e.g. bold, dotted, filled; cf. Section 2.3 'toplabel', # auxiliary label for nodes of shape M* 'URL', # URL associated with node (format-dependent) 'width', # default: .75; width in inches 'z', # default: 0.0; z coordinate for VRML output # maintained for backward compatibility or rdot internal 'bgcolor', 'rank' ] # options for edge declaration EDGE_OPTS = [ 'arrowhead', # default: normal; style of arrowhead at head end 'arrowsize', # default: 1.0; scaling factor for arrowheads 'arrowtail', # default: normal; style of arrowhead at tail end 'color', # default: black; edge stroke color 'comment', # any string (format-dependent) 'constraint', # default: true use edge to affect node ranking 'decorate', # if set, draws a line connecting labels with their edges 'dir', # default: forward; forward, back, both, or none 'fontcolor', # default: black type face color 'fontname', # default: Times-Roman; font family 'fontsize', # default: 14; point size of label 'headlabel', # label placed near head of edge 'headport', # n,ne,e,se,s,sw,w,nw 'headURL', # URL attached to head label if output format is ismap 'label', # edge label 'labelangle', # default: -25.0; angle in degrees which head or tail label is rotated off edge 'labeldistance', # default: 1.0; scaling factor for distance of head or tail label from node 'labelfloat', # default: false; lessen constraints on edge label placement 'labelfontcolor', # default: black; type face color for head and tail labels 'labelfontname', # default: Times-Roman; font family for head and tail labels 'labelfontsize', # default: 14 point size for head and tail labels 'layer', # default: overlay range; all, id or id:id 'lhead', # name of cluster to use as head of edge 'ltail', # name of cluster to use as tail of edge 'minlen', # default: 1 minimum rank distance between head and tail 'samehead', # tag for head node; edge heads with the same tag are merged onto the same port 'sametail', # tag for tail node; edge tails with the same tag are merged onto the same port 'style', # graphics options, e.g. bold, dotted, filled; cf. Section 2.3 'taillabel', # label placed near tail of edge 'tailport', # n,ne,e,se,s,sw,w,nw 'tailURL', # URL attached to tail label if output format is ismap 'weight', # default: 1; integer cost of stretching an edge # maintained for backward compatibility or rdot internal 'id' ] # options for graph declaration GRAPH_OPTS = [ 'bgcolor', 'center', 'clusterrank', 'color', 'concentrate', 'fontcolor', 'fontname', 'fontsize', 'label', 'layerseq', 'margin', 'mclimit', 'nodesep', 'nslimit', 'ordering', 'orientation', 'page', 'rank', 'rankdir', 'ranksep', 'ratio', 'size' ] # a root class for any element in dot notation class DOTSimpleElement attr_accessor :name def initialize (params = {}) @label = params['name'] ? params['name'] : '' end def to_s @name end end # an element that has options ( node, edge, or graph ) class DOTElement < DOTSimpleElement # attr_reader :parent attr_accessor :name, :options def initialize (params = {}, option_list = []) super(params) @name = params['name'] ? params['name'] : nil @parent = params['parent'] ? params['parent'] : nil @options = {} option_list.each{ |i| @options[i] = params[i] if params[i] } @options['label'] ||= @name if @name != 'node' end def each_option @options.each{ |i| yield i } end def each_option_pair @options.each_pair{ |key, val| yield key, val } end #def parent=( thing ) # @parent.delete( self ) if defined?( @parent ) and @parent # @parent = thing #end end # This is used when we build nodes that have shape=record # ports don't have options :) class DOTPort < DOTSimpleElement attr_accessor :label def initialize (params = {}) super(params) @name = params['label'] ? params['label'] : '' end def to_s ( @name && @name != "" ? "<#{@name}>" : "" ) + "#{@label}" end end # node element class DOTNode < DOTElement @ports def initialize (params = {}, option_list = NODE_OPTS) super(params, option_list) @ports = params['ports'] ? params['ports'] : [] end def each_port @ports.each { |i| yield i } end def << (thing) @ports << thing end def push (thing) @ports.push(thing) end def pop @ports.pop end def to_s (t = '') # This code is totally incomprehensible; it needs to be replaced! label = @options['shape'] != 'record' && @ports.length == 0 ? @options['label'] ? t + $tab + "label = \"#{@options['label']}\"\n" : '' : t + $tab + 'label = "' + " \\\n" + t + $tab2 + "#{@options['label']}| \\\n" + @ports.collect{ |i| t + $tab2 + i.to_s }.join( "| \\\n" ) + " \\\n" + t + $tab + '"' + "\n" t + "#{@name} [\n" + @options.to_a.collect{ |i| i[1] && i[0] != 'label' ? t + $tab + "#{i[0]} = #{i[1]}" : nil }.compact.join( ",\n" ) + ( label != '' ? ",\n" : "\n" ) + label + t + "]\n" end end # A subgraph element is the same to graph, but has another header in dot # notation. class DOTSubgraph < DOTElement @nodes @dot_string def initialize (params = {}, option_list = GRAPH_OPTS) super(params, option_list) @nodes = params['nodes'] ? params['nodes'] : [] @dot_string = 'graph' end def each_node @nodes.each{ |i| yield i } end def << (thing) @nodes << thing end def push (thing) @nodes.push( thing ) end def pop @nodes.pop end def to_s (t = '') hdr = t + "#{@dot_string} #{@name} {\n" options = @options.to_a.collect{ |name, val| val && name != 'label' ? t + $tab + "#{name} = #{val}" : name ? t + $tab + "#{name} = \"#{val}\"" : nil }.compact.join( "\n" ) + "\n" nodes = @nodes.collect{ |i| i.to_s( t + $tab ) }.join( "\n" ) + "\n" hdr + options + nodes + t + "}\n" end end # This is a graph. class DOTDigraph < DOTSubgraph def initialize (params = {}, option_list = GRAPH_OPTS) super(params, option_list) @dot_string = 'digraph' end end # This is an edge. class DOTEdge < DOTElement attr_accessor :from, :to def initialize (params = {}, option_list = EDGE_OPTS) super(params, option_list) @from = params['from'] ? params['from'] : nil @to = params['to'] ? params['to'] : nil end def edge_link '--' end def to_s (t = '') t + "#{@from} #{edge_link} #{to} [\n" + @options.to_a.collect{ |i| i[1] && i[0] != 'label' ? t + $tab + "#{i[0]} = #{i[1]}" : i[1] ? t + $tab + "#{i[0]} = \"#{i[1]}\"" : nil }.compact.join( "\n" ) + "\n#{t}]\n" end end class DOTDirectedEdge < DOTEdge def edge_link '->' end end end puppet/context/trusted_information.rb000064400000003063147634041260014202 0ustar00# @api private class Puppet::Context::TrustedInformation # one of 'remote', 'local', or false, where 'remote' is authenticated via cert, # 'local' is trusted by virtue of running on the same machine (not a remote # request), and false is an unauthenticated remote request. # # @return [String, Boolean] attr_reader :authenticated # The validated certificate name used for the request # # @return [String] attr_reader :certname # Extra information that comes from the trusted certificate's extensions. # # @return [Hash{Object => Object}] attr_reader :extensions def initialize(authenticated, certname, extensions) @authenticated = authenticated.freeze @certname = certname.freeze @extensions = extensions.freeze end def self.remote(authenticated, node_name, certificate) if authenticated extensions = {} if certificate.nil? Puppet.info('TrustedInformation expected a certificate, but none was given.') else extensions = Hash[certificate.custom_extensions.collect do |ext| [ext['oid'].freeze, ext['value'].freeze] end] end new('remote', node_name, extensions) else new(false, nil, {}) end end def self.local(node) # Always trust local data by picking up the available parameters. client_cert = node ? node.parameters['clientcert'] : nil new('local', client_cert, {}) end def to_h { 'authenticated'.freeze => authenticated, 'certname'.freeze => certname, 'extensions'.freeze => extensions }.freeze end end puppet/network/client_request.rb000064400000001464147634041260013141 0ustar00module Puppet::Network # :nodoc: # A struct-like class for passing around a client request. It's mostly # just used for validation and authorization. class ClientRequest attr_accessor :name, :ip, :authenticated, :handler, :method def authenticated? self.authenticated end # A common way of talking about the full call. Individual servers # are responsible for setting the values correctly, but this common # format makes it possible to check rights. def call raise ArgumentError, "Request is not set up; cannot build call" unless handler and method [handler, method].join(".") end def initialize(name, ip, authenticated) @name, @ip, @authenticated = name, ip, authenticated end def to_s "#{self.name}(#{self.ip})" end end end puppet/network/http.rb000064400000001325147634041260011066 0ustar00module Puppet::Network::HTTP HEADER_ENABLE_PROFILING = "X-Puppet-Profiling" HEADER_PUPPET_VERSION = "X-Puppet-Version" require 'puppet/network/http/issues' require 'puppet/network/http/error' require 'puppet/network/http/route' require 'puppet/network/http/api' require 'puppet/network/http/api/v1' require 'puppet/network/http/api/v2' require 'puppet/network/http/handler' require 'puppet/network/http/response' require 'puppet/network/http/request' require 'puppet/network/http/site' require 'puppet/network/http/session' require 'puppet/network/http/factory' require 'puppet/network/http/nocache_pool' require 'puppet/network/http/pool' require 'puppet/network/http/memory_response' end puppet/network/http/webrick/rest.rb000064400000005106147634041260013472 0ustar00require 'puppet/network/http/handler' require 'resolv' require 'webrick' require 'puppet/util/ssl' class Puppet::Network::HTTP::WEBrickREST < WEBrick::HTTPServlet::AbstractServlet include Puppet::Network::HTTP::Handler def self.mutex @mutex ||= Mutex.new end def initialize(server) raise ArgumentError, "server is required" unless server register([Puppet::Network::HTTP::API::V2.routes, Puppet::Network::HTTP::API::V1.routes]) super(server) end # Retrieve the request parameters, including authentication information. def params(request) params = request.query || {} params = Hash[params.collect do |key, value| all_values = value.list [key, all_values.length == 1 ? value : all_values] end] params = decode_params(params) params.merge(client_information(request)) end # WEBrick uses a service method to respond to requests. Simply delegate to # the handler response method. def service(request, response) self.class.mutex.synchronize do process(request, response) end end def headers(request) result = {} request.each do |k, v| result[k.downcase] = v end result end def http_method(request) request.request_method end def path(request) request.path end def body(request) request.body end def client_cert(request) if cert = request.client_cert cert = Puppet::SSL::Certificate.from_instance(cert) warn_if_near_expiration(cert) cert else nil end end # Set the specified format as the content type of the response. def set_content_type(response, format) response["content-type"] = format_to_mime(format) end def set_response(response, result, status = 200) response.status = status if status >= 200 and status != 304 response.body = result response["content-length"] = result.stat.size if result.is_a?(File) end if RUBY_VERSION[0,3] == "1.8" response["connection"] = 'close' end end # Retrieve node/cert/ip information from the request object. def client_information(request) result = {} if peer = request.peeraddr and ip = peer[3] result[:ip] = ip end # If they have a certificate (which will almost always be true) # then we get the hostname from the cert, instead of via IP # info result[:authenticated] = false if cert = request.client_cert and cn = Puppet::Util::SSL.cn_from_subject(cert.subject) result[:node] = cn result[:authenticated] = true else result[:node] = resolve_node(result) end result end end puppet/network/http/api/v2.rb000064400000001443147634041260012167 0ustar00module Puppet::Network::HTTP::API::V2 require 'puppet/network/http/api/v2/environments' require 'puppet/network/http/api/v2/authorization' def self.routes path(%r{^/v2\.0}). get(Authorization.new). chain(ENVIRONMENTS, NOT_FOUND) end private def self.path(path) Puppet::Network::HTTP::Route.path(path) end def self.provide(&block) lambda do |request, response| block.call.call(request, response) end end NOT_FOUND = Puppet::Network::HTTP::Route. path(/.*/). any(lambda do |req, res| raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new(req.path, Puppet::Network::HTTP::Issues::HANDLER_NOT_FOUND) end) ENVIRONMENTS = path(%r{^/environments$}).get(provide do Environments.new(Puppet.lookup(:environments)) end) end puppet/network/http/api/v1.rb000064400000020022147634041260012160 0ustar00require 'puppet/network/authorization' class Puppet::Network::HTTP::API::V1 include Puppet::Network::Authorization # How we map http methods and the indirection name in the URI # to an indirection method. METHOD_MAP = { "GET" => { :plural => :search, :singular => :find }, "POST" => { :singular => :find, }, "PUT" => { :singular => :save }, "DELETE" => { :singular => :destroy }, "HEAD" => { :singular => :head } } def self.routes Puppet::Network::HTTP::Route.path(/.*/).any(new) end # handle an HTTP request def call(request, response) indirection_name, method, key, params = uri2indirection(request.method, request.path, request.params) certificate = request.client_cert check_authorization(method, "/#{indirection_name}/#{key}", params) indirection = Puppet::Indirector::Indirection.instance(indirection_name.to_sym) raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless indirection if !indirection.allow_remote_requests? # TODO: should we tell the user we found an indirection but it doesn't # allow remote requests, or just pretend there's no handler at all? what # are the security implications for the former? raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("No handler for #{indirection.name}", :NO_INDIRECTION_REMOTE_REQUESTS) end trusted = Puppet::Context::TrustedInformation.remote(params[:authenticated], params[:node], certificate) Puppet.override(:trusted_information => trusted) do send("do_#{method}", indirection, key, params, request, response) end rescue Puppet::Network::HTTP::Error::HTTPError => e return do_http_control_exception(response, e) rescue Exception => e return do_exception(response, e) end def uri2indirection(http_method, uri, params) environment, indirection, key = uri.split("/", 4)[1..-1] # the first field is always nil because of the leading slash raise ArgumentError, "The environment must be purely alphanumeric, not '#{environment}'" unless Puppet::Node::Environment.valid_name?(environment) raise ArgumentError, "The indirection name must be purely alphanumeric, not '#{indirection}'" unless indirection =~ /^\w+$/ method = indirection_method(http_method, indirection) configured_environment = Puppet.lookup(:environments).get(environment) if configured_environment.nil? raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find environment '#{environment}'", Puppet::Network::HTTP::Issues::ENVIRONMENT_NOT_FOUND) else configured_environment = configured_environment.override_from_commandline(Puppet.settings) params[:environment] = configured_environment end params.delete(:bucket_path) raise ArgumentError, "No request key specified in #{uri}" if key == "" or key.nil? key = URI.unescape(key) [indirection, method, key, params] end private def do_http_control_exception(response, exception) msg = exception.message Puppet.info(msg) response.respond_with(exception.status, "text/plain", msg) end def do_exception(response, exception, status=400) if exception.is_a?(Puppet::Network::AuthorizationError) # make sure we return the correct status code # for authorization issues status = 403 if status == 400 end Puppet.log_exception(exception) response.respond_with(status, "text/plain", exception.to_s) end # Execute our find. def do_find(indirection, key, params, request, response) unless result = indirection.find(key, params) raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find #{indirection.name} #{key}", Puppet::Network::HTTP::Issues::RESOURCE_NOT_FOUND) end format = accepted_response_formatter_for(indirection.model, request) rendered_result = result if result.respond_to?(:render) Puppet::Util::Profiler.profile("Rendered result in #{format}", [:http, :v1_render, format]) do rendered_result = result.render(format) end end Puppet::Util::Profiler.profile("Sent response", [:http, :v1_response]) do response.respond_with(200, format, rendered_result) end end # Execute our head. def do_head(indirection, key, params, request, response) unless indirection.head(key, params) raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find #{indirection.name} #{key}", Puppet::Network::HTTP::Issues::RESOURCE_NOT_FOUND) end # No need to set a response because no response is expected from a # HEAD request. All we need to do is not die. end # Execute our search. def do_search(indirection, key, params, request, response) result = indirection.search(key, params) if result.nil? raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find instances in #{indirection.name} with '#{key}'", Puppet::Network::HTTP::Issues::RESOURCE_NOT_FOUND) end format = accepted_response_formatter_for(indirection.model, request) response.respond_with(200, format, indirection.model.render_multiple(format, result)) end # Execute our destroy. def do_destroy(indirection, key, params, request, response) formatter = accepted_response_formatter_or_yaml_for(indirection.model, request) result = indirection.destroy(key, params) response.respond_with(200, formatter, formatter.render(result)) end # Execute our save. def do_save(indirection, key, params, request, response) formatter = accepted_response_formatter_or_yaml_for(indirection.model, request) sent_object = read_body_into_model(indirection.model, request) result = indirection.save(sent_object, key) response.respond_with(200, formatter, formatter.render(result)) end def accepted_response_formatter_for(model_class, request) accepted_formats = request.headers['accept'] or raise Puppet::Network::HTTP::Error::HTTPNotAcceptableError.new("Missing required Accept header", Puppet::Network::HTTP::Issues::MISSING_HEADER_FIELD) request.response_formatter_for(model_class.supported_formats, accepted_formats) end def accepted_response_formatter_or_yaml_for(model_class, request) accepted_formats = request.headers['accept'] || "yaml" request.response_formatter_for(model_class.supported_formats, accepted_formats) end def read_body_into_model(model_class, request) data = request.body.to_s format = request.format model_class.convert_from(format, data) end def indirection_method(http_method, indirection) raise ArgumentError, "No support for http method #{http_method}" unless METHOD_MAP[http_method] unless method = METHOD_MAP[http_method][plurality(indirection)] raise ArgumentError, "No support for plurality #{plurality(indirection)} for #{http_method} operations" end method end def self.indirection2uri(request) indirection = request.method == :search ? pluralize(request.indirection_name.to_s) : request.indirection_name.to_s "/#{request.environment.to_s}/#{indirection}/#{request.escaped_key}#{request.query_string}" end def self.request_to_uri_and_body(request) indirection = request.method == :search ? pluralize(request.indirection_name.to_s) : request.indirection_name.to_s ["/#{request.environment.to_s}/#{indirection}/#{request.escaped_key}", request.query_string.sub(/^\?/,'')] end def self.pluralize(indirection) return(indirection == "status" ? "statuses" : indirection + "s") end def plurality(indirection) # NOTE This specific hook for facts is ridiculous, but it's a *many*-line # fix to not need this, and our goal is to move away from the complication # that leads to the fix being too long. return :singular if indirection == "facts" return :singular if indirection == "status" return :singular if indirection == "certificate_status" return :plural if indirection == "inventory" result = (indirection =~ /s$|_search$/) ? :plural : :singular indirection.sub!(/s$|_search$/, '') indirection.sub!(/statuse$/, 'status') result end end puppet/network/http/api/v2/environments.rb000064400000001456147634041260014722 0ustar00require 'json' class Puppet::Network::HTTP::API::V2::Environments def initialize(env_loader) @env_loader = env_loader end def call(request, response) response.respond_with(200, "application/json", JSON.dump({ "search_paths" => @env_loader.search_paths, "environments" => Hash[@env_loader.list.collect do |env| [env.name, { "settings" => { "modulepath" => env.full_modulepath, "manifest" => env.manifest, "environment_timeout" => timeout(env), "config_version" => env.config_version || '', } }] end] })) end private def timeout(env) ttl = @env_loader.get_conf(env.name).environment_timeout if ttl == 1.0 / 0.0 # INFINITY "unlimited" else ttl end end end puppet/network/http/api/v2/authorization.rb000064400000001145147634041260015066 0ustar00class Puppet::Network::HTTP::API::V2::Authorization include Puppet::Network::Authorization def call(request, response) if request.method != "GET" raise Puppet::Network::HTTP::Error::HTTPNotAuthorizedError.new("Only GET requests are authorized for V2 endpoints", Puppet::Network::HTTP::Issues::UNSUPPORTED_METHOD) end begin check_authorization(:find, request.path, request.params) rescue Puppet::Network::AuthorizationError => e raise Puppet::Network::HTTP::Error::HTTPNotAuthorizedError.new(e.message, Puppet::Network::HTTP::Issues::FAILED_AUTHORIZATION) end end end puppet/network/http/memory_response.rb000064400000000331147634041260014310 0ustar00class Puppet::Network::HTTP::MemoryResponse attr_reader :code, :type, :body def initialize @body = "" end def respond_with(code, type, body) @code = code @type = type @body += body end end puppet/network/http/webrick.rb000064400000007255147634041260012524 0ustar00require 'webrick' require 'webrick/https' require 'puppet/network/http/webrick/rest' require 'thread' require 'puppet/ssl/certificate' require 'puppet/ssl/certificate_revocation_list' require 'puppet/ssl/configuration' class Puppet::Network::HTTP::WEBrick CIPHERS = "EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH:+CAMELLIA256:+AES256:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA256-SHA:AES256-SHA:CAMELLIA128-SHA:AES128-SHA" def initialize @listening = false end def listen(address, port) @server = create_server(address, port) @server.listeners.each { |l| l.start_immediately = false } @server.mount('/', Puppet::Network::HTTP::WEBrickREST) raise "WEBrick server is already listening" if @listening @listening = true @thread = Thread.new do @server.start do |sock| timeout = 10.0 if ! IO.select([sock],nil,nil,timeout) raise "Client did not send data within %.1f seconds of connecting" % timeout end sock.accept @server.run(sock) end end sleep 0.1 until @server.status == :Running end def unlisten raise "WEBrick server is not listening" unless @listening @server.shutdown wait_for_shutdown @server = nil @listening = false end def listening? @listening end def wait_for_shutdown @thread.join end # @api private def create_server(address, port) arguments = {:BindAddress => address, :Port => port, :DoNotReverseLookup => true} arguments.merge!(setup_logger) arguments.merge!(setup_ssl) BasicSocket.do_not_reverse_lookup = true server = WEBrick::HTTPServer.new(arguments) server.ssl_context.ciphers = CIPHERS server end # Configure our http log file. def setup_logger # Make sure the settings are all ready for us. Puppet.settings.use(:main, :ssl, :application) if Puppet.run_mode.master? file = Puppet[:masterhttplog] else file = Puppet[:httplog] end # open the log manually to prevent file descriptor leak file_io = ::File.open(file, "a+") file_io.sync = true if defined?(Fcntl::FD_CLOEXEC) file_io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) end args = [file_io] args << WEBrick::Log::DEBUG if Puppet::Util::Log.level == :debug logger = WEBrick::Log.new(*args) return :Logger => logger, :AccessLog => [ [logger, WEBrick::AccessLog::COMMON_LOG_FORMAT ], [logger, WEBrick::AccessLog::REFERER_LOG_FORMAT ] ] end # Add all of the ssl cert information. def setup_ssl results = {} # Get the cached copy. We know it's been generated, too. host = Puppet::SSL::Host.localhost raise Puppet::Error, "Could not retrieve certificate for #{host.name} and not running on a valid certificate authority" unless host.certificate results[:SSLPrivateKey] = host.key.content results[:SSLCertificate] = host.certificate.content results[:SSLStartImmediately] = true results[:SSLEnable] = true results[:SSLOptions] = OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3 raise Puppet::Error, "Could not find CA certificate" unless Puppet::SSL::Certificate.indirection.find(Puppet::SSL::CA_NAME) results[:SSLCACertificateFile] = ssl_configuration.ca_auth_file results[:SSLVerifyClient] = OpenSSL::SSL::VERIFY_PEER results[:SSLCertificateStore] = host.ssl_store results end private def ssl_configuration @ssl_configuration ||= Puppet::SSL::Configuration.new( Puppet[:localcacert], :ca_chain_file => Puppet[:ssl_server_ca_chain], :ca_auth_file => Puppet[:ssl_server_ca_auth]) end end puppet/network/http/compression.rb000064400000006260147634041260013432 0ustar00require 'puppet/network/http' module Puppet::Network::HTTP::Compression # this module function allows to use the right underlying # methods depending on zlib presence def module return(Puppet.features.zlib? ? Active : None) end module_function :module module Active require 'zlib' require 'stringio' # return an uncompressed body if the response has been # compressed def uncompress_body(response) case response['content-encoding'] when 'gzip' return Zlib::GzipReader.new(StringIO.new(response.body)).read when 'deflate' return Zlib::Inflate.new.inflate(response.body) when nil, 'identity' return response.body else raise Net::HTTPError.new("Unknown content encoding - #{response['content-encoding']}", response) end end def uncompress(response) raise Net::HTTPError.new("No block passed") unless block_given? case response['content-encoding'] when 'gzip','deflate' uncompressor = ZlibAdapter.new when nil, 'identity' uncompressor = IdentityAdapter.new else raise Net::HTTPError.new("Unknown content encoding - #{response['content-encoding']}", response) end yield uncompressor uncompressor.close end def add_accept_encoding(headers={}) if Puppet.settings[:http_compression] headers['accept-encoding'] = 'gzip; q=1.0, deflate; q=1.0; identity' else headers['accept-encoding'] = 'identity' end headers end # This adapters knows how to uncompress both 'zlib' stream (the deflate algorithm from Content-Encoding) # and GZip streams. class ZlibAdapter def initialize # Create an inflater that knows to parse GZip streams and zlib streams. # This uses a property of the C Zlib library, documented as follow: # windowBits can also be greater than 15 for optional gzip decoding. Add # 32 to windowBits to enable zlib and gzip decoding with automatic header # detection, or add 16 to decode only the gzip format (the zlib format will # return a Z_DATA_ERROR). If a gzip stream is being decoded, strm->adler is # a crc32 instead of an adler32. @uncompressor = Zlib::Inflate.new(15 + 32) @first = true end def uncompress(chunk) out = @uncompressor.inflate(chunk) @first = false return out rescue Zlib::DataError # it can happen that we receive a raw deflate stream # which might make our inflate throw a data error. # in this case, we try with a verbatim (no header) # deflater. @uncompressor = Zlib::Inflate.new if @first then @first = false retry end raise end def close @uncompressor.finish @uncompressor.close end end end module None def uncompress_body(response) response.body end def add_accept_encoding(headers) headers end def uncompress(response) yield IdentityAdapter.new end end class IdentityAdapter def uncompress(chunk) chunk end def close end end end puppet/network/http/nocache_pool.rb000064400000000741147634041260013520 0ustar00# A pool that does not cache HTTP connections. # # @api private class Puppet::Network::HTTP::NoCachePool def initialize(factory = Puppet::Network::HTTP::Factory.new) @factory = factory end # Yields a Net::HTTP connection. # # @yieldparam http [Net::HTTP] An HTTP connection def with_connection(site, verify, &block) http = @factory.create_connection(site) verify.setup_connection(http) yield http end def close # do nothing end end puppet/network/http/handler.rb000064400000012476147634041260012514 0ustar00module Puppet::Network::HTTP end require 'puppet/network/http' require 'puppet/network/http/api/v1' require 'puppet/network/authentication' require 'puppet/network/rights' require 'puppet/util/profiler' require 'puppet/util/profiler/aggregate' require 'resolv' module Puppet::Network::HTTP::Handler include Puppet::Network::Authentication include Puppet::Network::HTTP::Issues # These shouldn't be allowed to be set by clients # in the query string, for security reasons. DISALLOWED_KEYS = ["node", "ip"] def register(routes) # There's got to be a simpler way to do this, right? dupes = {} routes.each { |r| dupes[r.path_matcher] = (dupes[r.path_matcher] || 0) + 1 } dupes = dupes.collect { |pm, count| pm if count > 1 }.compact if dupes.count > 0 raise ArgumentError, "Given multiple routes with identical path regexes: #{dupes.map{ |rgx| rgx.inspect }.join(', ')}" end @routes = routes Puppet.debug("Routes Registered:") @routes.each do |route| Puppet.debug(route.inspect) end end # Retrieve all headers from the http request, as a hash with the header names # (lower-cased) as the keys def headers(request) raise NotImplementedError end def format_to_mime(format) format.is_a?(Puppet::Network::Format) ? format.mime : format end # handle an HTTP request def process(request, response) new_response = Puppet::Network::HTTP::Response.new(self, response) request_headers = headers(request) request_params = params(request) request_method = http_method(request) request_path = path(request) new_request = Puppet::Network::HTTP::Request.new(request_headers, request_params, request_method, request_path, request_path, client_cert(request), body(request)) response[Puppet::Network::HTTP::HEADER_PUPPET_VERSION] = Puppet.version profiler = configure_profiler(request_headers, request_params) Puppet::Util::Profiler.profile("Processed request #{request_method} #{request_path}", [:http, request_method, request_path]) do if route = @routes.find { |route| route.matches?(new_request) } route.process(new_request, new_response) else raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("No route for #{new_request.method} #{new_request.path}", HANDLER_NOT_FOUND) end end rescue Puppet::Network::HTTP::Error::HTTPError => e Puppet.info(e.message) new_response.respond_with(e.status, "application/json", e.to_json) rescue Exception => e http_e = Puppet::Network::HTTP::Error::HTTPServerError.new(e) Puppet.err(http_e.message) new_response.respond_with(http_e.status, "application/json", http_e.to_json) ensure if profiler remove_profiler(profiler) end cleanup(request) end # Set the response up, with the body and status. def set_response(response, body, status = 200) raise NotImplementedError end # Set the specified format as the content type of the response. def set_content_type(response, format) raise NotImplementedError end # resolve node name from peer's ip address # this is used when the request is unauthenticated def resolve_node(result) begin return Resolv.getname(result[:ip]) rescue => detail Puppet.err "Could not resolve #{result[:ip]}: #{detail}" end result[:ip] end private # methods to be overridden by the including web server class def http_method(request) raise NotImplementedError end def path(request) raise NotImplementedError end def request_key(request) raise NotImplementedError end def body(request) raise NotImplementedError end def params(request) raise NotImplementedError end def client_cert(request) raise NotImplementedError end def cleanup(request) # By default, there is nothing to cleanup. end def decode_params(params) params.select { |key, _| allowed_parameter?(key) }.inject({}) do |result, ary| param, value = ary result[param.to_sym] = parse_parameter_value(param, value) result end end def allowed_parameter?(name) not (name.nil? || name.empty? || DISALLOWED_KEYS.include?(name)) end def parse_parameter_value(param, value) case value when /^---/ Puppet.debug("Found YAML while processing request parameter #{param} (value: <#{value}>)") Puppet.deprecation_warning("YAML in network requests is deprecated and will be removed in a future version. See http://links.puppetlabs.com/deprecate_yaml_on_network") YAML.load(value, :safe => true, :deserialize_symbols => true) when Array value.collect { |v| parse_primitive_parameter_value(v) } else parse_primitive_parameter_value(value) end end def parse_primitive_parameter_value(value) case value when "true" true when "false" false when /^\d+$/ Integer(value) when /^\d+\.\d+$/ value.to_f else value end end def configure_profiler(request_headers, request_params) if (request_headers.has_key?(Puppet::Network::HTTP::HEADER_ENABLE_PROFILING.downcase) or Puppet[:profile]) Puppet::Util::Profiler.add_profiler(Puppet::Util::Profiler::Aggregate.new(Puppet.method(:debug), request_params.object_id)) end end def remove_profiler(profiler) profiler.shutdown Puppet::Util::Profiler.remove_profiler(profiler) end end puppet/network/http/route.rb000064400000004215147634041260012225 0ustar00class Puppet::Network::HTTP::Route MethodNotAllowedHandler = lambda do |req, res| raise Puppet::Network::HTTP::Error::HTTPMethodNotAllowedError.new("method #{req.method} not allowed for route #{req.path}", Puppet::Network::HTTP::Issues::UNSUPPORTED_METHOD) end NO_HANDLERS = [MethodNotAllowedHandler] attr_reader :path_matcher def self.path(path_matcher) new(path_matcher) end def initialize(path_matcher) @path_matcher = path_matcher @method_handlers = { :GET => NO_HANDLERS, :HEAD => NO_HANDLERS, :OPTIONS => NO_HANDLERS, :POST => NO_HANDLERS, :PUT => NO_HANDLERS, :DELETE => NO_HANDLERS } @chained = [] end def get(*handlers) @method_handlers[:GET] = handlers return self end def head(*handlers) @method_handlers[:HEAD] = handlers return self end def options(*handlers) @method_handlers[:OPTIONS] = handlers return self end def post(*handlers) @method_handlers[:POST] = handlers return self end def put(*handlers) @method_handlers[:PUT] = handlers return self end def delete(*handlers) @method_handlers[:DELETE] = handlers return self end def any(*handlers) @method_handlers.each do |method, registered_handlers| @method_handlers[method] = handlers end return self end def chain(*routes) @chained = routes self end def matches?(request) Puppet.debug("Evaluating match for #{self.inspect}") if match(request.routing_path) return true else Puppet.debug("Did not match path (#{request.routing_path.inspect})") end return false end def process(request, response) handlers = @method_handlers[request.method.upcase.intern] || NO_HANDLERS handlers.each do |handler| handler.call(request, response) end subrequest = request.route_into(match(request.routing_path).to_s) if chained_route = @chained.find { |route| route.matches?(subrequest) } chained_route.process(subrequest, response) end end def inspect "Route #{@path_matcher.inspect}" end private def match(path) @path_matcher.match(path) end end puppet/network/http/error.rb000064400000003347147634041260012225 0ustar00require 'json' module Puppet::Network::HTTP::Error Issues = Puppet::Network::HTTP::Issues class HTTPError < Exception attr_reader :status, :issue_kind def initialize(message, status, issue_kind) super(message) @status = status @issue_kind = issue_kind end def to_json JSON({:message => message, :issue_kind => @issue_kind}) end end class HTTPNotAcceptableError < HTTPError CODE = 406 def initialize(message, issue_kind = Issues::RUNTIME_ERROR) super("Not Acceptable: " + message, CODE, issue_kind) end end class HTTPNotFoundError < HTTPError CODE = 404 def initialize(message, issue_kind = Issues::RUNTIME_ERROR) super("Not Found: " + message, CODE, issue_kind) end end class HTTPNotAuthorizedError < HTTPError CODE = 403 def initialize(message, issue_kind = Issues::RUNTIME_ERROR) super("Not Authorized: " + message, CODE, issue_kind) end end class HTTPBadRequestError < HTTPError CODE = 400 def initialize(message, issue_kind = Issues::RUNTIME_ERROR) super("Bad Request: " + message, CODE, issue_kind) end end class HTTPMethodNotAllowedError < HTTPError CODE = 405 def initialize(message, issue_kind = Issues::RUNTIME_ERROR) super("Method Not Allowed: " + message, CODE, issue_kind) end end class HTTPServerError < HTTPError CODE = 500 attr_reader :backtrace def initialize(original_error, issue_kind = Issues::RUNTIME_ERROR) super("Server Error: " + original_error.message, CODE, issue_kind) @backtrace = original_error.backtrace end def to_json JSON({:message => message, :issue_kind => @issue_kind, :stacktrace => self.backtrace}) end end end puppet/network/http/rack/rest.rb000064400000007025147634041260012766 0ustar00require 'openssl' require 'cgi' require 'puppet/network/http/handler' require 'puppet/util/ssl' class Puppet::Network::HTTP::RackREST include Puppet::Network::HTTP::Handler ContentType = 'Content-Type'.freeze CHUNK_SIZE = 8192 class RackFile def initialize(file) @file = file end def each while chunk = @file.read(CHUNK_SIZE) yield chunk end end def close @file.close end end def initialize(args={}) super() register([Puppet::Network::HTTP::API::V2.routes, Puppet::Network::HTTP::API::V1.routes]) end def set_content_type(response, format) response[ContentType] = format_to_mime(format) end # produce the body of the response def set_response(response, result, status = 200) response.status = status unless result.is_a?(File) response.write result else response["Content-Length"] = result.stat.size.to_s response.body = RackFile.new(result) end end # Retrieve all headers from the http request, as a map. def headers(request) headers = request.env.select {|k,v| k.start_with? 'HTTP_'}.inject({}) do |m, (k,v)| m[k.sub(/^HTTP_/, '').gsub('_','-').downcase] = v m end headers['content-type'] = request.content_type headers end # Return which HTTP verb was used in this request. def http_method(request) request.request_method end # Return the query params for this request. def params(request) if request.post? params = request.params else # rack doesn't support multi-valued query parameters, # e.g. ignore, so parse them ourselves params = CGI.parse(request.query_string) convert_singular_arrays_to_value(params) end result = decode_params(params) result.merge(extract_client_info(request)) end # what path was requested? (this is, without any query parameters) def path(request) request.path end # return the request body def body(request) request.body.read end def client_cert(request) # This environment variable is set by mod_ssl, note that it # requires the `+ExportCertData` option in the `SSLOptions` directive cert = request.env['SSL_CLIENT_CERT'] # NOTE: The SSL_CLIENT_CERT environment variable will be the empty string # when Puppet agent nodes have not yet obtained a signed certificate. if cert.nil? || cert.empty? nil else cert = Puppet::SSL::Certificate.from_instance(OpenSSL::X509::Certificate.new(cert)) warn_if_near_expiration(cert) cert end end # Passenger freaks out if we finish handling the request without reading any # part of the body, so make sure we have. def cleanup(request) request.body.read(1) nil end def extract_client_info(request) result = {} result[:ip] = request.ip # if we find SSL info in the headers, use them to get a hostname from the CN. # try this with :ssl_client_header, which defaults should work for # Apache with StdEnvVars. subj_str = request.env[Puppet[:ssl_client_header]] subject = Puppet::Util::SSL.subject_from_dn(subj_str || "") if cn = Puppet::Util::SSL.cn_from_subject(subject) result[:node] = cn result[:authenticated] = (request.env[Puppet[:ssl_client_verify_header]] == 'SUCCESS') else result[:node] = resolve_node(result) result[:authenticated] = false end result end def convert_singular_arrays_to_value(hash) hash.each do |key, value| if value.size == 1 hash[key] = value.first end end end end puppet/network/http/issues.rb000064400000000706147634041260012403 0ustar00module Puppet::Network::HTTP::Issues NO_INDIRECTION_REMOTE_REQUESTS = :NO_INDIRECTION_REMOTE_REQUESTS HANDLER_NOT_FOUND = :HANDLER_NOT_FOUND RESOURCE_NOT_FOUND = :RESOURCE_NOT_FOUND ENVIRONMENT_NOT_FOUND = :ENVIRONMENT_NOT_FOUND RUNTIME_ERROR = :RUNTIME_ERROR MISSING_HEADER_FIELD = :MISSING_HEADER_FIELD UNSUPPORTED_FORMAT = :UNSUPPORTED_FORMAT UNSUPPORTED_METHOD = :UNSUPPORTED_METHOD FAILED_AUTHORIZATION = :FAILED_AUTHORIZATION end puppet/network/http/response.rb000064400000000420147634041260012717 0ustar00class Puppet::Network::HTTP::Response def initialize(handler, response) @handler = handler @response = response end def respond_with(code, type, body) @handler.set_content_type(@response, type) @handler.set_response(@response, body, code) end end puppet/network/http/rack.rb000064400000002205147634041260012004 0ustar00require 'rack' require 'rack/request' require 'rack/response' require 'puppet/network/http' require 'puppet/network/http/rack/rest' # An rack application, for running the Puppet HTTP Server. class Puppet::Network::HTTP::Rack # The real rack application (which needs to respond to call). # The work we need to do, roughly is: # * Read request (from env) and prepare a response # * Route the request to the correct handler # * Return the response (in rack-format) to our caller. def call(env) request = Rack::Request.new(env) response = Rack::Response.new Puppet.debug 'Handling request: %s %s' % [request.request_method, request.fullpath] begin Puppet::Network::HTTP::RackREST.new.process(request, response) rescue => detail # Send a Status 500 Error on unhandled exceptions. response.status = 500 response['Content-Type'] = 'text/plain' response.write 'Internal Server Error: "%s"' % detail.message # log what happened Puppet.log_exception(detail, "Puppet Server (Rack): Internal Server Error: Unhandled Exception: \"%s\"" % detail.message) end response.finish end end puppet/network/http/api.rb000064400000000045147634041260011635 0ustar00class Puppet::Network::HTTP::API end puppet/network/http/factory.rb000064400000002104147634041260012531 0ustar00require 'openssl' require 'net/http' # Factory for Net::HTTP objects. # # Encapsulates the logic for creating a Net::HTTP object based on the # specified {Puppet::Network::HTTP::Site Site} and puppet settings. # # @api private # class Puppet::Network::HTTP::Factory @@openssl_initialized = false def initialize # PUP-1411, make sure that openssl is initialized before we try to connect if ! @@openssl_initialized OpenSSL::SSL::SSLContext.new @@openssl_initialized = true end end def create_connection(site) Puppet.debug("Creating new connection for #{site}") args = [site.host, site.port] if Puppet[:http_proxy_host] == "none" args << nil << nil else args << Puppet[:http_proxy_host] << Puppet[:http_proxy_port] end http = Net::HTTP.new(*args) http.use_ssl = site.use_ssl? # Use configured timeout (#1176) http.read_timeout = Puppet[:configtimeout] http.open_timeout = Puppet[:configtimeout] if Puppet[:http_debug] http.set_debug_output($stderr) end http end end puppet/network/http/pool.rb000064400000005566147634041260012052 0ustar00# A pool for persistent Net::HTTP connections. Connections are # stored in the pool indexed by their {Puppet::Network::HTTP::Site Site}. # Connections are borrowed from the pool, yielded to the caller, and # released back into the pool. If a connection is expired, it will be # closed either when a connection to that site is requested, or when # the pool is closed. The pool can store multiple connections to the # same site, and will be reused in MRU order. # # @api private # class Puppet::Network::HTTP::Pool FIFTEEN_SECONDS = 15 attr_reader :factory def initialize(keepalive_timeout = FIFTEEN_SECONDS) @pool = {} @factory = Puppet::Network::HTTP::Factory.new @keepalive_timeout = keepalive_timeout end def with_connection(site, verify, &block) reuse = true http = borrow(site, verify) begin if http.use_ssl? && http.verify_mode != OpenSSL::SSL::VERIFY_PEER reuse = false end yield http rescue => detail reuse = false raise detail ensure if reuse release(site, http) else close_connection(site, http) end end end def close @pool.each_pair do |site, sessions| sessions.each do |session| close_connection(site, session.connection) end end @pool.clear end # @api private def pool @pool end # Safely close a persistent connection. # # @api private def close_connection(site, http) Puppet.debug("Closing connection for #{site}") http.finish rescue => detail Puppet.log_exception(detail, "Failed to close connection for #{site}: #{detail}") end # Borrow and take ownership of a persistent connection. If a new # connection is created, it will be started prior to being returned. # # @api private def borrow(site, verify) @pool[site] = active_sessions(site) session = @pool[site].shift if session Puppet.debug("Using cached connection for #{site}") session.connection else http = @factory.create_connection(site) verify.setup_connection(http) Puppet.debug("Starting connection for #{site}") http.start http end end # Release a connection back into the pool. # # @api private def release(site, http) expiration = Time.now + @keepalive_timeout session = Puppet::Network::HTTP::Session.new(http, expiration) Puppet.debug("Caching connection for #{site}") sessions = @pool[site] if sessions sessions.unshift(session) else @pool[site] = [session] end end # Returns an Array of sessions whose connections are not expired. # # @api private def active_sessions(site) now = Time.now sessions = @pool[site] || [] sessions.select do |session| if session.expired?(now) close_connection(site, session.connection) false else true end end end end puppet/network/http/site.rb000064400000001423147634041270012032 0ustar00# Represents a site to which HTTP connections are made. It is a value # object, and is suitable for use in a hash. If two sites are equal, # then a persistent connection made to the first site, can be re-used # for the second. # # @api private # class Puppet::Network::HTTP::Site attr_reader :scheme, :host, :port def initialize(scheme, host, port) @scheme = scheme @host = host @port = port.to_i end def addr "#{@scheme}://#{@host}:#{@port.to_s}" end alias to_s addr def ==(rhs) (@scheme == rhs.scheme) && (@host == rhs.host) && (@port == rhs.port) end alias eql? == def hash [@scheme, @host, @port].hash end def use_ssl? @scheme == 'https' end def move_to(uri) self.class.new(uri.scheme, uri.host, uri.port) end end puppet/network/http/request.rb000064400000003751147634041270012564 0ustar00Puppet::Network::HTTP::Request = Struct.new(:headers, :params, :method, :path, :routing_path, :client_cert, :body) do def self.from_hash(hash) symbol_members = members.collect(&:intern) unknown = hash.keys - symbol_members if unknown.empty? new(hash[:headers] || {}, hash[:params] || {}, hash[:method] || "GET", hash[:path], hash[:routing_path] || hash[:path], hash[:client_cert], hash[:body]) else raise ArgumentError, "Unknown arguments: #{unknown.collect(&:inspect).join(', ')}" end end def route_into(prefix) self.class.new(headers, params, method, path, routing_path.sub(prefix, ''), client_cert, body) end def format if header = headers['content-type'] header.gsub!(/\s*;.*$/,'') # strip any charset format = Puppet::Network::FormatHandler.mime(header) if format.nil? raise "Client sent a mime-type (#{header}) that doesn't correspond to a format we support" else report_if_deprecated(format) return format.name.to_s if format.suitable? end end raise "No Content-Type header was received, it isn't possible to unserialize the request" end def response_formatter_for(supported_formats, accepted_formats = headers['accept']) formatter = Puppet::Network::FormatHandler.most_suitable_format_for( accepted_formats.split(/\s*,\s*/), supported_formats) if formatter.nil? raise Puppet::Network::HTTP::Error::HTTPNotAcceptableError.new("No supported formats are acceptable (Accept: #{accepted_formats})", Puppet::Network::HTTP::Issues::UNSUPPORTED_FORMAT) end report_if_deprecated(formatter) formatter end def report_if_deprecated(format) if format.name == :yaml || format.name == :b64_zlib_yaml Puppet.deprecation_warning("YAML in network requests is deprecated and will be removed in a future version. See http://links.puppetlabs.com/deprecate_yaml_on_network") end end end puppet/network/http/connection.rb000064400000017724147634041270013240 0ustar00require 'net/https' require 'puppet/ssl/host' require 'puppet/ssl/configuration' require 'puppet/ssl/validator' require 'puppet/network/authentication' require 'puppet/network/http' require 'uri' module Puppet::Network::HTTP # This will be raised if too many redirects happen for a given HTTP request class RedirectionLimitExceededException < Puppet::Error ; end # This class provides simple methods for issuing various types of HTTP # requests. It's interface is intended to mirror Ruby's Net::HTTP # object, but it provides a few important bits of additional # functionality. Notably: # # * Any HTTPS requests made using this class will use Puppet's SSL # certificate configuration for their authentication, and # * Provides some useful error handling for any SSL errors that occur # during a request. # @api public class Connection include Puppet::Network::Authentication OPTION_DEFAULTS = { :use_ssl => true, :verify => nil, :redirect_limit => 10, } # Creates a new HTTP client connection to `host`:`port`. # @param host [String] the host to which this client will connect to # @param port [Fixnum] the port to which this client will connect to # @param options [Hash] options influencing the properties of the created # connection, # @option options [Boolean] :use_ssl true to connect with SSL, false # otherwise, defaults to true # @option options [#setup_connection] :verify An object that will configure # any verification to do on the connection # @option options [Fixnum] :redirect_limit the number of allowed # redirections, defaults to 10 passing any other option in the options # hash results in a Puppet::Error exception # # @note the HTTP connection itself happens lazily only when {#request}, or # one of the {#get}, {#post}, {#delete}, {#head} or {#put} is called # @note The correct way to obtain a connection is to use one of the factory # methods on {Puppet::Network::HttpPool} # @api private def initialize(host, port, options = {}) @host = host @port = port unknown_options = options.keys - OPTION_DEFAULTS.keys raise Puppet::Error, "Unrecognized option(s): #{unknown_options.map(&:inspect).sort.join(', ')}" unless unknown_options.empty? options = OPTION_DEFAULTS.merge(options) @use_ssl = options[:use_ssl] @verify = options[:verify] @redirect_limit = options[:redirect_limit] @site = Puppet::Network::HTTP::Site.new(@use_ssl ? 'https' : 'http', host, port) @pool = Puppet.lookup(:http_pool) end # @!macro [new] common_options # @param options [Hash] options influencing the request made # @option options [Hash{Symbol => String}] :basic_auth The basic auth # :username and :password to use for the request # @param path [String] # @param headers [Hash{String => String}] # @!macro common_options # @api public def get(path, headers = {}, options = {}) request_with_redirects(Net::HTTP::Get.new(path, headers), options) end # @param path [String] # @param data [String] # @param headers [Hash{String => String}] # @!macro common_options # @api public def post(path, data, headers = nil, options = {}) request = Net::HTTP::Post.new(path, headers) request.body = data request_with_redirects(request, options) end # @param path [String] # @param headers [Hash{String => String}] # @!macro common_options # @api public def head(path, headers = {}, options = {}) request_with_redirects(Net::HTTP::Head.new(path, headers), options) end # @param path [String] # @param headers [Hash{String => String}] # @!macro common_options # @api public def delete(path, headers = {'Depth' => 'Infinity'}, options = {}) request_with_redirects(Net::HTTP::Delete.new(path, headers), options) end # @param path [String] # @param data [String] # @param headers [Hash{String => String}] # @!macro common_options # @api public def put(path, data, headers = nil, options = {}) request = Net::HTTP::Put.new(path, headers) request.body = data request_with_redirects(request, options) end def request(method, *args) self.send(method, *args) end # TODO: These are proxies for the Net::HTTP#request_* methods, which are # almost the same as the "get", "post", etc. methods that we've ported above, # but they are able to accept a code block and will yield to it, which is # necessary to stream responses, e.g. file content. For now # we're not funneling these proxy implementations through our #request # method above, so they will not inherit the same error handling. In the # future we may want to refactor these so that they are funneled through # that method and do inherit the error handling. def request_get(*args, &block) with_connection(@site) do |connection| connection.request_get(*args, &block) end end def request_head(*args, &block) with_connection(@site) do |connection| connection.request_head(*args, &block) end end def request_post(*args, &block) with_connection(@site) do |connection| connection.request_post(*args, &block) end end # end of Net::HTTP#request_* proxies # The address to connect to. def address @site.host end # The port to connect to. def port @site.port end # Whether to use ssl def use_ssl? @site.use_ssl? end private def request_with_redirects(request, options) current_request = request current_site = @site response = nil 0.upto(@redirect_limit) do |redirection| return response if response with_connection(current_site) do |connection| apply_options_to(current_request, options) current_response = execute_request(connection, current_request) if [301, 302, 307].include?(current_response.code.to_i) # handle the redirection location = URI.parse(current_response['location']) current_site = current_site.move_to(location) # update to the current request path current_request = current_request.class.new(location.path) current_request.body = request.body request.each do |header, value| current_request[header] = value end else response = current_response end end # and try again... end raise RedirectionLimitExceededException, "Too many HTTP redirections for #{@host}:#{@port}" end def apply_options_to(request, options) if options[:basic_auth] request.basic_auth(options[:basic_auth][:user], options[:basic_auth][:password]) end end def execute_request(connection, request) response = connection.request(request) # Check the peer certs and warn if they're nearing expiration. warn_if_near_expiration(*@verify.peer_certs) response end def with_connection(site, &block) response = nil @pool.with_connection(site, @verify) do |conn| response = yield conn end response rescue OpenSSL::SSL::SSLError => error if error.message.include? "certificate verify failed" msg = error.message msg << ": [" + @verify.verify_errors.join('; ') + "]" raise Puppet::Error, msg, error.backtrace elsif error.message =~ /hostname.*not match.*server certificate/ leaf_ssl_cert = @verify.peer_certs.last valid_certnames = [leaf_ssl_cert.name, *leaf_ssl_cert.subject_alt_names].uniq msg = valid_certnames.length > 1 ? "one of #{valid_certnames.join(', ')}" : valid_certnames.first msg = "Server hostname '#{site.host}' did not match server certificate; expected #{msg}" raise Puppet::Error, msg, error.backtrace else raise end end end end puppet/network/http/session.rb000064400000000562147634041270012554 0ustar00# An HTTP session that references a persistent HTTP connection and # an expiration time for the connection. # # @api private # class Puppet::Network::HTTP::Session attr_reader :connection def initialize(connection, expiration_time) @connection = connection @expiration_time = expiration_time end def expired?(now) @expiration_time <= now end end puppet/network/rights.rb000064400000013442147634041270011413 0ustar00require 'puppet/network/authstore' require 'puppet/error' module Puppet::Network # this exception is thrown when a request is not authenticated class AuthorizationError < Puppet::Error; end # Rights class manages a list of ACLs for paths. class Rights # Check that name is allowed or not def allowed?(name, *args) !is_forbidden_and_why?(name, :node => args[0], :ip => args[1]) end def is_request_forbidden_and_why?(method, path, params) methods_to_check = if method == :head # :head is ok if either :find or :save is ok. [:find, :save] else [method] end authorization_failure_exceptions = methods_to_check.map do |method| is_forbidden_and_why?(path, params.merge({:method => method})) end if authorization_failure_exceptions.include? nil # One of the methods we checked is ok, therefore this request is ok. nil else # Just need to return any of the failure exceptions. authorization_failure_exceptions.first end end def is_forbidden_and_why?(name, args = {}) res = :nomatch right = @rights.find do |acl| found = false # an acl can return :dunno, which means "I'm not qualified to answer your question, # please ask someone else". This is used when for instance an acl matches, but not for the # current rest method, where we might think some other acl might be more specific. if match = acl.match?(name) args[:match] = match if (res = acl.allowed?(args[:node], args[:ip], args)) != :dunno # return early if we're allowed return nil if res # we matched, select this acl found = true end end found end # if we end up here, then that means we either didn't match or failed, in any # case will return an error to the outside world host_description = args[:node] ? "#{args[:node]}(#{args[:ip]})" : args[:ip] msg = "#{host_description} access to #{name} [#{args[:method]}]" if args[:authenticated] msg += " authenticated " end if right msg += " at #{right.file}:#{right.line}" end AuthorizationError.new("Forbidden request: #{msg}") end def initialize @rights = [] end def [](name) @rights.find { |acl| acl == name } end def empty? @rights.empty? end def include?(name) @rights.include?(name) end def each @rights.each { |r| yield r.name,r } end # Define a new right to which access can be provided. def newright(name, line=nil, file=nil) add_right( Right.new(name, line, file) ) end private def add_right(right) @rights << right right end # Retrieve a right by name. def right(name) self[name] end # A right. class Right < Puppet::Network::AuthStore attr_accessor :name, :key # Overriding Object#methods sucks for debugging. If we're in here in the # future, it would be nice to rename Right#methods attr_accessor :methods, :environment, :authentication attr_accessor :line, :file ALL = [:save, :destroy, :find, :search] Puppet::Util.logmethods(self, true) def initialize(name, line, file) @methods = [] @environment = [] @authentication = true # defaults to authenticated @name = name @line = line || 0 @file = file @methods = ALL case name when /^\// @key = Regexp.new("^" + Regexp.escape(name)) when /^~/ # this is a regex @name = name.gsub(/^~\s+/,'') @key = Regexp.new(@name) else raise ArgumentError, "Unknown right type '#{name}'" end super() end def to_s "access[#{@name}]" end # There's no real check to do at this point def valid? true end # does this right is allowed for this triplet? # if this right is too restrictive (ie we don't match this access method) # then return :dunno so that upper layers have a chance to try another right # tailored to the given method def allowed?(name, ip, args = {}) if not @methods.include?(args[:method]) return :dunno elsif @environment.size > 0 and not @environment.include?(args[:environment]) return :dunno elsif (@authentication and not args[:authenticated]) return :dunno end begin # make sure any capture are replaced if needed interpolate(args[:match]) if args[:match] res = super(name,ip) ensure reset_interpolation end res end # restrict this right to some method only def restrict_method(m) m = m.intern if m.is_a?(String) raise ArgumentError, "'#{m}' is not an allowed value for method directive" unless ALL.include?(m) # if we were allowing all methods, then starts from scratch if @methods === ALL @methods = [] end raise ArgumentError, "'#{m}' is already in the '#{name}' ACL" if @methods.include?(m) @methods << m end def restrict_environment(environment) env = Puppet.lookup(:environments).get(environment) raise ArgumentError, "'#{env}' is already in the '#{name}' ACL" if @environment.include?(env) @environment << env end def restrict_authenticated(authentication) case authentication when "yes", "on", "true", true authentication = true when "no", "off", "false", false, "all" ,"any", :all, :any authentication = false else raise ArgumentError, "'#{name}' incorrect authenticated value: #{authentication}" end @authentication = authentication end def match?(key) # otherwise match with the regex self.key.match(key) end def ==(name) self.name == name.gsub(/^~\s+/,'') end end end end puppet/network/rest_controller.rb000064400000000064147634041270013327 0ustar00class Puppet::Network::RESTController # :nodoc: end puppet/network/authstore.rb000064400000021335147634041270012131 0ustar00# standard module for determining whether a given hostname or IP has access to # the requested resource require 'ipaddr' require 'puppet/util/logging' module Puppet class AuthStoreError < Puppet::Error; end class AuthorizationError < Puppet::Error; end class Network::AuthStore include Puppet::Util::Logging # Is a given combination of name and ip address allowed? If either input # is non-nil, then both inputs must be provided. If neither input # is provided, then the authstore is considered local and defaults to "true". def allowed?(name, ip) if name or ip # This is probably unnecessary, and can cause some weirdnesses in # cases where we're operating over localhost but don't have a real # IP defined. raise Puppet::DevError, "Name and IP must be passed to 'allowed?'" unless name and ip # else, we're networked and such else # we're local return true end # yay insecure overrides return true if globalallow? if decl = declarations.find { |d| d.match?(name, ip) } return decl.result end info "defaulting to no access for #{name}" false end # Mark a given pattern as allowed. def allow(pattern) # a simple way to allow anyone at all to connect if pattern == "*" @globalallow = true else store(:allow, pattern) end nil end def allow_ip(pattern) store(:allow_ip, pattern) end # Deny a given pattern. def deny(pattern) store(:deny, pattern) end def deny_ip(pattern) store(:deny_ip, pattern) end # Is global allow enabled? def globalallow? @globalallow end # does this auth store has any rules? def empty? @globalallow.nil? && @declarations.size == 0 end def initialize @globalallow = nil @declarations = [] end def to_s "authstore" end def interpolate(match) @modified_declarations = @declarations.collect { |ace| ace.interpolate(match) }.sort end def reset_interpolation @modified_declarations = nil end private # Returns our ACEs list, but if we have a modification of it, let's return # it. This is used if we want to override the this purely immutable list # by a modified version. def declarations @modified_declarations || @declarations end # Store the results of a pattern into our hash. Basically just # converts the pattern and sticks it into the hash. def store(type, pattern) @declarations << Declaration.new(type, pattern) @declarations.sort! nil end # A single declaration. Stores the info for a given declaration, # provides the methods for determining whether a declaration matches, # and handles sorting the declarations appropriately. class Declaration include Puppet::Util include Comparable # The type of declaration: either :allow or :deny attr_reader :type VALID_TYPES = [ :allow, :deny, :allow_ip, :deny_ip ] attr_accessor :name # The pattern we're matching against. Can be an IPAddr instance, # or an array of strings, resulting from reversing a hostname # or domain name. attr_reader :pattern # The length. Only used for iprange and domain. attr_accessor :length # Sort the declarations most specific first. def <=>(other) compare(exact?, other.exact?) || compare(ip?, other.ip?) || ((length != other.length) && (other.length <=> length)) || compare(deny?, other.deny?) || ( ip? ? pattern.to_s <=> other.pattern.to_s : pattern <=> other.pattern) end def deny? type == :deny end def exact? @exact == :exact end def initialize(type, pattern) self.type = type self.pattern = pattern end # Are we an IP type? def ip? name == :ip end # Does this declaration match the name/ip combo? def match?(name, ip) if ip? pattern.include?(IPAddr.new(ip)) else matchname?(name) end end # Set the pattern appropriately. Also sets the name and length. def pattern=(pattern) if [:allow_ip, :deny_ip].include?(self.type) parse_ip(pattern) else parse(pattern) end @orig = pattern end # Mapping a type of statement into a return value. def result [:allow, :allow_ip].include?(type) end def to_s "#{type}: #{pattern}" end # Set the declaration type. Either :allow or :deny. def type=(type) type = type.intern raise ArgumentError, "Invalid declaration type #{type}" unless VALID_TYPES.include?(type) @type = type end # interpolate a pattern to replace any # backreferences by the given match # for instance if our pattern is $1.reductivelabs.com # and we're called with a MatchData whose capture 1 is puppet # we'll return a pattern of puppet.reductivelabs.com def interpolate(match) clone = dup if @name == :dynamic clone.pattern = clone.pattern.reverse.collect do |p| p.gsub(/\$(\d)/) { |m| match[$1.to_i] } end.join(".") end clone end private # Returns nil if both values are true or both are false, returns # -1 if the first is true, and 1 if the second is true. Used # in the <=> operator. def compare(me, them) (me and them) ? nil : me ? -1 : them ? 1 : nil end # Does the name match our pattern? def matchname?(name) case @name when :domain, :dynamic, :opaque name = munge_name(name) (pattern == name) or (not exact? and pattern.zip(name).all? { |p,n| p == n }) when :regex Regexp.new(pattern.slice(1..-2)).match(name) end end # Convert the name to a common pattern. def munge_name(name) # Change to name.downcase.split(".",-1).reverse for FQDN support name.downcase.split(".").reverse end # Parse our input pattern and figure out what kind of allowable # statement it is. The output of this is used for later matching. Octet = '(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])' IPv4 = "#{Octet}\.#{Octet}\.#{Octet}\.#{Octet}" IPv6_full = "_:_:_:_:_:_:_:_|_:_:_:_:_:_::_?|_:_:_:_:_::((_:)?_)?|_:_:_:_::((_:){0,2}_)?|_:_:_::((_:){0,3}_)?|_:_::((_:){0,4}_)?|_::((_:){0,5}_)?|::((_:){0,6}_)?" IPv6_partial = "_:_:_:_:_:_:|_:_:_:_::(_:)?|_:_::(_:){0,2}|_::(_:){0,3}" # It should be: # IP = "#{IPv4}|#{IPv6_full}|(#{IPv6_partial}#{IPv4})".gsub(/_/,'([0-9a-fA-F]{1,4})').gsub(/\(/,'(?:') # but ruby's ipaddr lib doesn't support the hybrid format IP = "#{IPv4}|#{IPv6_full}".gsub(/_/,'([0-9a-fA-F]{1,4})').gsub(/\(/,'(?:') def parse_ip(value) @name = :ip @exact, @length, @pattern = *case value when /^(?:#{IP})\/(\d+)$/ # 12.34.56.78/24, a001:b002::efff/120, c444:1000:2000::9:192.168.0.1/112 [:inexact, $1.to_i, IPAddr.new(value)] when /^(#{IP})$/ # 10.20.30.40, [:exact, nil, IPAddr.new(value)] when /^(#{Octet}\.){1,3}\*$/ # an ip address with a '*' at the end segments = value.split(".")[0..-2] bits = 8*segments.length [:inexact, bits, IPAddr.new((segments+[0,0,0])[0,4].join(".") + "/#{bits}")] else raise AuthStoreError, "Invalid IP pattern #{value}" end end def parse(value) @name,@exact,@length,@pattern = *case value when /^(\w[-\w]*\.)+[-\w]+$/ # a full hostname # Change to /^(\w[-\w]*\.)+[-\w]+\.?$/ for FQDN support [:domain,:exact,nil,munge_name(value)] when /^\*(\.(\w[-\w]*)){1,}$/ # *.domain.com host_sans_star = munge_name(value)[0..-2] [:domain,:inexact,host_sans_star.length,host_sans_star] when /\$\d+/ # a backreference pattern ala $1.reductivelabs.com or 192.168.0.$1 or $1.$2 [:dynamic,:exact,nil,munge_name(value)] when /^\w[-.@\w]*$/ # ? Just like a host name but allow '@'s and ending '.'s [:opaque,:exact,nil,[value]] when /^\/.*\/$/ # a regular expression [:regex,:inexact,nil,value] else raise AuthStoreError, "Invalid pattern #{value}" end end end end end puppet/network/resolver.rb000064400000005474147634041270011762 0ustar00require 'resolv' module Puppet::Network; end module Puppet::Network::Resolver # Iterate through the list of servers that service this hostname # and yield each server/port since SRV records have ports in them # It will override whatever masterport setting is already set. def self.each_srv_record(domain, service_name = :puppet, &block) if (domain.nil? or domain.empty?) Puppet.debug "Domain not known; skipping SRV lookup" return end Puppet.debug "Searching for SRV records for domain: #{domain}" case service_name when :puppet then service = '_x-puppet' when :ca then service = '_x-puppet-ca' when :report then service = '_x-puppet-report' when :file then service = '_x-puppet-fileserver' else service = "_x-puppet-#{service_name.to_s}" end srv_record = "#{service}._tcp.#{domain}" resolver = Resolv::DNS.new records = resolver.getresources(srv_record, Resolv::DNS::Resource::IN::SRV) Puppet.debug "Found #{records.size} SRV records for: #{srv_record}" if records.size == 0 && service_name != :puppet # Try the generic :puppet service if no SRV records were found # for the specific service. each_srv_record(domain, :puppet, &block) else each_priority(records) do |priority, records| while next_rr = records.delete(find_weighted_server(records)) Puppet.debug "Yielding next server of #{next_rr.target.to_s}:#{next_rr.port}" yield next_rr.target.to_s, next_rr.port end end end end private def self.each_priority(records) pri_hash = records.inject({}) do |groups, element| groups[element.priority] ||= [] groups[element.priority] << element groups end pri_hash.keys.sort.each do |key| yield key, pri_hash[key] end end def self.find_weighted_server(records) return nil if records.nil? || records.empty? return records.first if records.size == 1 # Calculate the sum of all weights in the list of resource records, # This is used to then select hosts until the weight exceeds what # random number we selected. For example, if we have weights of 1 8 and 3: # # |-|---|--------| # ^ # We generate a random number 5, and iterate through the records, adding # the current record's weight to the accumulator until the weight of the # current record plus previous records is greater than the random number. total_weight = records.inject(0) { |sum,record| sum + weight(record) } current_weight = 0 chosen_weight = 1 + Kernel.rand(total_weight) records.each do |record| current_weight += weight(record) return record if current_weight >= chosen_weight end end def self.weight(record) record.weight == 0 ? 1 : record.weight * 10 end end puppet/network/authconfig.rb000064400000005531147634041270012242 0ustar00require 'puppet/network/rights' module Puppet class ConfigurationError < Puppet::Error; end class Network::AuthConfig attr_accessor :rights DEFAULT_ACL = [ { :acl => "~ ^\/catalog\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true }, { :acl => "~ ^\/node\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true }, # this one will allow all file access, and thus delegate # to fileserver.conf { :acl => "/file" }, { :acl => "/certificate_revocation_list/ca", :method => :find, :authenticated => true }, { :acl => "~ ^\/report\/([^\/]+)$", :method => :save, :allow => '$1', :authenticated => true }, # These allow `auth any`, because if you can do them anonymously you # should probably also be able to do them when trusted. { :acl => "/certificate/ca", :method => :find, :authenticated => :any }, { :acl => "/certificate/", :method => :find, :authenticated => :any }, { :acl => "/certificate_request", :method => [:find, :save], :authenticated => :any }, { :acl => "/status", :method => [:find], :authenticated => true }, # API V2.0 { :acl => "/v2.0/environments", :method => :find, :allow => '*', :authenticated => true }, ] # Just proxy the setting methods to our rights stuff [:allow, :deny].each do |method| define_method(method) do |*args| @rights.send(method, *args) end end # force regular ACLs to be present def insert_default_acl DEFAULT_ACL.each do |acl| unless rights[acl[:acl]] Puppet.info "Inserting default '#{acl[:acl]}' (auth #{acl[:authenticated]}) ACL" mk_acl(acl) end end # queue an empty (ie deny all) right for every other path # actually this is not strictly necessary as the rights system # denies not explicitely allowed paths unless rights["/"] rights.newright("/").restrict_authenticated(:any) end end def mk_acl(acl) right = @rights.newright(acl[:acl]) right.allow(acl[:allow] || "*") if method = acl[:method] method = [method] unless method.is_a?(Array) method.each { |m| right.restrict_method(m) } end right.restrict_authenticated(acl[:authenticated]) unless acl[:authenticated].nil? end # check whether this request is allowed in our ACL # raise an Puppet::Network::AuthorizedError if the request # is denied. def check_authorization(method, path, params) if authorization_failure_exception = @rights.is_request_forbidden_and_why?(method, path, params) Puppet.warning("Denying access: #{authorization_failure_exception}") raise authorization_failure_exception end end def initialize(rights=nil) @rights = rights || Puppet::Network::Rights.new insert_default_acl end end end puppet/network/formats.rb000064400000013523147634041270011566 0ustar00require 'puppet/network/format_handler' Puppet::Network::FormatHandler.create_serialized_formats(:msgpack, :weight => 20, :mime => "application/x-msgpack", :required_methods => [:render_method, :intern_method], :intern_method => :from_data_hash) do confine :feature => :msgpack def intern(klass, text) data = MessagePack.unpack(text) return data if data.is_a?(klass) klass.from_data_hash(data) end def intern_multiple(klass, text) MessagePack.unpack(text).collect do |data| klass.from_data_hash(data) end end def render_multiple(instances) instances.to_msgpack end end Puppet::Network::FormatHandler.create_serialized_formats(:yaml) do def intern(klass, text) data = YAML.load(text, :safe => true, :deserialize_symbols => true) data_to_instance(klass, data) end def intern_multiple(klass, text) data = YAML.load(text, :safe => true, :deserialize_symbols => true) unless data.respond_to?(:collect) raise Puppet::Network::FormatHandler::FormatError, "Serialized YAML did not contain a collection of instances when calling intern_multiple" end data.collect do |datum| data_to_instance(klass, datum) end end def data_to_instance(klass, data) return data if data.is_a?(klass) unless data.is_a? Hash raise Puppet::Network::FormatHandler::FormatError, "Serialized YAML did not contain a valid instance of #{klass}" end klass.from_data_hash(data) end def render(instance) instance.to_yaml end # Yaml monkey-patches Array, so this works. def render_multiple(instances) instances.to_yaml end def supported?(klass) true end end # This is a "special" format which is used for the moment only when sending facts # as REST GET parameters (see Puppet::Configurer::FactHandler). # This format combines a yaml serialization, then zlib compression and base64 encoding. Puppet::Network::FormatHandler.create_serialized_formats(:b64_zlib_yaml) do require 'base64' def use_zlib? Puppet.features.zlib? && Puppet[:zlib] end def requiring_zlib if use_zlib? yield else raise Puppet::Error, "the zlib library is not installed or is disabled." end end def intern(klass, text) requiring_zlib do Puppet::Network::FormatHandler.format(:yaml).intern(klass, decode(text)) end end def intern_multiple(klass, text) requiring_zlib do Puppet::Network::FormatHandler.format(:yaml).intern_multiple(klass, decode(text)) end end def render(instance) encode(instance.to_yaml) end def render_multiple(instances) encode(instances.to_yaml) end def supported?(klass) true end def decode(data) Zlib::Inflate.inflate(Base64.decode64(data)) end def encode(text) requiring_zlib do Base64.encode64(Zlib::Deflate.deflate(text, Zlib::BEST_COMPRESSION)) end end end Puppet::Network::FormatHandler.create(:s, :mime => "text/plain", :extension => "txt") # A very low-weight format so it'll never get chosen automatically. Puppet::Network::FormatHandler.create(:raw, :mime => "application/x-raw", :weight => 1) do def intern_multiple(klass, text) raise NotImplementedError end def render_multiple(instances) raise NotImplementedError end # LAK:NOTE The format system isn't currently flexible enough to handle # what I need to support raw formats just for individual instances (rather # than both individual and collections), but we don't yet have enough data # to make a "correct" design. # So, we hack it so it works for singular but fail if someone tries it # on plurals. def supported?(klass) true end end Puppet::Network::FormatHandler.create_serialized_formats(:pson, :weight => 10, :required_methods => [:render_method, :intern_method], :intern_method => :from_data_hash) do def intern(klass, text) data_to_instance(klass, PSON.parse(text)) end def intern_multiple(klass, text) PSON.parse(text).collect do |data| data_to_instance(klass, data) end end # PSON monkey-patches Array, so this works. def render_multiple(instances) instances.to_pson end # If they pass class information, we want to ignore it. By default, # we'll include class information but we won't rely on it - we don't # want class names to be required because we then can't change our # internal class names, which is bad. def data_to_instance(klass, data) if data.is_a?(Hash) and d = data['data'] data = d end return data if data.is_a?(klass) klass.from_data_hash(data) end end # This is really only ever going to be used for Catalogs. Puppet::Network::FormatHandler.create_serialized_formats(:dot, :required_methods => [:render_method]) Puppet::Network::FormatHandler.create(:console, :mime => 'text/x-console-text', :weight => 0) do def json @json ||= Puppet::Network::FormatHandler.format(:pson) end def render(datum) # String to String return datum if datum.is_a? String return datum if datum.is_a? Numeric # Simple hash to table if datum.is_a? Hash and datum.keys.all? { |x| x.is_a? String or x.is_a? Numeric } output = '' column_a = datum.empty? ? 2 : datum.map{ |k,v| k.to_s.length }.max + 2 datum.sort_by { |k,v| k.to_s } .each do |key, value| output << key.to_s.ljust(column_a) output << json.render(value). chomp.gsub(/\n */) { |x| x + (' ' * column_a) } output << "\n" end return output end # Print one item per line for arrays if datum.is_a? Array output = '' datum.each do |item| output << item.to_s output << "\n" end return output end # ...or pretty-print the inspect outcome. return json.render(datum) end def render_multiple(data) data.collect(&:render).join("\n") end end puppet/network/http_pool.rb000064400000004170147634041270012121 0ustar00require 'puppet/network/http/connection' module Puppet::Network; end # This module contains the factory methods that should be used for getting a # {Puppet::Network::HTTP::Connection} instance. The pool may return a new # connection or a persistent cached connection, depending on the underlying # pool implementation in use. # # @api public # module Puppet::Network::HttpPool @http_client_class = Puppet::Network::HTTP::Connection def self.http_client_class @http_client_class end def self.http_client_class=(klass) @http_client_class = klass end # Retrieve a connection for the given host and port. # # @param host [String] The hostname to connect to # @param port [Integer] The port on the host to connect to # @param use_ssl [Boolean] Whether to use an SSL connection # @param verify_peer [Boolean] Whether to verify the peer credentials, if possible. Verification will not take place if the CA certificate is missing. # @return [Puppet::Network::HTTP::Connection] # # @api public # def self.http_instance(host, port, use_ssl = true, verify_peer = true) verifier = if verify_peer Puppet::SSL::Validator.default_validator() else Puppet::SSL::Validator.no_validator() end http_client_class.new(host, port, :use_ssl => use_ssl, :verify => verifier) end # Get an http connection that will be secured with SSL and have the # connection verified with the given verifier # # @param host [String] the DNS name to connect to # @param port [Integer] the port to connect to # @param verifier [#setup_connection, #peer_certs, #verify_errors] An object that will setup the appropriate # verification on a Net::HTTP instance and report any errors and the certificates used. # @return [Puppet::Network::HTTP::Connection] # # @api public # def self.http_ssl_instance(host, port, verifier = Puppet::SSL::Validator.default_validator()) http_client_class.new(host, port, :use_ssl => true, :verify => verifier) end end puppet/network/format_support.rb000064400000006650147634041270013202 0ustar00require 'puppet/network/format_handler' # Provides network serialization support when included # @api public module Puppet::Network::FormatSupport def self.included(klass) klass.extend(ClassMethods) end module ClassMethods def convert_from(format, data) get_format(format).intern(self, data) rescue => err raise Puppet::Network::FormatHandler::FormatError, "Could not intern from #{format}: #{err}", err.backtrace end def convert_from_multiple(format, data) get_format(format).intern_multiple(self, data) rescue => err raise Puppet::Network::FormatHandler::FormatError, "Could not intern_multiple from #{format}: #{err}", err.backtrace end def render_multiple(format, instances) get_format(format).render_multiple(instances) rescue => err raise Puppet::Network::FormatHandler::FormatError, "Could not render_multiple to #{format}: #{err}", err.backtrace end def default_format supported_formats[0] end def support_format?(name) Puppet::Network::FormatHandler.format(name).supported?(self) end def supported_formats result = format_handler.formats.collect do |f| format_handler.format(f) end.find_all do |f| f.supported?(self) end.sort do |a, b| # It's an inverse sort -- higher weight formats go first. b.weight <=> a.weight end.collect do |f| f.name end result = put_preferred_format_first(result) Puppet.debug "#{friendly_name} supports formats: #{result.join(' ')}" result end # @api private def get_format(format_name) format_handler.format_for(format_name) end private def format_handler Puppet::Network::FormatHandler end def friendly_name if self.respond_to? :indirection indirection.name else self end end def put_preferred_format_first(list) preferred_format = Puppet.settings[:preferred_serialization_format].to_sym if list.include?(preferred_format) list.delete(preferred_format) list.unshift(preferred_format) else Puppet.debug "Value of 'preferred_serialization_format' (#{preferred_format}) is invalid for #{friendly_name}, using default (#{list.first})" end list end end def to_msgpack(*args) to_data_hash.to_msgpack(*args) end def to_pson(*args) to_data_hash.to_pson(*args) end def render(format = nil) format ||= self.class.default_format self.class.get_format(format).render(self) rescue => err raise Puppet::Network::FormatHandler::FormatError, "Could not render to #{format}: #{err}", err.backtrace end def mime(format = nil) format ||= self.class.default_format self.class.get_format(format).mime rescue => err raise Puppet::Network::FormatHandler::FormatError, "Could not mime to #{format}: #{err}", err.backtrace end def support_format?(name) self.class.support_format?(name) end # @comment Document to_data_hash here as it is called as a hook from to_msgpack if it exists # @!method to_data_hash(*args) # @api public # @abstract # This method may be implemented to return a hash object that is used for serializing. # The object returned by this method should contain all the info needed to instantiate it again. # If the method exists it will be called from to_msgpack and other serialization methods. # @return [Hash] end puppet/network/format_handler.rb000064400000005336147634041270013103 0ustar00require 'yaml' require 'puppet/network' require 'puppet/network/format' module Puppet::Network::FormatHandler class FormatError < Puppet::Error; end ALL_MEDIA_TYPES = '*/*'.freeze @formats = {} def self.create(*args, &block) instance = Puppet::Network::Format.new(*args, &block) @formats[instance.name] = instance instance end def self.create_serialized_formats(name,options = {},&block) ["application/x-#{name}", "application/#{name}", "text/x-#{name}", "text/#{name}"].each { |mime_type| create name, {:mime => mime_type}.update(options), &block } end def self.format(name) @formats[name.to_s.downcase.intern] end def self.format_for(name) name = format_to_canonical_name(name) format(name) end def self.format_by_extension(ext) @formats.each do |name, format| return format if format.extension == ext end nil end # Provide a list of all formats. def self.formats @formats.keys end # Return a format capable of handling the provided mime type. def self.mime(mimetype) mimetype = mimetype.to_s.downcase @formats.values.find { |format| format.mime == mimetype } end # Return a format name given: # * a format name # * a mime-type # * a format instance def self.format_to_canonical_name(format) case format when Puppet::Network::Format out = format when %r{\w+/\w+} out = mime(format) else out = format(format) end raise ArgumentError, "No format match the given format name or mime-type (#{format})" if out.nil? out.name end # Determine which of the accepted formats should be used given what is supported. # # @param accepted [Array] the accepted formats in a form a # that generally conforms to an HTTP Accept header. Any quality specifiers # are ignored and instead the formats are simply in strict preference order # (most preferred is first) # @param supported [Array] the names of the supported formats (the # most preferred format is first) # @return [Puppet::Network::Format, nil] the most suitable format # @api private def self.most_suitable_format_for(accepted, supported) format_name = accepted.collect do |accepted| accepted.to_s.sub(/;q=.*$/, '') end.collect do |accepted| if accepted == ALL_MEDIA_TYPES supported.first else format_to_canonical_name_or_nil(accepted) end end.find do |accepted| supported.include?(accepted) end if format_name format_for(format_name) end end # @api private def self.format_to_canonical_name_or_nil(format) format_to_canonical_name(format) rescue ArgumentError nil end end require 'puppet/network/formats' puppet/network/format.rb000064400000006563147634041270011411 0ustar00require 'puppet/confiner' # A simple class for modeling encoding formats for moving # instances around the network. class Puppet::Network::Format include Puppet::Confiner attr_reader :name, :mime attr_accessor :intern_method, :render_method, :intern_multiple_method, :render_multiple_method, :weight, :required_methods, :extension def init_attribute(name, default) if value = @options[name] @options.delete(name) else value = default end self.send(name.to_s + "=", value) end def initialize(name, options = {}, &block) @name = name.to_s.downcase.intern @options = options # This must be done early the values can be used to set required_methods define_method_names method_list = { :intern_method => "from_#{name}", :intern_multiple_method => "from_multiple_#{name}", :render_multiple_method => "to_multiple_#{name}", :render_method => "to_#{name}" } init_attribute(:mime, "text/#{name}") init_attribute(:weight, 5) init_attribute(:required_methods, method_list.keys) init_attribute(:extension, name.to_s) method_list.each do |method, value| init_attribute(method, value) end raise ArgumentError, "Unsupported option(s) #{@options.keys}" unless @options.empty? @options = nil instance_eval(&block) if block_given? end def intern(klass, text) return klass.send(intern_method, text) if klass.respond_to?(intern_method) raise NotImplementedError, "#{klass} does not respond to #{intern_method}; can not intern instances from #{mime}" end def intern_multiple(klass, text) return klass.send(intern_multiple_method, text) if klass.respond_to?(intern_multiple_method) raise NotImplementedError, "#{klass} does not respond to #{intern_multiple_method}; can not intern multiple instances from #{mime}" end def mime=(mime) @mime = mime.to_s.downcase end def render(instance) return instance.send(render_method) if instance.respond_to?(render_method) raise NotImplementedError, "#{instance.class} does not respond to #{render_method}; can not render instances to #{mime}" end def render_multiple(instances) # This method implicitly assumes that all instances are of the same type. return instances[0].class.send(render_multiple_method, instances) if instances[0].class.respond_to?(render_multiple_method) raise NotImplementedError, "#{instances[0].class} does not respond to #{render_multiple_method}; can not intern multiple instances to #{mime}" end def required_methods_present?(klass) [:intern_method, :intern_multiple_method, :render_multiple_method].each do |name| return false unless required_method_present?(name, klass, :class) end return false unless required_method_present?(:render_method, klass, :instance) true end def supported?(klass) suitable? and required_methods_present?(klass) end def to_s "Puppet::Network::Format[#{name}]" end private def define_method_names @intern_method = "from_#{name}" @render_method = "to_#{name}" @intern_multiple_method = "from_multiple_#{name}" @render_multiple_method = "to_multiple_#{name}" end def required_method_present?(name, klass, type) return true unless required_methods.include?(name) method = send(name) return(type == :class ? klass.respond_to?(method) : klass.method_defined?(method)) end end puppet/network/authorization.rb000064400000001717147634041270013015 0ustar00require 'puppet/network/client_request' require 'puppet/network/authconfig' require 'puppet/network/auth_config_parser' module Puppet::Network class AuthConfigLoader # Create our config object if necessary. If there's no configuration file # we install our defaults def self.authconfig @auth_config_file ||= Puppet::Util::WatchedFile.new(Puppet[:rest_authconfig]) if (not @auth_config) or @auth_config_file.changed? begin @auth_config = Puppet::Network::AuthConfigParser.new_from_file(Puppet[:rest_authconfig]).parse rescue Errno::ENOENT, Errno::ENOTDIR @auth_config = Puppet::Network::AuthConfig.new end end @auth_config end end module Authorization def authconfig AuthConfigLoader.authconfig end # Verify that our client has access. def check_authorization(method, path, params) authconfig.check_authorization(method, path, params) end end end puppet/network/authentication.rb000064400000003465147634041270013136 0ustar00require 'puppet/ssl/certificate_authority' require 'puppet/util/log/rate_limited_logger' # Place for any authentication related bits module Puppet::Network::Authentication # Create a rate-limited logger for the expiration warning that uses the run interval # as the minimum amount of time before a warning about the same cert can be logged again. # This is a class variable so that all classes that include the module share the same logger. @@logger = Puppet::Util::Log::RateLimitedLogger.new(Puppet[:runinterval]) # Check the expiration of known certificates and optionally any that are specified as part of a request def warn_if_near_expiration(*certs) # Check CA cert if we're functioning as a CA certs << Puppet::SSL::CertificateAuthority.instance.host.certificate if Puppet::SSL::CertificateAuthority.ca? # Depending on the run mode, the localhost certificate will be for the # master or the agent. Don't load the certificate if the CA cert is not # present: infinite recursion will occur as another authenticated request # will be spawned to download the CA cert. if [Puppet[:hostcert], Puppet[:localcacert]].all? {|path| Puppet::FileSystem.exist?(path) } certs << Puppet::SSL::Host.localhost.certificate end # Remove nil values for caller convenience certs.compact.each do |cert| # Allow raw OpenSSL certificate instances or Puppet certificate wrappers to be specified cert = Puppet::SSL::Certificate.from_instance(cert) if cert.is_a?(OpenSSL::X509::Certificate) raise ArgumentError, "Invalid certificate '#{cert.inspect}'" unless cert.is_a?(Puppet::SSL::Certificate) if cert.near_expiration? @@logger.warning("Certificate '#{cert.unmunged_name}' will expire on #{cert.expiration.strftime('%Y-%m-%dT%H:%M:%S%Z')}") end end end end puppet/network/auth_config_parser.rb000064400000004767147634041270013767 0ustar00require 'puppet/network/rights' module Puppet::Network class AuthConfigParser def self.new_from_file(file) self.new(File.read(file)) end def initialize(string) @string = string end def parse Puppet::Network::AuthConfig.new(parse_rights) end def parse_rights rights = Puppet::Network::Rights.new right = nil count = 1 @string.each_line { |line| case line.chomp when /^\s*#/, /^\s*$/ # skip comments and blank lines when /^path\s+((?:~\s+)?[^ ]+)\s*$/ # "path /path" or "path ~ regex" name = $1.chomp right = rights.newright(name, count, @file) when /^\s*(allow(?:_ip)?|deny(?:_ip)?|method|environment|auth(?:enticated)?)\s+(.+?)(\s*#.*)?$/ if right.nil? raise Puppet::ConfigurationError, "Missing or invalid 'path' before right directive at line #{count} of #{@file}" end parse_right_directive(right, $1, $2, count) else raise Puppet::ConfigurationError, "Invalid line #{count}: #{line}" end count += 1 } # Verify each of the rights are valid. # We let the check raise an error, so that it can raise an error # pointing to the specific problem. rights.each { |name, right| right.valid? } rights end def parse_right_directive(right, var, value, count) value.strip! case var when "allow" modify_right(right, :allow, value, "allowing %s access", count) when "deny" modify_right(right, :deny, value, "denying %s access", count) when "allow_ip" modify_right(right, :allow_ip, value, "allowing IP %s access", count) when "deny_ip" modify_right(right, :deny_ip, value, "denying IP %s access", count) when "method" modify_right(right, :restrict_method, value, "allowing 'method' %s", count) when "environment" modify_right(right, :restrict_environment, value, "adding environment %s", count) when /auth(?:enticated)?/ modify_right(right, :restrict_authenticated, value, "adding authentication %s", count) else raise Puppet::ConfigurationError, "Invalid argument '#{var}' at line #{count}" end end def modify_right(right, method, value, msg, count) value.split(/\s*,\s*/).each do |val| begin val.strip! right.info msg % val right.send(method, val) rescue Puppet::AuthStoreError => detail raise Puppet::ConfigurationError, "#{detail} at line #{count} of #{@file}", detail.backtrace end end end end end puppet/network/server.rb000064400000001457147634041270011424 0ustar00require 'puppet/network/http' require 'puppet/network/http/webrick' # # @api private class Puppet::Network::Server attr_reader :address, :port def initialize(address, port) @port = port @address = address @http_server = Puppet::Network::HTTP::WEBrick.new @listening = false # Make sure we have all of the directories we need to function. Puppet.settings.use(:main, :ssl, :application) end def listening? @listening end def start raise "Cannot listen -- already listening." if listening? @listening = true @http_server.listen(address, port) end def stop raise "Cannot unlisten -- not currently listening." unless listening? @http_server.unlisten @listening = false end def wait_for_shutdown @http_server.wait_for_shutdown end end puppet/settings.rb000064400000133462147634041270010267 0ustar00require 'puppet' require 'getoptlong' require 'puppet/util/watched_file' require 'puppet/util/command_line/puppet_option_parser' require 'forwardable' # The class for handling configuration files. class Puppet::Settings extend Forwardable include Enumerable require 'puppet/settings/errors' require 'puppet/settings/base_setting' require 'puppet/settings/string_setting' require 'puppet/settings/enum_setting' require 'puppet/settings/array_setting' require 'puppet/settings/file_setting' require 'puppet/settings/directory_setting' require 'puppet/settings/file_or_directory_setting' require 'puppet/settings/path_setting' require 'puppet/settings/boolean_setting' require 'puppet/settings/terminus_setting' require 'puppet/settings/duration_setting' require 'puppet/settings/ttl_setting' require 'puppet/settings/priority_setting' require 'puppet/settings/autosign_setting' require 'puppet/settings/config_file' require 'puppet/settings/value_translator' require 'puppet/settings/environment_conf' # local reference for convenience PuppetOptionParser = Puppet::Util::CommandLine::PuppetOptionParser attr_accessor :files attr_reader :timer # These are the settings that every app is required to specify; there are reasonable defaults defined in application.rb. REQUIRED_APP_SETTINGS = [:logdir, :confdir, :vardir] # This method is intended for puppet internal use only; it is a convenience method that # returns reasonable application default settings values for a given run_mode. def self.app_defaults_for_run_mode(run_mode) { :name => run_mode.to_s, :run_mode => run_mode.name, :confdir => run_mode.conf_dir, :vardir => run_mode.var_dir, :rundir => run_mode.run_dir, :logdir => run_mode.log_dir, } end def self.default_certname() hostname = hostname_fact domain = domain_fact if domain and domain != "" fqdn = [hostname, domain].join(".") else fqdn = hostname end fqdn.to_s.gsub(/\.$/, '') end def self.hostname_fact() Facter["hostname"].value end def self.domain_fact() Facter["domain"].value end def self.default_config_file_name "puppet.conf" end # Create a new collection of config settings. def initialize @config = {} @shortnames = {} @created = [] # Keep track of set values. @value_sets = { :cli => Values.new(:cli, @config), :memory => Values.new(:memory, @config), :application_defaults => Values.new(:application_defaults, @config), :overridden_defaults => Values.new(:overridden_defaults, @config), } @configuration_file = nil # And keep a per-environment cache @cache = Hash.new { |hash, key| hash[key] = {} } @values = Hash.new { |hash, key| hash[key] = {} } # The list of sections we've used. @used = [] @hooks_to_call_on_application_initialization = [] @deprecated_setting_names = [] @deprecated_settings_that_have_been_configured = [] @translate = Puppet::Settings::ValueTranslator.new @config_file_parser = Puppet::Settings::ConfigFile.new(@translate) end # @param name [Symbol] The name of the setting to fetch # @return [Puppet::Settings::BaseSetting] The setting object def setting(name) @config[name] end # Retrieve a config value # @param param [Symbol] the name of the setting # @return [Object] the value of the setting # @api private def [](param) if @deprecated_setting_names.include?(param) issue_deprecation_warning(setting(param), "Accessing '#{param}' as a setting is deprecated.") end value(param) end # Set a config value. This doesn't set the defaults, it sets the value itself. # @param param [Symbol] the name of the setting # @param value [Object] the new value of the setting # @api private def []=(param, value) if @deprecated_setting_names.include?(param) issue_deprecation_warning(setting(param), "Modifying '#{param}' as a setting is deprecated.") end @value_sets[:memory].set(param, value) unsafe_flush_cache end # Create a new default value for the given setting. The default overrides are # higher precedence than the defaults given in defaults.rb, but lower # precedence than any other values for the setting. This allows one setting # `a` to change the default of setting `b`, but still allow a user to provide # a value for setting `b`. # # @param param [Symbol] the name of the setting # @param value [Object] the new default value for the setting # @api private def override_default(param, value) @value_sets[:overridden_defaults].set(param, value) unsafe_flush_cache end # Generate the list of valid arguments, in a format that GetoptLong can # understand, and add them to the passed option list. def addargs(options) # Add all of the settings as valid options. self.each { |name, setting| setting.getopt_args.each { |args| options << args } } options end # Generate the list of valid arguments, in a format that OptionParser can # understand, and add them to the passed option list. def optparse_addargs(options) # Add all of the settings as valid options. self.each { |name, setting| options << setting.optparse_args } options end # Is our setting a boolean setting? def boolean?(param) param = param.to_sym @config.include?(param) and @config[param].kind_of?(BooleanSetting) end # Remove all set values, potentially skipping cli values. def clear unsafe_clear end # Remove all set values, potentially skipping cli values. def unsafe_clear(clear_cli = true, clear_application_defaults = false) if clear_application_defaults @value_sets[:application_defaults] = Values.new(:application_defaults, @config) @app_defaults_initialized = false end if clear_cli @value_sets[:cli] = Values.new(:cli, @config) # Only clear the 'used' values if we were explicitly asked to clear out # :cli values; otherwise, it may be just a config file reparse, # and we want to retain this cli values. @used = [] end @value_sets[:memory] = Values.new(:memory, @config) @value_sets[:overridden_defaults] = Values.new(:overridden_defaults, @config) @deprecated_settings_that_have_been_configured.clear @values.clear @cache.clear end private :unsafe_clear # Clears all cached settings for a particular environment to ensure # that changes to environment.conf are reflected in the settings if # the environment timeout has expired. # # param [String, Symbol] environment the name of environment to clear settings for # # @api private def clear_environment_settings(environment) if environment.nil? return end @cache[environment.to_sym].clear @values[environment.to_sym] = {} end # Clear @cache, @used and the Environment. # # Whenever an object is returned by Settings, a copy is stored in @cache. # As long as Setting attributes that determine the content of returned # objects remain unchanged, Settings can keep returning objects from @cache # without re-fetching or re-generating them. # # Whenever a Settings attribute changes, such as @values or @preferred_run_mode, # this method must be called to clear out the caches so that updated # objects will be returned. def flush_cache unsafe_flush_cache end def unsafe_flush_cache clearused # Clear the list of environments, because they cache, at least, the module path. # We *could* preferentially just clear them if the modulepath is changed, # but we don't really know if, say, the vardir is changed and the modulepath # is defined relative to it. We need the defined?(stuff) because of loading # order issues. Puppet::Node::Environment.clear if defined?(Puppet::Node) and defined?(Puppet::Node::Environment) end private :unsafe_flush_cache def clearused @cache.clear @used = [] end def global_defaults_initialized?() @global_defaults_initialized end def initialize_global_settings(args = []) raise Puppet::DevError, "Attempting to initialize global default settings more than once!" if global_defaults_initialized? # The first two phases of the lifecycle of a puppet application are: # 1) Parse the command line options and handle any of them that are # registered, defined "global" puppet settings (mostly from defaults.rb). # 2) Parse the puppet config file(s). parse_global_options(args) parse_config_files @global_defaults_initialized = true end # This method is called during application bootstrapping. It is responsible for parsing all of the # command line options and initializing the settings accordingly. # # It will ignore options that are not defined in the global puppet settings list, because they may # be valid options for the specific application that we are about to launch... however, at this point # in the bootstrapping lifecycle, we don't yet know what that application is. def parse_global_options(args) # Create an option parser option_parser = PuppetOptionParser.new option_parser.ignore_invalid_options = true # Add all global options to it. self.optparse_addargs([]).each do |option| option_parser.on(*option) do |arg| opt, val = Puppet::Settings.clean_opt(option[0], arg) handlearg(opt, val) end end option_parser.on('--run_mode', "The effective 'run mode' of the application: master, agent, or user.", :REQUIRED) do |arg| Puppet.settings.preferred_run_mode = arg end option_parser.parse(args) # remove run_mode options from the arguments so that later parses don't think # it is an unknown option. while option_index = args.index('--run_mode') do args.delete_at option_index args.delete_at option_index end args.reject! { |arg| arg.start_with? '--run_mode=' } end private :parse_global_options # A utility method (public, is used by application.rb and perhaps elsewhere) that munges a command-line # option string into the format that Puppet.settings expects. (This mostly has to deal with handling the # "no-" prefix on flag/boolean options). # # @param [String] opt the command line option that we are munging # @param [String, TrueClass, FalseClass] val the value for the setting (as determined by the OptionParser) def self.clean_opt(opt, val) # rewrite --[no-]option to --no-option if that's what was given if opt =~ /\[no-\]/ and !val opt = opt.gsub(/\[no-\]/,'no-') end # otherwise remove the [no-] prefix to not confuse everybody opt = opt.gsub(/\[no-\]/, '') [opt, val] end def app_defaults_initialized? @app_defaults_initialized end def initialize_app_defaults(app_defaults) REQUIRED_APP_SETTINGS.each do |key| raise SettingsError, "missing required app default setting '#{key}'" unless app_defaults.has_key?(key) end app_defaults.each do |key, value| if key == :run_mode self.preferred_run_mode = value else @value_sets[:application_defaults].set(key, value) unsafe_flush_cache end end apply_metadata call_hooks_deferred_to_application_initialization issue_deprecations @app_defaults_initialized = true end def call_hooks_deferred_to_application_initialization(options = {}) @hooks_to_call_on_application_initialization.each do |setting| begin setting.handle(self.value(setting.name)) rescue InterpolationError => err raise InterpolationError, err, err.backtrace unless options[:ignore_interpolation_dependency_errors] #swallow. We're not concerned if we can't call hooks because dependencies don't exist yet #we'll get another chance after application defaults are initialized end end end private :call_hooks_deferred_to_application_initialization # Return a value's description. def description(name) if obj = @config[name.to_sym] obj.desc else nil end end def_delegator :@config, :each # Iterate over each section name. def eachsection yielded = [] @config.each do |name, object| section = object.section unless yielded.include? section yield section yielded << section end end end # Return an object by name. def setting(param) param = param.to_sym @config[param] end # Handle a command-line argument. def handlearg(opt, value = nil) @cache.clear if value.is_a?(FalseClass) value = "false" elsif value.is_a?(TrueClass) value = "true" end value &&= @translate[value] str = opt.sub(/^--/,'') bool = true newstr = str.sub(/^no-/, '') if newstr != str str = newstr bool = false end str = str.intern if @config[str].is_a?(Puppet::Settings::BooleanSetting) if value == "" or value.nil? value = bool end end if s = @config[str] @deprecated_settings_that_have_been_configured << s if s.completely_deprecated? end @value_sets[:cli].set(str, value) unsafe_flush_cache end def include?(name) name = name.intern if name.is_a? String @config.include?(name) end # check to see if a short name is already defined def shortinclude?(short) short = short.intern if name.is_a? String @shortnames.include?(short) end # Prints the contents of a config file with the available config settings, or it # prints a single value of a config setting. def print_config_options env = value(:environment) val = value(:configprint) if val == "all" hash = {} each do |name, obj| val = value(name,env) val = val.inspect if val == "" hash[name] = val end hash.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, val| puts "#{name} = #{val}" end else val.split(/\s*,\s*/).sort.each do |v| if include?(v) #if there is only one value, just print it for back compatibility if v == val puts value(val,env) break end puts "#{v} = #{value(v,env)}" else puts "invalid setting: #{v}" return false end end end true end def generate_config puts to_config true end def generate_manifest puts to_manifest true end def print_configs return print_config_options if value(:configprint) != "" return generate_config if value(:genconfig) generate_manifest if value(:genmanifest) end def print_configs? (value(:configprint) != "" || value(:genconfig) || value(:genmanifest)) && true end # Return a given object's file metadata. def metadata(param) if obj = @config[param.to_sym] and obj.is_a?(FileSetting) { :owner => obj.owner, :group => obj.group, :mode => obj.mode }.delete_if { |key, value| value.nil? } else nil end end # Make a directory with the appropriate user, group, and mode def mkdir(default) obj = get_config_file_default(default) Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do mode = obj.mode || 0750 Dir.mkdir(obj.value, mode) end end # The currently configured run mode that is preferred for constructing the application configuration. def preferred_run_mode @preferred_run_mode_name || :user end # PRIVATE! This only exists because we need a hook to validate the run mode when it's being set, and # it should never, ever, ever, ever be called from outside of this file. # This method is also called when --run_mode MODE is used on the command line to set the default # # @param mode [String|Symbol] the name of the mode to have in effect # @api private def preferred_run_mode=(mode) mode = mode.to_s.downcase.intern raise ValidationError, "Invalid run mode '#{mode}'" unless [:master, :agent, :user].include?(mode) @preferred_run_mode_name = mode # Changing the run mode has far-reaching consequences. Flush any cached # settings so they will be re-generated. flush_cache mode end # Return all of the settings associated with a given section. def params(section = nil) if section section = section.intern if section.is_a? String @config.find_all { |name, obj| obj.section == section }.collect { |name, obj| name } else @config.keys end end def parse_config(text, file = "text") begin data = @config_file_parser.parse_file(file, text) rescue => detail Puppet.log_exception(detail, "Could not parse #{file}: #{detail}") return end # If we get here and don't have any data, we just return and don't muck with the current state of the world. return if data.nil? # If we get here then we have some data, so we need to clear out any # previous settings that may have come from config files. unsafe_clear(false, false) record_deprecations_from_puppet_conf(data) # And now we can repopulate with the values from our last parsing of the config files. @configuration_file = data # Determine our environment, if we have one. if @config[:environment] env = self.value(:environment).to_sym else env = "none" end # Call any hooks we should be calling. value_sets = value_sets_for(env, preferred_run_mode) @config.values.select(&:has_hook?).each do |setting| value_sets.each do |source| if source.include?(setting.name) # We still have to use value to retrieve the value, since # we want the fully interpolated value, not $vardir/lib or whatever. # This results in extra work, but so few of the settings # will have associated hooks that it ends up being less work this # way overall. if setting.call_hook_on_initialize? @hooks_to_call_on_application_initialization << setting else setting.handle(ChainedValues.new( preferred_run_mode, env, value_sets, @config).interpolate(setting.name)) end break end end end call_hooks_deferred_to_application_initialization :ignore_interpolation_dependency_errors => true apply_metadata end # Parse the configuration file. Just provides thread safety. def parse_config_files file = which_configuration_file if Puppet::FileSystem.exist?(file) begin text = read_file(file) rescue => detail Puppet.log_exception(detail, "Could not load #{file}: #{detail}") return end else return end parse_config(text, file) end private :parse_config_files def main_config_file if explicit_config_file? return self[:config] else return File.join(Puppet::Util::RunMode[:master].conf_dir, config_file_name) end end private :main_config_file def user_config_file return File.join(Puppet::Util::RunMode[:user].conf_dir, config_file_name) end private :user_config_file # This method is here to get around some life-cycle issues. We need to be # able to determine the config file name before the settings / defaults are # fully loaded. However, we also need to respect any overrides of this value # that the user may have specified on the command line. # # The easiest way to do this is to attempt to read the setting, and if we # catch an error (meaning that it hasn't been set yet), we'll fall back to # the default value. def config_file_name begin return self[:config_file_name] if self[:config_file_name] rescue SettingsError # This just means that the setting wasn't explicitly set on the command line, so we will ignore it and # fall through to the default name. end return self.class.default_config_file_name end private :config_file_name def apply_metadata # We have to do it in the reverse of the search path, # because multiple sections could set the same value # and I'm too lazy to only set the metadata once. if @configuration_file searchpath.reverse.each do |source| source = preferred_run_mode if source == :run_mode if section = @configuration_file.sections[source] apply_metadata_from_section(section) end end end end private :apply_metadata def apply_metadata_from_section(section) section.settings.each do |setting| if setting.has_metadata? && type = @config[setting.name] type.set_meta(setting.meta) end end end SETTING_TYPES = { :string => StringSetting, :file => FileSetting, :directory => DirectorySetting, :file_or_directory => FileOrDirectorySetting, :path => PathSetting, :boolean => BooleanSetting, :terminus => TerminusSetting, :duration => DurationSetting, :ttl => TTLSetting, :array => ArraySetting, :enum => EnumSetting, :priority => PrioritySetting, :autosign => AutosignSetting, } # Create a new setting. The value is passed in because it's used to determine # what kind of setting we're creating, but the value itself might be either # a default or a value, so we can't actually assign it. # # See #define_settings for documentation on the legal values for the ":type" option. def newsetting(hash) klass = nil hash[:section] = hash[:section].to_sym if hash[:section] if type = hash[:type] unless klass = SETTING_TYPES[type] raise ArgumentError, "Invalid setting type '#{type}'" end hash.delete(:type) else # The only implicit typing we still do for settings is to fall back to "String" type if they didn't explicitly # specify a type. Personally I'd like to get rid of this too, and make the "type" option mandatory... but # there was a little resistance to taking things quite that far for now. --cprice 2012-03-19 klass = StringSetting end hash[:settings] = self setting = klass.new(hash) setting end # This has to be private, because it doesn't add the settings to @config private :newsetting # Iterate across all of the objects in a given section. def persection(section) section = section.to_sym self.each { |name, obj| if obj.section == section yield obj end } end # Reparse our config file, if necessary. def reparse_config_files if files if filename = any_files_changed? Puppet.notice "Config file #{filename} changed; triggering re-parse of all config files." parse_config_files reuse end end end def files return @files if @files @files = [] [main_config_file, user_config_file].each do |path| if Puppet::FileSystem.exist?(path) @files << Puppet::Util::WatchedFile.new(path) end end @files end private :files # Checks to see if any of the config files have been modified # @return the filename of the first file that is found to have changed, or # nil if no files have changed def any_files_changed? files.each do |file| return file.to_str if file.changed? end nil end private :any_files_changed? def reuse return unless defined?(@used) new = @used @used = [] self.use(*new) end # The order in which to search for values. def searchpath(environment = nil) [:memory, :cli, environment, :run_mode, :main, :application_defaults, :overridden_defaults].compact end # Get a list of objects per section def sectionlist sectionlist = [] self.each { |name, obj| section = obj.section || "puppet" sections[section] ||= [] sectionlist << section unless sectionlist.include?(section) sections[section] << obj } return sectionlist, sections end def service_user_available? return @service_user_available if defined?(@service_user_available) if self[:user] user = Puppet::Type.type(:user).new :name => self[:user], :audit => :ensure @service_user_available = user.exists? else @service_user_available = false end end def service_group_available? return @service_group_available if defined?(@service_group_available) if self[:group] group = Puppet::Type.type(:group).new :name => self[:group], :audit => :ensure @service_group_available = group.exists? else @service_group_available = false end end # Allow later inspection to determine if the setting was set on the # command line, or through some other code path. Used for the # `dns_alt_names` option during cert generate. --daniel 2011-10-18 def set_by_cli?(param) param = param.to_sym !@value_sets[:cli].lookup(param).nil? end def set_value(param, value, type, options = {}) Puppet.deprecation_warning("Puppet.settings.set_value is deprecated. Use Puppet[]= instead.") if @value_sets[type] @value_sets[type].set(param, value) unsafe_flush_cache end end # Deprecated; use #define_settings instead def setdefaults(section, defs) Puppet.deprecation_warning("'setdefaults' is deprecated and will be removed; please call 'define_settings' instead") define_settings(section, defs) end # Define a group of settings. # # @param [Symbol] section a symbol to use for grouping multiple settings together into a conceptual unit. This value # (and the conceptual separation) is not used very often; the main place where it will have a potential impact # is when code calls Settings#use method. See docs on that method for further details, but basically that method # just attempts to do any preparation that may be necessary before code attempts to leverage the value of a particular # setting. This has the most impact for file/directory settings, where #use will attempt to "ensure" those # files / directories. # @param [Hash[Hash]] defs the settings to be defined. This argument is a hash of hashes; each key should be a symbol, # which is basically the name of the setting that you are defining. The value should be another hash that specifies # the parameters for the particular setting. Legal values include: # [:default] => not required; this is the value for the setting if no other value is specified (via cli, config file, etc.) # For string settings this may include "variables", demarcated with $ or ${} which will be interpolated with values of other settings. # The default value may also be a Proc that will be called only once to evaluate the default when the setting's value is retrieved. # [:desc] => required; a description of the setting, used in documentation / help generation # [:type] => not required, but highly encouraged! This specifies the data type that the setting represents. If # you do not specify it, it will default to "string". Legal values include: # :string - A generic string setting # :boolean - A boolean setting; values are expected to be "true" or "false" # :file - A (single) file path; puppet may attempt to create this file depending on how the settings are used. This type # also supports additional options such as "mode", "owner", "group" # :directory - A (single) directory path; puppet may attempt to create this file depending on how the settings are used. This type # also supports additional options such as "mode", "owner", "group" # :path - This is intended to be used for settings whose value can contain multiple directory paths, respresented # as strings separated by the system path separator (e.g. system path, module path, etc.). # [:mode] => an (optional) octal value to be used as the permissions/mode for :file and :directory settings # [:owner] => optional owner username/uid for :file and :directory settings # [:group] => optional group name/gid for :file and :directory settings # def define_settings(section, defs) section = section.to_sym call = [] defs.each do |name, hash| raise ArgumentError, "setting definition for '#{name}' is not a hash!" unless hash.is_a? Hash name = name.to_sym hash[:name] = name hash[:section] = section raise ArgumentError, "Setting #{name} is already defined" if @config.include?(name) tryconfig = newsetting(hash) if short = tryconfig.short if other = @shortnames[short] raise ArgumentError, "Setting #{other.name} is already using short name '#{short}'" end @shortnames[short] = tryconfig end @config[name] = tryconfig # Collect the settings that need to have their hooks called immediately. # We have to collect them so that we can be sure we're fully initialized before # the hook is called. if tryconfig.has_hook? if tryconfig.call_hook_on_define? call << tryconfig elsif tryconfig.call_hook_on_initialize? @hooks_to_call_on_application_initialization << tryconfig end end @deprecated_setting_names << name if tryconfig.deprecated? end call.each do |setting| setting.handle(self.value(setting.name)) end end # Convert the settings we manage into a catalog full of resources that model those settings. def to_catalog(*sections) sections = nil if sections.empty? catalog = Puppet::Resource::Catalog.new("Settings", Puppet::Node::Environment::NONE) @config.keys.find_all { |key| @config[key].is_a?(FileSetting) }.each do |key| next if (key == :manifestdir && should_skip_manifestdir?()) file = @config[key] next unless (sections.nil? or sections.include?(file.section)) next unless resource = file.to_resource next if catalog.resource(resource.ref) Puppet.debug("Using settings: adding file resource '#{key}': '#{resource.inspect}'") catalog.add_resource(resource) end add_user_resources(catalog, sections) add_environment_resources(catalog, sections) catalog end def should_skip_manifestdir?() setting = @config[:environmentpath] !(setting.nil? || setting.value.nil? || setting.value.empty?) end private :should_skip_manifestdir? # Convert our list of config settings into a configuration file. def to_config str = %{The configuration file for #{Puppet.run_mode.name}. Note that this file is likely to have unused settings in it; any setting that's valid anywhere in Puppet can be in any config file, even if it's not used. Every section can specify three special parameters: owner, group, and mode. These parameters affect the required permissions of any files specified after their specification. Puppet will sometimes use these parameters to check its own configured state, so they can be used to make Puppet a bit more self-managing. The file format supports octothorpe-commented lines, but not partial-line comments. Generated on #{Time.now}. }.gsub(/^/, "# ") # Add a section heading that matches our name. str += "[#{preferred_run_mode}]\n" eachsection do |section| persection(section) do |obj| str += obj.to_config + "\n" unless obj.name == :genconfig end end return str end # Convert to a parseable manifest def to_manifest catalog = to_catalog catalog.resource_refs.collect do |ref| catalog.resource(ref).to_manifest end.join("\n\n") end # Create the necessary objects to use a section. This is idempotent; # you can 'use' a section as many times as you want. def use(*sections) sections = sections.collect { |s| s.to_sym } sections = sections.reject { |s| @used.include?(s) } return if sections.empty? begin catalog = to_catalog(*sections).to_ral rescue => detail Puppet.log_and_raise(detail, "Could not create resources for managing Puppet's files and directories in sections #{sections.inspect}: #{detail}") end catalog.host_config = false catalog.apply do |transaction| if transaction.any_failed? report = transaction.report status_failures = report.resource_statuses.values.select { |r| r.failed? } status_fail_msg = status_failures. collect(&:events). flatten. select { |event| event.status == 'failure' }. collect { |event| "#{event.resource}: #{event.message}" }.join("; ") raise "Got #{status_failures.length} failure(s) while initializing: #{status_fail_msg}" end end sections.each { |s| @used << s } @used.uniq! end def valid?(param) param = param.to_sym @config.has_key?(param) end def uninterpolated_value(param, environment = nil) Puppet.deprecation_warning("Puppet.settings.uninterpolated_value is deprecated. Use Puppet.settings.value instead") param = param.to_sym environment &&= environment.to_sym values(environment, self.preferred_run_mode).lookup(param) end # Retrieve an object that can be used for looking up values of configuration # settings. # # @param environment [Symbol] The name of the environment in which to lookup # @param section [Symbol] The name of the configuration section in which to lookup # @return [Puppet::Settings::ChainedValues] An object to perform lookups # @api public def values(environment, section) @values[environment][section] ||= ChainedValues.new( section, environment, value_sets_for(environment, section), @config) end # Find the correct value using our search path. # # @param param [String, Symbol] The value to look up # @param environment [String, Symbol] The environment to check for the value # @param bypass_interpolation [true, false] Whether to skip interpolation # # @return [Object] The looked up value # # @raise [InterpolationError] def value(param, environment = nil, bypass_interpolation = false) param = param.to_sym environment &&= environment.to_sym setting = @config[param] # Short circuit to nil for undefined settings. return nil if setting.nil? # Check the cache first. It needs to be a per-environment # cache so that we don't spread values from one env # to another. if @cache[environment||"none"].has_key?(param) return @cache[environment||"none"][param] elsif bypass_interpolation val = values(environment, self.preferred_run_mode).lookup(param) else val = values(environment, self.preferred_run_mode).interpolate(param) end @cache[environment||"none"][param] = val val end ## # (#15337) All of the logic to determine the configuration file to use # should be centralized into this method. The simplified approach is: # # 1. If there is an explicit configuration file, use that. (--confdir or # --config) # 2. If we're running as a root process, use the system puppet.conf # (usually /etc/puppet/puppet.conf) # 3. Otherwise, use the user puppet.conf (usually ~/.puppet/puppet.conf) # # @api private # @todo this code duplicates {Puppet::Util::RunMode#which_dir} as described # in {http://projects.puppetlabs.com/issues/16637 #16637} def which_configuration_file if explicit_config_file? or Puppet.features.root? then return main_config_file else return user_config_file end end # This method just turns a file into a new ConfigFile::Conf instance # @param file [String] absolute path to the configuration file # @return [Puppet::Settings::ConfigFile::Conf] # @api private def parse_file(file) @config_file_parser.parse_file(file, read_file(file)) end private DEPRECATION_REFS = { [:manifest, :modulepath, :config_version, :templatedir, :manifestdir] => "See http://links.puppetlabs.com/env-settings-deprecations" }.freeze # Record that we want to issue a deprecation warning later in the application # initialization cycle when we have settings bootstrapped to the point where # we can read the Puppet[:disable_warnings] setting. # # We are only recording warnings applicable to settings set in puppet.conf # itself. def record_deprecations_from_puppet_conf(puppet_conf) conf_sections = puppet_conf.sections.inject([]) do |accum,entry| accum << entry[1] if [:main, :master, :agent, :user].include?(entry[0]) accum end conf_sections.each do |section| section.settings.each do |conf_setting| if setting = self.setting(conf_setting.name) @deprecated_settings_that_have_been_configured << setting if setting.deprecated? end end end end def issue_deprecations @deprecated_settings_that_have_been_configured.each do |setting| issue_deprecation_warning(setting) end end def issue_deprecation_warning(setting, msg = nil) name = setting.name ref = DEPRECATION_REFS.find { |params,reference| params.include?(name) } ref = ref[1] if ref case when msg msg << " #{ref}" if ref Puppet.deprecation_warning(msg) when setting.completely_deprecated? Puppet.deprecation_warning("Setting #{name} is deprecated. #{ref}", "setting-#{name}") when setting.allowed_on_commandline? Puppet.deprecation_warning("Setting #{name} is deprecated in puppet.conf. #{ref}", "puppet-conf-setting-#{name}") end end def get_config_file_default(default) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default #{default}" end raise ArgumentError, "Default #{default} is not a file" unless obj.is_a? FileSetting obj end def add_environment_resources(catalog, sections) path = self[:environmentpath] envdir = path.split(File::PATH_SEPARATOR).first if path configured_environment = self[:environment] if configured_environment == "production" && envdir && Puppet::FileSystem.exist?(envdir) configured_environment_path = File.join(envdir, configured_environment) if !Puppet::FileSystem.symlink?(configured_environment_path) catalog.add_resource( Puppet::Resource.new(:file, configured_environment_path, :parameters => { :ensure => 'directory' }) ) end end end def add_user_resources(catalog, sections) return unless Puppet.features.root? return if Puppet.features.microsoft_windows? return unless self[:mkusers] @config.each do |name, setting| next unless setting.respond_to?(:owner) next unless sections.nil? or sections.include?(setting.section) if user = setting.owner and user != "root" and catalog.resource(:user, user).nil? resource = Puppet::Resource.new(:user, user, :parameters => {:ensure => :present}) resource[:gid] = self[:group] if self[:group] catalog.add_resource resource end if group = setting.group and ! %w{root wheel}.include?(group) and catalog.resource(:group, group).nil? catalog.add_resource Puppet::Resource.new(:group, group, :parameters => {:ensure => :present}) end end end # Yield each search source in turn. def value_sets_for(environment, mode) searchpath(environment).collect do |name| case name when :cli, :memory, :application_defaults, :overridden_defaults @value_sets[name] when :run_mode if @configuration_file section = @configuration_file.sections[mode] if section ValuesFromSection.new(mode, section) end end else values_from_section = nil if @configuration_file if section = @configuration_file.sections[name] values_from_section = ValuesFromSection.new(name, section) end end if values_from_section.nil? && global_defaults_initialized? values_from_section = ValuesFromEnvironmentConf.new(name) end values_from_section end end.compact end # Read the file in. # @api private def read_file(file) return Puppet::FileSystem.read(file) end # Private method for internal test use only; allows to do a comprehensive clear of all settings between tests. # # @return nil def clear_everything_for_tests() unsafe_clear(true, true) @configuration_file = nil @global_defaults_initialized = false @app_defaults_initialized = false end private :clear_everything_for_tests def explicit_config_file? # Figure out if the user has provided an explicit configuration file. If # so, return the path to the file, if not return nil. # # The easiest way to determine whether an explicit one has been specified # is to simply attempt to evaluate the value of ":config". This will # obviously be successful if they've passed an explicit value for :config, # but it will also result in successful interpolation if they've only # passed an explicit value for :confdir. # # If they've specified neither, then the interpolation will fail and we'll # get an exception. # begin return true if self[:config] rescue InterpolationError # This means we failed to interpolate, which means that they didn't # explicitly specify either :config or :confdir... so we'll fall out to # the default value. return false end end private :explicit_config_file? # Lookup configuration setting value through a chain of different value sources. # # @api public class ChainedValues ENVIRONMENT_SETTING = "environment".freeze ENVIRONMENT_INTERPOLATION_ALLOWED = ['config_version'].freeze # @see Puppet::Settings.values # @api private def initialize(mode, environment, value_sets, defaults) @mode = mode @environment = environment @value_sets = value_sets @defaults = defaults end # Lookup the uninterpolated value. # # @param name [Symbol] The configuration setting name to look up # @return [Object] The configuration setting value or nil if the setting is not known # @api public def lookup(name) set = @value_sets.find do |set| set.include?(name) end if set value = set.lookup(name) if !value.nil? return value end end @defaults[name].default end # Lookup the interpolated value. All instances of `$name` in the value will # be replaced by performing a lookup of `name` and substituting the text # for `$name` in the original value. This interpolation is only performed # if the looked up value is a String. # # @param name [Symbol] The configuration setting name to look up # @return [Object] The configuration setting value or nil if the setting is not known # @api public def interpolate(name) setting = @defaults[name] if setting val = lookup(name) # if we interpolate code, all hell breaks loose. if name == :code val else # Convert it if necessary begin val = convert(val, name) rescue InterpolationError => err # This happens because we don't have access to the param name when the # exception is originally raised, but we want it in the message raise InterpolationError, "Error converting value for param '#{name}': #{err}", err.backtrace end setting.munge(val) end else nil end end private def convert(value, setting_name) case value when nil nil when String failed_environment_interpolation = false interpolated_value = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |expression| varname = $2 || $1 interpolated_expression = if varname != ENVIRONMENT_SETTING || ok_to_interpolate_environment(setting_name) if varname == ENVIRONMENT_SETTING && @environment @environment elsif varname == "run_mode" @mode elsif !(pval = interpolate(varname.to_sym)).nil? pval else raise InterpolationError, "Could not find value for #{expression}" end else failed_environment_interpolation = true expression end interpolated_expression end if failed_environment_interpolation Puppet.warning("You cannot interpolate $environment within '#{setting_name}' when using directory environments. Its value will remain #{interpolated_value}.") end interpolated_value else value end end def ok_to_interpolate_environment(setting_name) return true if Puppet.settings.value(:environmentpath, nil, true).empty? ENVIRONMENT_INTERPOLATION_ALLOWED.include?(setting_name.to_s) end end class Values extend Forwardable def initialize(name, defaults) @name = name @values = {} @defaults = defaults end def_delegator :@values, :include? def_delegator :@values, :[], :lookup def set(name, value) default = @defaults[name] if !default raise ArgumentError, "Attempt to assign a value to unknown setting #{name.inspect}" end if default.has_hook? default.handle(value) end @values[name] = value end end class ValuesFromSection def initialize(name, section) @name = name @section = section end def include?(name) !@section.setting(name).nil? end def lookup(name) setting = @section.setting(name) if setting setting.value end end end # @api private class ValuesFromEnvironmentConf def initialize(environment_name) @environment_name = environment_name end def include?(name) if Puppet::Settings::EnvironmentConf::VALID_SETTINGS.include?(name) && conf return true end false end def lookup(name) return nil unless Puppet::Settings::EnvironmentConf::VALID_SETTINGS.include?(name) conf.send(name) if conf end def conf @conf ||= if environments = Puppet.lookup(:environments) environments.get_conf(@environment_name) end end end end puppet/property/ensure.rb000064400000006036147634041270011610 0ustar00require 'puppet/property' # This property is automatically added to any {Puppet::Type} that responds # to the methods 'exists?', 'create', and 'destroy'. # # Ensure defaults to having the wanted _(should)_ value `:present`. # # @api public # class Puppet::Property::Ensure < Puppet::Property @name = :ensure def self.defaultvalues newvalue(:present) do if @resource.provider and @resource.provider.respond_to?(:create) @resource.provider.create else @resource.create end nil # return nil so the event is autogenerated end newvalue(:absent) do if @resource.provider and @resource.provider.respond_to?(:destroy) @resource.provider.destroy else @resource.destroy end nil # return nil so the event is autogenerated end defaultto do if @resource.managed? :present else nil end end # This doc will probably get overridden @doc ||= "The basic property that the resource should be in." end def self.inherited(sub) # Add in the two properties that everyone will have. sub.class_eval do end end def change_to_s(currentvalue, newvalue) begin if currentvalue == :absent or currentvalue.nil? return "created" elsif newvalue == :absent return "removed" else return "#{self.name} changed '#{self.is_to_s(currentvalue)}' to '#{self.should_to_s(newvalue)}'" end rescue Puppet::Error, Puppet::DevError raise rescue => detail raise Puppet::DevError, "Could not convert change #{self.name} to string: #{detail}", detail.backtrace end end # Retrieves the _is_ value for the ensure property. # The existence of the resource is checked by first consulting the provider (if it responds to # `:exists`), and secondly the resource. A a value of `:present` or `:absent` is returned # depending on if the managed entity exists or not. # # @return [Symbol] a value of `:present` or `:absent` depending on if it exists or not # @raise [Puppet::DevError] if neither the provider nor the resource responds to `:exists` # def retrieve # XXX This is a problem -- whether the object exists or not often # depends on the results of other properties, yet we're the first property # to get checked, which means that those other properties do not have # @is values set. This seems to be the source of quite a few bugs, # although they're mostly logging bugs, not functional ones. if prov = @resource.provider and prov.respond_to?(:exists?) result = prov.exists? elsif @resource.respond_to?(:exists?) result = @resource.exists? else raise Puppet::DevError, "No ability to determine if #{@resource.class.name} exists" end if result return :present else return :absent end end # If they're talking about the thing at all, they generally want to # say it should exist. #defaultto :present defaultto do if @resource.managed? :present else nil end end end puppet/property/ordered_list.rb000064400000001474147634041270012767 0ustar00require 'puppet/property/list' module Puppet class Property # This subclass of {Puppet::Property} manages an ordered list of values. # The maintained order is the order defined by the 'current' set of values (i.e. the # original order is not disrupted). Any additions are added after the current values # in their given order). # # For an unordered list see {Puppet::Property::List}. # class OrderedList < List def add_should_with_current(should, current) if current.is_a?(Array) #tricky trick #Preserve all the current items in the list #but move them to the back of the line should = should + (current - should) end should end def dearrayify(array) array.join(delimiter) end end end end puppet/property/list.rb000064400000003453147634041270011262 0ustar00require 'puppet/property' module Puppet class Property # This subclass of {Puppet::Property} manages an unordered list of values. # For an ordered list see {Puppet::Property::OrderedList}. # class List < Property def should_to_s(should_value) #just return the should value should_value end def is_to_s(currentvalue) if currentvalue == :absent return "absent" else return currentvalue.join(delimiter) end end def membership :membership end def add_should_with_current(should, current) should += current if current.is_a?(Array) should.uniq end def inclusive? @resource[membership] == :inclusive end #dearrayify was motivated because to simplify the implementation of the OrderedList property def dearrayify(array) array.sort.join(delimiter) end def should return nil unless @should members = @should #inclusive means we are managing everything so if it isn't in should, its gone members = add_should_with_current(members, retrieve) if ! inclusive? dearrayify(members) end def delimiter "," end def retrieve #ok, some 'convention' if the list property is named groups, provider should implement a groups method if provider and tmp = provider.send(name) and tmp != :absent return tmp.split(delimiter) else return :absent end end def prepare_is_for_comparison(is) if is == :absent is = [] end dearrayify(is) end def insync?(is) return true unless is (prepare_is_for_comparison(is) == self.should) end end end end puppet/property/keyvalue.rb000064400000005414147634041270012133 0ustar00require 'puppet/property' module Puppet class Property # This subclass of {Puppet::Property} manages string key value pairs. # In order to use this property: # # * the _should_ value must be an array of key-value pairs separated by the 'separator' # * the retrieve method should return a hash with the keys as symbols # @note **IMPORTANT**: In order for this property to work there must also be a 'membership' parameter # The class that inherits from property should override that method with the symbol for the membership # @todo The node with an important message is not very clear. # class KeyValue < Property def hash_to_key_value_s(hash) hash.select { |k,v| true }.map { |pair| pair.join(separator) }.join(delimiter) end def should_to_s(should_value) hash_to_key_value_s(should_value) end def is_to_s(current_value) hash_to_key_value_s(current_value) end def membership :key_value_membership end def inclusive? @resource[membership] == :inclusive end def hashify(key_value_array) #turns string array into a hash key_value_array.inject({}) do |hash, key_value| tmp = key_value.split(separator) hash[tmp[0].intern] = tmp[1] hash end end def process_current_hash(current) return {} if current == :absent #inclusive means we are managing everything so if it isn't in should, its gone current.each_key { |key| current[key] = nil } if inclusive? current end def should return nil unless @should members = hashify(@should) current = process_current_hash(retrieve) #shared keys will get overwritten by members current.merge(members) end # @return [String] Returns a default separator of "=" def separator "=" end # @return [String] Returns a default delimiter of ";" def delimiter ";" end # Retrieves the key-hash from the provider by invoking its method named the same as this property. # @return [Hash] the hash from the provider, or `:absent` # def retrieve #ok, some 'convention' if the keyvalue property is named properties, provider should implement a properties method if key_hash = provider.send(name) and key_hash != :absent return key_hash else return :absent end end # Returns true if there is no _is_ value, else returns if _is_ is equal to _should_ using == as comparison. # @return [Boolean] whether the property is in sync or not. # def insync?(is) return true unless is (is == self.should) end end end end puppet/property/boolean.rb000064400000000226147634041270011721 0ustar00require 'puppet/coercion' class Puppet::Property::Boolean < Puppet::Property def unsafe_munge(value) Puppet::Coercion.boolean(value) end end puppet/configurer/downloader_factory.rb000064400000001625147634041270014452 0ustar00require 'puppet/configurer' # Factory for Puppet::Configurer::Downloader objects. # # Puppet's pluginsync facilities can be used to download modules # and external facts, each with a different destination directory # and related settings. # # @api private # class Puppet::Configurer::DownloaderFactory def create_plugin_downloader(environment) plugin_downloader = Puppet::Configurer::Downloader.new( "plugin", Puppet[:plugindest], Puppet[:pluginsource], Puppet[:pluginsignore], environment ) end def create_plugin_facts_downloader(environment) source_permissions = Puppet.features.microsoft_windows? ? :ignore : :use plugin_fact_downloader = Puppet::Configurer::Downloader.new( "pluginfacts", Puppet[:pluginfactdest], Puppet[:pluginfactsource], Puppet[:pluginsignore], environment, source_permissions ) end end puppet/configurer/downloader.rb000064400000003165147634041270012724 0ustar00require 'puppet/configurer' require 'puppet/resource/catalog' class Puppet::Configurer::Downloader attr_reader :name, :path, :source, :ignore # Evaluate our download, returning the list of changed values. def evaluate Puppet.info "Retrieving #{name}" files = [] begin catalog.apply do |trans| trans.changed?.each do |resource| yield resource if block_given? files << resource[:path] end end rescue Puppet::Error => detail Puppet.log_exception(detail, "Could not retrieve #{name}: #{detail}") end files end def initialize(name, path, source, ignore = nil, environment = nil, source_permissions = :ignore) @name, @path, @source, @ignore, @environment, @source_permissions = name, path, source, ignore, environment, source_permissions end def catalog catalog = Puppet::Resource::Catalog.new("PluginSync", @environment) catalog.host_config = false catalog.add_resource(file) catalog end def file args = default_arguments.merge(:path => path, :source => source) args[:ignore] = ignore.split if ignore Puppet::Type.type(:file).new(args) end private def default_arguments defargs = { :path => path, :recurse => true, :source => source, :source_permissions => @source_permissions, :tag => name, :purge => true, :force => true, :backup => false, :noop => false } if !Puppet.features.microsoft_windows? defargs.merge!( { :owner => Process.uid, :group => Process.gid } ) end return defargs end end puppet/configurer/plugin_handler.rb000064400000001231147634041270013551 0ustar00# Break out the code related to plugins. This module is # just included into the agent, but having it here makes it # easier to test. require 'puppet/configurer' class Puppet::Configurer::PluginHandler def initialize(factory) @factory = factory end # Retrieve facts from the central server. def download_plugins(environment) plugin_downloader = @factory.create_plugin_downloader(environment) if Puppet.features.external_facts? plugin_fact_downloader = @factory.create_plugin_facts_downloader(environment) plugin_fact_downloader.evaluate end plugin_downloader.evaluate Puppet::Util::Autoload.reload_changed end end puppet/configurer/fact_handler.rb000064400000002267147634041270013202 0ustar00require 'puppet/indirector/facts/facter' require 'puppet/configurer' require 'puppet/configurer/downloader' # Break out the code related to facts. This module is # just included into the agent, but having it here makes it # easier to test. module Puppet::Configurer::FactHandler def find_facts # This works because puppet agent configures Facts to use 'facter' for # finding facts and the 'rest' terminus for caching them. Thus, we'll # compile them and then "cache" them on the server. begin facts = Puppet::Node::Facts.indirection.find(Puppet[:node_name_value], :environment => @environment) unless Puppet[:node_name_fact].empty? Puppet[:node_name_value] = facts.values[Puppet[:node_name_fact]] facts.name = Puppet[:node_name_value] end facts rescue SystemExit,NoMemoryError raise rescue Exception => detail message = "Could not retrieve local facts: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message, detail.backtrace end end def facts_for_uploading facts = find_facts text = facts.render(:pson) {:facts_format => :pson, :facts => CGI.escape(text)} end end puppet/file_bucket.rb000064400000000115147634041270010667 0ustar00# stub module Puppet::FileBucket class BucketError < RuntimeError; end end puppet/ssl/certificate_signer.rb000064400000001075147634041270013053 0ustar00# Take care of signing a certificate in a FIPS 140-2 compliant manner. # # @see http://projects.puppetlabs.com/issues/17295 # # @api private class Puppet::SSL::CertificateSigner def initialize if OpenSSL::Digest.const_defined?('SHA256') @digest = OpenSSL::Digest::SHA256 elsif OpenSSL::Digest.const_defined?('SHA1') @digest = OpenSSL::Digest::SHA1 else raise Puppet::Error, "No FIPS 140-2 compliant digest algorithm in OpenSSL::Digest" end @digest end def sign(content, key) content.sign(key, @digest.new) end end puppet/ssl/certificate_request.rb000064400000025637147634041270013266 0ustar00require 'puppet/ssl/base' require 'puppet/ssl/certificate_signer' # This class creates and manages X509 certificate signing requests. # # ## CSR attributes # # CSRs may contain a set of attributes that includes supplementary information # about the CSR or information for the signed certificate. # # PKCS#9/RFC 2985 section 5.4 formally defines the "Challenge password", # "Extension request", and "Extended-certificate attributes", but this # implementation only handles the "Extension request" attribute. Other # attributes may be defined on a CSR, but the RFC doesn't define behavior for # any other attributes so we treat them as only informational. # # ## CSR Extension request attribute # # CSRs may contain an optional set of extension requests, which allow CSRs to # include additional information that may be included in the signed # certificate. Any additional information that should be copied from the CSR # to the signed certificate MUST be included in this attribute. # # This behavior is dictated by PKCS#9/RFC 2985 section 5.4.2. # # @see http://tools.ietf.org/html/rfc2985 "RFC 2985 Section 5.4.2 Extension request" # class Puppet::SSL::CertificateRequest < Puppet::SSL::Base wraps OpenSSL::X509::Request extend Puppet::Indirector # If auto-signing is on, sign any certificate requests as they are saved. module AutoSigner def save(instance, key = nil) super # Try to autosign the CSR. if ca = Puppet::SSL::CertificateAuthority.instance ca.autosign(instance) end end end indirects :certificate_request, :terminus_class => :file, :extend => AutoSigner, :doc => <>] :csr_attributes A hash # of OIDs and values that are either a string or array of strings. # @option options [Array] :extension_requests A hash of # certificate extensions to add to the CSR extReq attribute, excluding # the Subject Alternative Names extension. # # @raise [Puppet::Error] If the generated CSR signature couldn't be verified # # @return [OpenSSL::X509::Request] The generated CSR def generate(key, options = {}) Puppet.info "Creating a new SSL certificate request for #{name}" # Support either an actual SSL key, or a Puppet key. key = key.content if key.is_a?(Puppet::SSL::Key) # If we're a CSR for the CA, then use the real ca_name, rather than the # fake 'ca' name. This is mostly for backward compatibility with 0.24.x, # but it's also just a good idea. common_name = name == Puppet::SSL::CA_NAME ? Puppet.settings[:ca_name] : name csr = OpenSSL::X509::Request.new csr.version = 0 csr.subject = OpenSSL::X509::Name.new([["CN", common_name]]) csr.public_key = key.public_key if options[:csr_attributes] add_csr_attributes(csr, options[:csr_attributes]) end if (ext_req_attribute = extension_request_attribute(options)) csr.add_attribute(ext_req_attribute) end signer = Puppet::SSL::CertificateSigner.new signer.sign(csr, key) raise Puppet::Error, "CSR sign verification failed; you need to clean the certificate request for #{name} on the server" unless csr.verify(key.public_key) @content = csr Puppet.info "Certificate Request fingerprint (#{digest.name}): #{digest.to_hex}" @content end # Return the set of extensions requested on this CSR, in a form designed to # be useful to Ruby: an array of hashes. Which, not coincidentally, you can pass # successfully to the OpenSSL constructor later, if you want. # # @return [Array String}>] An array of two or three element # hashes, with key/value pairs for the extension's oid, its value, and # optionally its critical state. def request_extensions raise Puppet::Error, "CSR needs content to extract fields" unless @content # Prefer the standard extReq, but accept the Microsoft specific version as # a fallback, if the standard version isn't found. attribute = @content.attributes.find {|x| x.oid == "extReq" } attribute ||= @content.attributes.find {|x| x.oid == "msExtReq" } return [] unless attribute extensions = unpack_extension_request(attribute) index = -1 extensions.map do |ext_values| index += 1 context = "#{attribute.oid} extension index #{index}" # OK, turn that into an extension, to unpack the content. Lovely that # we have to swap the order of arguments to the underlying method, or # perhaps that the ASN.1 representation chose to pack them in a # strange order where the optional component comes *earlier* than the # fixed component in the sequence. case ext_values.length when 2 ev = OpenSSL::X509::Extension.new(ext_values[0].value, ext_values[1].value) { "oid" => ev.oid, "value" => ev.value } when 3 ev = OpenSSL::X509::Extension.new(ext_values[0].value, ext_values[2].value, ext_values[1].value) { "oid" => ev.oid, "value" => ev.value, "critical" => ev.critical? } else raise Puppet::Error, "In #{attribute.oid}, expected extension record #{index} to have two or three items, but found #{ext_values.length}" end end end def subject_alt_names @subject_alt_names ||= request_extensions. select {|x| x["oid"] == "subjectAltName" }. map {|x| x["value"].split(/\s*,\s*/) }. flatten. sort. uniq end # Return all user specified attributes attached to this CSR as a hash. IF an # OID has a single value it is returned as a string, otherwise all values are # returned as an array. # # The format of CSR attributes is specified in PKCS#10/RFC 2986 # # @see http://tools.ietf.org/html/rfc2986 "RFC 2986 Certification Request Syntax Specification" # # @api public # # @return [Hash] def custom_attributes x509_attributes = @content.attributes.reject do |attr| PRIVATE_CSR_ATTRIBUTES.include? attr.oid end x509_attributes.map do |attr| {"oid" => attr.oid, "value" => attr.value.first.value} end end private # Exclude OIDs that may conflict with how Puppet creates CSRs. # # We only have nominal support for Microsoft extension requests, but since we # ultimately respect that field when looking for extension requests in a CSR # we need to prevent that field from being written to directly. PRIVATE_CSR_ATTRIBUTES = [ 'extReq', '1.2.840.113549.1.9.14', 'msExtReq', '1.3.6.1.4.1.311.2.1.14', ] def add_csr_attributes(csr, csr_attributes) csr_attributes.each do |oid, value| begin if PRIVATE_CSR_ATTRIBUTES.include? oid raise ArgumentError, "Cannot specify CSR attribute #{oid}: conflicts with internally used CSR attribute" end encoded = OpenSSL::ASN1::PrintableString.new(value.to_s) attr_set = OpenSSL::ASN1::Set.new([encoded]) csr.add_attribute(OpenSSL::X509::Attribute.new(oid, attr_set)) Puppet.debug("Added csr attribute: #{oid} => #{attr_set.inspect}") rescue OpenSSL::X509::AttributeError => e raise Puppet::Error, "Cannot create CSR with attribute #{oid}: #{e.message}", e.backtrace end end end private PRIVATE_EXTENSIONS = [ 'subjectAltName', '2.5.29.17', ] # @api private def extension_request_attribute(options) extensions = [] if options[:extension_requests] options[:extension_requests].each_pair do |oid, value| begin if PRIVATE_EXTENSIONS.include? oid raise Puppet::Error, "Cannot specify CSR extension request #{oid}: conflicts with internally used extension request" end ext = OpenSSL::X509::Extension.new(oid, value.to_s, false) extensions << ext rescue OpenSSL::X509::ExtensionError => e raise Puppet::Error, "Cannot create CSR with extension request #{oid}: #{e.message}", e.backtrace end end end if options[:dns_alt_names] names = options[:dns_alt_names].split(/\s*,\s*/).map(&:strip) + [name] names = names.sort.uniq.map {|name| "DNS:#{name}" }.join(", ") alt_names_ext = extension_factory.create_extension("subjectAltName", names, false) extensions << alt_names_ext end unless extensions.empty? seq = OpenSSL::ASN1::Sequence(extensions) ext_req = OpenSSL::ASN1::Set([seq]) OpenSSL::X509::Attribute.new("extReq", ext_req) end end # Unpack the extReq attribute into an array of Extensions. # # The extension request attribute is structured like # `Set[Sequence[Extensions]]` where the outer Set only contains a single # sequence. # # In addition the Ruby implementation of ASN1 requires that all ASN1 values # contain a single value, so Sets and Sequence have to contain an array # that in turn holds the elements. This is why we have to unpack an array # every time we unpack a Set/Seq. # # @see http://tools.ietf.org/html/rfc2985#ref-10 5.4.2 CSR Extension Request structure # @see http://tools.ietf.org/html/rfc5280 4.1 Certificate Extension structure # # @api private # # @param attribute [OpenSSL::X509::Attribute] The X509 extension request # # @return [Array>] A array of arrays containing the extension # OID the critical state if present, and the extension value. def unpack_extension_request(attribute) unless attribute.value.is_a? OpenSSL::ASN1::Set raise Puppet::Error, "In #{attribute.oid}, expected Set but found #{attribute.value.class}" end unless attribute.value.value.is_a? Array raise Puppet::Error, "In #{attribute.oid}, expected Set[Array] but found #{attribute.value.value.class}" end unless attribute.value.value.size == 1 raise Puppet::Error, "In #{attribute.oid}, expected Set[Array] with one value but found #{attribute.value.value.size} elements" end unless attribute.value.value.first.is_a? OpenSSL::ASN1::Sequence raise Puppet::Error, "In #{attribute.oid}, expected Set[Array[Sequence[...]]], but found #{extension.class}" end unless attribute.value.value.first.value.is_a? Array raise Puppet::Error, "In #{attribute.oid}, expected Set[Array[Sequence[Array[...]]]], but found #{extension.value.class}" end extensions = attribute.value.value.first.value extensions.map(&:value) end end puppet/ssl/digest.rb000064400000000515147634041270010477 0ustar00class Puppet::SSL::Digest attr_reader :digest def initialize(algorithm, content) algorithm ||= 'SHA256' @digest = OpenSSL::Digest.new(algorithm, content) end def to_s "(#{name}) #{to_hex}" end def to_hex @digest.hexdigest.scan(/../).join(':').upcase end def name @digest.name.upcase end end puppet/ssl/certificate_authority/interface.rb000064400000013311147634041270015550 0ustar00module Puppet module SSL class CertificateAuthority # This class is basically a hidden class that knows how to act on the # CA. Its job is to provide a CLI-like interface to the CA class. class Interface INTERFACE_METHODS = [:destroy, :list, :revoke, :generate, :sign, :print, :verify, :fingerprint, :reinventory] SUBJECTLESS_METHODS = [:list, :reinventory] class InterfaceError < ArgumentError; end attr_reader :method, :subjects, :digest, :options # Actually perform the work. def apply(ca) unless subjects || SUBJECTLESS_METHODS.include?(method) raise ArgumentError, "You must provide hosts or --all when using #{method}" end # if the interface implements the method, use it instead of the ca's method if respond_to?(method) send(method, ca) else (subjects == :all ? ca.list : subjects).each do |host| ca.send(method, host) end end end def generate(ca) raise InterfaceError, "It makes no sense to generate all hosts; you must specify a list" if subjects == :all subjects.each do |host| ca.generate(host, options) end end def initialize(method, options) self.method = method self.subjects = options.delete(:to) @digest = options.delete(:digest) @options = options end # List the hosts. def list(ca) signed = ca.list if [:signed, :all].include?(subjects) requests = ca.waiting? case subjects when :all hosts = [signed, requests].flatten when :signed hosts = signed.flatten when nil hosts = requests else hosts = subjects signed = ca.list(hosts) end certs = {:signed => {}, :invalid => {}, :request => {}} return if hosts.empty? hosts.uniq.sort.each do |host| begin ca.verify(host) unless requests.include?(host) rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError => details verify_error = details.to_s end if verify_error certs[:invalid][host] = [ Puppet::SSL::Certificate.indirection.find(host), verify_error ] elsif (signed and signed.include?(host)) certs[:signed][host] = Puppet::SSL::Certificate.indirection.find(host) else certs[:request][host] = Puppet::SSL::CertificateRequest.indirection.find(host) end end names = certs.values.map(&:keys).flatten name_width = names.sort_by(&:length).last.length rescue 0 # We quote these names, so account for those characters name_width += 2 output = [:request, :signed, :invalid].map do |type| next if certs[type].empty? certs[type].map do |host,info| format_host(ca, host, type, info, name_width) end end.flatten.compact.sort.join("\n") puts output end def format_host(ca, host, type, info, width) cert, verify_error = info alt_names = case type when :signed cert.subject_alt_names when :request cert.subject_alt_names else [] end alt_names.delete(host) alt_str = "(alt names: #{alt_names.map(&:inspect).join(', ')})" unless alt_names.empty? glyph = {:signed => '+', :request => ' ', :invalid => '-'}[type] name = host.inspect.ljust(width) fingerprint = cert.digest(@digest).to_s explanation = "(#{verify_error})" if verify_error [glyph, name, fingerprint, alt_str, explanation].compact.join(' ') end # Set the method to apply. def method=(method) raise ArgumentError, "Invalid method #{method} to apply" unless INTERFACE_METHODS.include?(method) @method = method end # Print certificate information. def print(ca) (subjects == :all ? ca.list : subjects).each do |host| if value = ca.print(host) puts value else Puppet.err "Could not find certificate for #{host}" end end end # Print certificate information. def fingerprint(ca) (subjects == :all ? ca.list + ca.waiting?: subjects).each do |host| if cert = (Puppet::SSL::Certificate.indirection.find(host) || Puppet::SSL::CertificateRequest.indirection.find(host)) puts "#{host} #{cert.digest(@digest)}" else Puppet.err "Could not find certificate for #{host}" end end end # Signs given certificates or waiting of subjects == :all def sign(ca) list = subjects == :all ? ca.waiting? : subjects raise InterfaceError, "No waiting certificate requests to sign" if list.empty? list.each do |host| ca.sign(host, options[:allow_dns_alt_names]) end end def reinventory(ca) ca.inventory.rebuild end # Set the list of hosts we're operating on. Also supports keywords. def subjects=(value) unless value == :all || value == :signed || value.is_a?(Array) raise ArgumentError, "Subjects must be an array or :all; not #{value}" end @subjects = (value == []) ? nil : value end end end end end puppet/ssl/certificate_authority/autosign_command.rb000064400000002300147634041270017133 0ustar00require 'puppet/ssl/certificate_authority' require 'puppet/file_system/uniquefile' # This class wraps a given command and invokes it with a CSR name and body to # determine if the given CSR should be autosigned # # @api private class Puppet::SSL::CertificateAuthority::AutosignCommand class CheckFailure < Puppet::Error; end def initialize(path) @path = path end # Run the autosign command with the given CSR name as an argument and the # CSR body on stdin. # # @param csr [String] The CSR name to check for autosigning # @return [true, false] If the CSR should be autosigned def allowed?(csr) name = csr.name cmd = [@path, name] output = Puppet::FileSystem::Uniquefile.open_tmp('puppet-csr') do |csr_file| csr_file.write(csr.to_s) csr_file.flush execute_options = {:stdinfile => csr_file.path, :combine => true, :failonfail => false} Puppet::Util::Execution.execute(cmd, execute_options) end output.chomp! Puppet.debug "Autosign command '#{@path}' exit status: #{output.exitstatus}" Puppet.debug "Autosign command '#{@path}' output: #{output}" case output.exitstatus when 0 true else false end end end puppet/ssl/key.rb000064400000002653147634041270010015 0ustar00require 'puppet/ssl/base' require 'puppet/indirector' # Manage private and public keys as a pair. class Puppet::SSL::Key < Puppet::SSL::Base wraps OpenSSL::PKey::RSA extend Puppet::Indirector indirects :key, :terminus_class => :file, :doc => < Puppet[:ssl_client_ca_chain], :ca_auth_file => Puppet[:ssl_client_ca_auth] }), ssl_host = Puppet::SSL::Host.localhost) reset! @ssl_configuration = ssl_configuration @ssl_host = ssl_host end # Resets this validator to its initial validation state. The ssl configuration is not changed. # # @api private # def reset! @peer_certs = [] @verify_errors = [] end # Performs verification of the SSL connection and collection of the # certificates for use in constructing the error message if the verification # failed. This callback will be executed once for each certificate in a # chain being verified. # # From the [OpenSSL # documentation](http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html): # The `verify_callback` function is used to control the behaviour when the # SSL_VERIFY_PEER flag is set. It must be supplied by the application and # receives two arguments: preverify_ok indicates, whether the verification of # the certificate in question was passed (preverify_ok=1) or not # (preverify_ok=0). x509_store_ctx is a pointer to the complete context used for # the certificate chain verification. # # See {Puppet::Network::HTTP::Connection} for more information and where this # class is intended to be used. # # @param [Boolean] preverify_ok indicates whether the verification of the # certificate in question was passed (preverify_ok=true) # @param [OpenSSL::X509::StoreContext] store_context holds the X509 store context # for the chain being verified. # # @return [Boolean] false if the peer is invalid, true otherwise. # # @api private # def call(preverify_ok, store_context) # We must make a copy since the scope of the store_context will be lost # across invocations of this method. if preverify_ok current_cert = store_context.current_cert @peer_certs << Puppet::SSL::Certificate.from_instance(current_cert) # If we've copied all of the certs in the chain out of the SSL library if @peer_certs.length == store_context.chain.length # (#20027) The peer cert must be issued by a specific authority preverify_ok = valid_peer? end else error = store_context.error || 0 error_string = store_context.error_string || "OpenSSL error #{error}" case error when OpenSSL::X509::V_ERR_CRL_NOT_YET_VALID # current_crl can be nil # https://github.com/ruby/ruby/blob/ruby_1_9_3/ext/openssl/ossl_x509store.c#L501-L510 crl = store_context.current_crl if crl if crl.last_update && crl.last_update < Time.now + FIVE_MINUTES_AS_SECONDS Puppet.debug("Ignoring CRL not yet valid, current time #{Time.now.utc}, CRL last updated #{crl.last_update.utc}") preverify_ok = true else @verify_errors << "#{error_string} for #{crl.issuer}" end else @verify_errors << error_string end else current_cert = store_context.current_cert @verify_errors << "#{error_string} for #{current_cert.subject}" end end preverify_ok rescue => ex @verify_errors << ex.message false end # Registers the instance's call method with the connection. # # @param [Net::HTTP] connection The connection to validate # # @return [void] # # @api private # def setup_connection(connection) if ssl_certificates_are_present? connection.cert_store = @ssl_host.ssl_store connection.ca_file = @ssl_configuration.ca_auth_file connection.cert = @ssl_host.certificate.content connection.key = @ssl_host.key.content connection.verify_mode = OpenSSL::SSL::VERIFY_PEER connection.verify_callback = self else connection.verify_mode = OpenSSL::SSL::VERIFY_NONE end end # Validates the peer certificates against the authorized certificates. # # @api private # def valid_peer? descending_cert_chain = @peer_certs.reverse.map {|c| c.content } authz_ca_certs = ssl_configuration.ca_auth_certificates if not has_authz_peer_cert(descending_cert_chain, authz_ca_certs) msg = "The server presented a SSL certificate chain which does not include a " << "CA listed in the ssl_client_ca_auth file. " msg << "Authorized Issuers: #{authz_ca_certs.collect {|c| c.subject}.join(', ')} " << "Peer Chain: #{descending_cert_chain.collect {|c| c.subject}.join(' => ')}" @verify_errors << msg false else true end end # Checks if the set of peer_certs contains at least one certificate issued # by a certificate listed in authz_certs # # @return [Boolean] # # @api private # def has_authz_peer_cert(peer_certs, authz_certs) peer_certs.any? do |peer_cert| authz_certs.any? do |authz_cert| peer_cert.verify(authz_cert.public_key) end end end # @api private # def ssl_certificates_are_present? Puppet::FileSystem.exist?(Puppet[:hostcert]) && Puppet::FileSystem.exist?(@ssl_configuration.ca_auth_file) end end puppet/ssl/oids.rb000064400000005762147634041270010167 0ustar00require 'puppet/ssl' # This module defines OIDs for use within Puppet. # # == ASN.1 Definition # # The following is the formal definition of OIDs specified in this file. # # puppetCertExtensions OBJECT IDENTIFIER ::= {iso(1) identified-organization(3) # dod(6) internet(1) private(4) enterprise(1) 34380 1} # # -- the tree under registeredExtensions 'belongs' to puppetlabs # -- privateExtensions can be extended by enterprises to suit their own needs # registeredExtensions OBJECT IDENTIFIER ::= { puppetCertExtensions 1 } # privateExtensions OBJECT IDENTIFIER ::= { puppetCertExtensions 2 } # # -- subtree of common registered extensions # -- The short names for these OIDs are intentionally lowercased and formatted # -- since they may be exposed inside the Puppet DSL as variables. # pp_uuid OBJECT IDENTIFIER ::= { registeredExtensions 1 } # pp_instance_id OBJECT IDENTIFIER ::= { registeredExtensions 2 } # pp_image_name OBJECT IDENTIFIER ::= { registeredExtensions 3 } # pp_preshared_key OBJECT IDENTIFIER ::= { registeredExtensions 4 } # # @api private module Puppet::SSL::Oids PUPPET_OIDS = [ ["1.3.6.1.4.1.34380", 'puppetlabs', 'Puppet Labs'], ["1.3.6.1.4.1.34380.1", 'ppCertExt', 'Puppet Certificate Extension'], ["1.3.6.1.4.1.34380.1.1", 'ppRegCertExt', 'Puppet Registered Certificate Extension'], ["1.3.6.1.4.1.34380.1.1.1", 'pp_uuid', 'Puppet Node UUID'], ["1.3.6.1.4.1.34380.1.1.2", 'pp_instance_id', 'Puppet Node Instance ID'], ["1.3.6.1.4.1.34380.1.1.3", 'pp_image_name', 'Puppet Node Image Name'], ["1.3.6.1.4.1.34380.1.1.4", 'pp_preshared_key', 'Puppet Node Preshared Key'], ["1.3.6.1.4.1.34380.1.2", 'ppPrivCertExt', 'Puppet Private Certificate Extension'], ] PUPPET_OIDS.each do |oid_defn| OpenSSL::ASN1::ObjectId.register(*oid_defn) end # Determine if the first OID contains the second OID # # @param first [String] The containing OID, in dotted form or as the short name # @param second [String] The contained OID, in dotted form or as the short name # @param exclusive [true, false] If an OID should not be considered as a subtree of itself # # @example Comparing two dotted OIDs # Puppet::SSL::Oids.subtree_of?('1.3.6.1', '1.3.6.1.4.1') #=> true # Puppet::SSL::Oids.subtree_of?('1.3.6.1', '1.3.6') #=> false # # @example Comparing an OID short name with a dotted OID # Puppet::SSL::Oids.subtree_of?('IANA', '1.3.6.1.4.1') #=> true # Puppet::SSL::Oids.subtree_of?('1.3.6.1', 'enterprises') #=> true # # @example Comparing an OID against itself # Puppet::SSL::Oids.subtree_of?('IANA', 'IANA') #=> true # Puppet::SSL::Oids.subtree_of?('IANA', 'IANA', true) #=> false # # @return [true, false] def self.subtree_of?(first, second, exclusive = false) first_oid = OpenSSL::ASN1::ObjectId.new(first).oid second_oid = OpenSSL::ASN1::ObjectId.new(second).oid if exclusive and first_oid == second_oid false else second_oid.index(first_oid) == 0 end rescue OpenSSL::ASN1::ASN1Error false end end puppet/ssl/validator.rb000064400000003102147634041270011200 0ustar00require 'openssl' # API for certificate verification # # @api public class Puppet::SSL::Validator # Factory method for creating an instance of a null/no validator. # This method does not have to be implemented by concrete implementations of this API. # # @return [Puppet::SSL::Validator] produces a validator that performs no validation # # @api public # def self.no_validator() @@no_validator_cache ||= Puppet::SSL::Validator::NoValidator.new() end # Factory method for creating an instance of the default Puppet validator. # This method does not have to be implemented by concrete implementations of this API. # # @return [Puppet::SSL::Validator] produces a validator that performs no validation # # @api public # def self.default_validator() Puppet::SSL::Validator::DefaultValidator.new() end # Array of peer certificates # @return [Array] peer certificates # # @api public # def peer_certs raise NotImplementedError, "Concrete class should have implemented this method" end # Contains the result of validation # @return [Array, nil] nil, empty Array, or Array with messages # # @api public # def verify_errors raise NotImplementedError, "Concrete class should have implemented this method" end # Registers the connection to validate. # # @param [Net::HTTP] connection The connection to validate # # @return [void] # # @api public # def setup_connection(connection) raise NotImplementedError, "Concrete class should have implemented this method" end end puppet/ssl/base.rb000064400000006754147634041270010145 0ustar00require 'openssl' require 'puppet/ssl' require 'puppet/ssl/digest' require 'puppet/util/ssl' # The base class for wrapping SSL instances. class Puppet::SSL::Base # For now, use the YAML separator. SEPARATOR = "\n---\n" # Only allow printing ascii characters, excluding / VALID_CERTNAME = /\A[ -.0-~]+\Z/ def self.from_multiple_s(text) text.split(SEPARATOR).collect { |inst| from_s(inst) } end def self.to_multiple_s(instances) instances.collect { |inst| inst.to_s }.join(SEPARATOR) end def self.wraps(klass) @wrapped_class = klass end def self.wrapped_class raise(Puppet::DevError, "#{self} has not declared what class it wraps") unless defined?(@wrapped_class) @wrapped_class end def self.validate_certname(name) raise "Certname #{name.inspect} must not contain unprintable or non-ASCII characters" unless name =~ VALID_CERTNAME end attr_accessor :name, :content # Is this file for the CA? def ca? name == Puppet::SSL::Host.ca_name end def generate raise Puppet::DevError, "#{self.class} did not override 'generate'" end def initialize(name) @name = name.to_s.downcase self.class.validate_certname(@name) end ## # name_from_subject extracts the common name attribute from the subject of an # x.509 certificate certificate # # @api private # # @param [OpenSSL::X509::Name] subject The full subject (distinguished name) of the x.509 # certificate. # # @return [String] the name (CN) extracted from the subject. def self.name_from_subject(subject) Puppet::Util::SSL.cn_from_subject(subject) end # Create an instance of our Puppet::SSL::* class using a given instance of the wrapped class def self.from_instance(instance, name = nil) raise ArgumentError, "Object must be an instance of #{wrapped_class}, #{instance.class} given" unless instance.is_a? wrapped_class raise ArgumentError, "Name must be supplied if it cannot be determined from the instance" if name.nil? and !instance.respond_to?(:subject) name ||= name_from_subject(instance.subject) result = new(name) result.content = instance result end # Convert a string into an instance def self.from_s(string, name = nil) instance = wrapped_class.new(string) from_instance(instance, name) end # Read content from disk appropriately. def read(path) @content = wrapped_class.new(File.read(path)) end # Convert our thing to pem. def to_s return "" unless content content.to_pem end def to_data_hash to_s end # Provide the full text of the thing we're dealing with. def to_text return "" unless content content.to_text end def fingerprint(md = :SHA256) mds = md.to_s.upcase digest(mds).to_hex end def digest(algorithm=nil) unless algorithm algorithm = digest_algorithm end Puppet::SSL::Digest.new(algorithm, content.to_der) end def digest_algorithm # The signature_algorithm on the X509 cert is a combination of the digest # algorithm and the encryption algorithm # e.g. md5WithRSAEncryption, sha256WithRSAEncryption # Unfortunately there isn't a consistent pattern # See RFCs 3279, 5758 digest_re = Regexp.union( /ripemd160/i, /md[245]/i, /sha\d*/i ) ln = content.signature_algorithm if match = digest_re.match(ln) match[0].downcase else raise Puppet::Error, "Unknown signature algorithm '#{ln}'" end end private def wrapped_class self.class.wrapped_class end end puppet/ssl/certificate_revocation_list.rb000064400000006615147634041270014775 0ustar00require 'puppet/ssl/base' require 'puppet/indirector' # Manage the CRL. class Puppet::SSL::CertificateRevocationList < Puppet::SSL::Base FIVE_YEARS = 5 * 365*24*60*60 wraps OpenSSL::X509::CRL extend Puppet::Indirector indirects :certificate_revocation_list, :terminus_class => :file, :doc => < :file, :doc => < 'pp_uuid', 'value' => 'abcd'}] # # @return [Array String}>] An array of two element hashes, # with key/value pairs for the extension's oid, and its value. def custom_extensions custom_exts = content.extensions.select do |ext| Puppet::SSL::Oids.subtree_of?('ppRegCertExt', ext.oid) or Puppet::SSL::Oids.subtree_of?('ppPrivCertExt', ext.oid) end custom_exts.map { |ext| {'oid' => ext.oid, 'value' => ext.value} } end end puppet/ssl/certificate_authority.rb000064400000041365147634041270013622 0ustar00require 'puppet/ssl/host' require 'puppet/ssl/certificate_request' require 'puppet/ssl/certificate_signer' require 'puppet/util' # The class that knows how to sign certificates. It creates # a 'special' SSL::Host whose name is 'ca', thus indicating # that, well, it's the CA. There's some magic in the # indirector/ssl_file terminus base class that does that # for us. # This class mostly just signs certs for us, but # it can also be seen as a general interface into all of the # SSL stuff. class Puppet::SSL::CertificateAuthority # We will only sign extensions on this whitelist, ever. Any CSR with a # requested extension that we don't recognize is rejected, against the risk # that it will introduce some security issue through our ignorance of it. # # Adding an extension to this whitelist simply means we will consider it # further, not that we will always accept a certificate with an extension # requested on this list. RequestExtensionWhitelist = %w{subjectAltName} require 'puppet/ssl/certificate_factory' require 'puppet/ssl/inventory' require 'puppet/ssl/certificate_revocation_list' require 'puppet/ssl/certificate_authority/interface' require 'puppet/ssl/certificate_authority/autosign_command' require 'puppet/network/authstore' class CertificateVerificationError < RuntimeError attr_accessor :error_code def initialize(code) @error_code = code end end def self.singleton_instance @singleton_instance ||= new end class CertificateSigningError < RuntimeError attr_accessor :host def initialize(host) @host = host end end def self.ca? # running as ca? - ensure boolean answer !!(Puppet[:ca] && Puppet.run_mode.master?) end # If this process can function as a CA, then return a singleton instance. def self.instance ca? ? singleton_instance : nil end attr_reader :name, :host # If autosign is configured, autosign the csr we are passed. # @param csr [Puppet::SSL::CertificateRequest] The csr to sign. # @return [Void] # @api private def autosign(csr) if autosign?(csr) Puppet.info "Autosigning #{csr.name}" sign(csr.name) end end # Determine if a CSR can be autosigned by the autosign store or autosign command # # @param csr [Puppet::SSL::CertificateRequest] The CSR to check # @return [true, false] # @api private def autosign?(csr) auto = Puppet[:autosign] decider = case auto when false AutosignNever.new when true AutosignAlways.new else file = Puppet::FileSystem.pathname(auto) if Puppet::FileSystem.executable?(file) Puppet::SSL::CertificateAuthority::AutosignCommand.new(auto) elsif Puppet::FileSystem.exist?(file) AutosignConfig.new(file) else AutosignNever.new end end decider.allowed?(csr) end # Retrieves (or creates, if necessary) the certificate revocation list. def crl unless defined?(@crl) unless @crl = Puppet::SSL::CertificateRevocationList.indirection.find(Puppet::SSL::CA_NAME) @crl = Puppet::SSL::CertificateRevocationList.new(Puppet::SSL::CA_NAME) @crl.generate(host.certificate.content, host.key.content) Puppet::SSL::CertificateRevocationList.indirection.save(@crl) end end @crl end # Delegates this to our Host class. def destroy(name) Puppet::SSL::Host.destroy(name) end # Generates a new certificate. # @return Puppet::SSL::Certificate def generate(name, options = {}) raise ArgumentError, "A Certificate already exists for #{name}" if Puppet::SSL::Certificate.indirection.find(name) # Pass on any requested subjectAltName field. san = options[:dns_alt_names] host = Puppet::SSL::Host.new(name) host.generate_certificate_request(:dns_alt_names => san) # CSR may have been implicitly autosigned, generating a certificate # Or sign explicitly host.certificate || sign(name, !!san) end # Generate our CA certificate. def generate_ca_certificate generate_password unless password? host.generate_key unless host.key # Create a new cert request. We do this specially, because we don't want # to actually save the request anywhere. request = Puppet::SSL::CertificateRequest.new(host.name) # We deliberately do not put any subjectAltName in here: the CA # certificate absolutely does not need them. --daniel 2011-10-13 request.generate(host.key) # Create a self-signed certificate. @certificate = sign(host.name, false, request) # And make sure we initialize our CRL. crl end def initialize Puppet.settings.use :main, :ssl, :ca @name = Puppet[:certname] @host = Puppet::SSL::Host.new(Puppet::SSL::Host.ca_name) setup end # Retrieve (or create, if necessary) our inventory manager. def inventory @inventory ||= Puppet::SSL::Inventory.new end # Generate a new password for the CA. def generate_password pass = "" 20.times { pass += (rand(74) + 48).chr } begin Puppet.settings.setting(:capass).open('w') { |f| f.print pass } rescue Errno::EACCES => detail raise Puppet::Error, "Could not write CA password: #{detail}", detail.backtrace end @password = pass pass end # Lists the names of all signed certificates. # # @param name [Array] filter to cerificate names # # @return [Array] def list(name='*') list_certificates(name).collect { |c| c.name } end # Return all the certificate objects as found by the indirector # API for PE license checking. # # Created to prevent the case of reading all certs from disk, getting # just their names and verifying the cert for each name, which then # causes the cert to again be read from disk. # # @author Jeff Weiss # @api Puppet Enterprise Licensing # # @param name [Array] filter to cerificate names # # @return [Array] def list_certificates(name='*') Puppet::SSL::Certificate.indirection.search(name) end # Read the next serial from the serial file, and increment the # file so this one is considered used. def next_serial serial = 1 Puppet.settings.setting(:serial).exclusive_open('a+') do |f| f.rewind serial = f.read.chomp.hex if serial == 0 serial = 1 end f.truncate(0) f.rewind # We store the next valid serial, not the one we just used. f << "%04X" % (serial + 1) end serial end # Does the password file exist? def password? Puppet::FileSystem.exist?(Puppet[:capass]) end # Print a given host's certificate as text. def print(name) (cert = Puppet::SSL::Certificate.indirection.find(name)) ? cert.to_text : nil end # Revoke a given certificate. def revoke(name) raise ArgumentError, "Cannot revoke certificates when the CRL is disabled" unless crl cert = Puppet::SSL::Certificate.indirection.find(name) serials = if cert [cert.content.serial] elsif name =~ /^0x[0-9A-Fa-f]+$/ [name.hex] else inventory.serials(name) end if serials.empty? raise ArgumentError, "Could not find a serial number for #{name}" end serials.each do |s| crl.revoke(s, host.key.content) end end # This initializes our CA so it actually works. This should be a private # method, except that you can't any-instance stub private methods, which is # *awesome*. This method only really exists to provide a stub-point during # testing. def setup generate_ca_certificate unless @host.certificate end # Sign a given certificate request. def sign(hostname, allow_dns_alt_names = false, self_signing_csr = nil) # This is a self-signed certificate if self_signing_csr # # This is a self-signed certificate, which is for the CA. Since this # # forces the certificate to be self-signed, anyone who manages to trick # # the system into going through this path gets a certificate they could # # generate anyway. There should be no security risk from that. csr = self_signing_csr cert_type = :ca issuer = csr.content else allow_dns_alt_names = true if hostname == Puppet[:certname].downcase unless csr = Puppet::SSL::CertificateRequest.indirection.find(hostname) raise ArgumentError, "Could not find certificate request for #{hostname}" end cert_type = :server issuer = host.certificate.content # Make sure that the CSR conforms to our internal signing policies. # This will raise if the CSR doesn't conform, but just in case... check_internal_signing_policies(hostname, csr, allow_dns_alt_names) or raise CertificateSigningError.new(hostname), "CSR had an unknown failure checking internal signing policies, will not sign!" end cert = Puppet::SSL::Certificate.new(hostname) cert.content = Puppet::SSL::CertificateFactory. build(cert_type, csr, issuer, next_serial) signer = Puppet::SSL::CertificateSigner.new signer.sign(cert.content, host.key.content) Puppet.notice "Signed certificate request for #{hostname}" # Add the cert to the inventory before we save it, since # otherwise we could end up with it being duplicated, if # this is the first time we build the inventory file. inventory.add(cert) # Save the now-signed cert. This should get routed correctly depending # on the certificate type. Puppet::SSL::Certificate.indirection.save(cert) # And remove the CSR if this wasn't self signed. Puppet::SSL::CertificateRequest.indirection.destroy(csr.name) unless self_signing_csr cert end def check_internal_signing_policies(hostname, csr, allow_dns_alt_names) # Reject unknown request extensions. unknown_req = csr.request_extensions.reject do |x| RequestExtensionWhitelist.include? x["oid"] or Puppet::SSL::Oids.subtree_of?('ppRegCertExt', x["oid"], true) or Puppet::SSL::Oids.subtree_of?('ppPrivCertExt', x["oid"], true) end if unknown_req and not unknown_req.empty? names = unknown_req.map {|x| x["oid"] }.sort.uniq.join(", ") raise CertificateSigningError.new(hostname), "CSR has request extensions that are not permitted: #{names}" end # Do not sign misleading CSRs cn = csr.content.subject.to_a.assoc("CN")[1] if hostname != cn raise CertificateSigningError.new(hostname), "CSR subject common name #{cn.inspect} does not match expected certname #{hostname.inspect}" end if hostname !~ Puppet::SSL::Base::VALID_CERTNAME raise CertificateSigningError.new(hostname), "CSR #{hostname.inspect} subject contains unprintable or non-ASCII characters" end # Wildcards: we don't allow 'em at any point. # # The stringification here makes the content visible, and saves us having # to scrobble through the content of the CSR subject field to make sure it # is what we expect where we expect it. if csr.content.subject.to_s.include? '*' raise CertificateSigningError.new(hostname), "CSR subject contains a wildcard, which is not allowed: #{csr.content.subject.to_s}" end unless csr.content.verify(csr.content.public_key) raise CertificateSigningError.new(hostname), "CSR contains a public key that does not correspond to the signing key" end unless csr.subject_alt_names.empty? # If you alt names are allowed, they are required. Otherwise they are # disallowed. Self-signed certs are implicitly trusted, however. unless allow_dns_alt_names raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' contains subject alternative names (#{csr.subject_alt_names.join(', ')}), which are disallowed. Use `puppet cert --allow-dns-alt-names sign #{csr.name}` to sign this request." end # If subjectAltNames are present, validate that they are only for DNS # labels, not any other kind. unless csr.subject_alt_names.all? {|x| x =~ /^DNS:/ } raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' contains a subjectAltName outside the DNS label space: #{csr.subject_alt_names.join(', ')}. To continue, this CSR needs to be cleaned." end # Check for wildcards in the subjectAltName fields too. if csr.subject_alt_names.any? {|x| x.include? '*' } raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' subjectAltName contains a wildcard, which is not allowed: #{csr.subject_alt_names.join(', ')} To continue, this CSR needs to be cleaned." end end return true # good enough for us! end # Utility method for optionally caching the X509 Store for verifying a # large number of certificates in a short amount of time--exactly the # case we have during PE license checking. # # @example Use the cached X509 store # x509store(:cache => true) # # @example Use a freshly create X509 store # x509store # x509store(:cache => false) # # @param [Hash] options the options used for retrieving the X509 Store # @option options [Boolean] :cache whether or not to use a cached version # of the X509 Store # # @return [OpenSSL::X509::Store] def x509_store(options = {}) if (options[:cache]) return @x509store unless @x509store.nil? @x509store = create_x509_store else create_x509_store end end private :x509_store # Creates a brand new OpenSSL::X509::Store with the appropriate # Certificate Revocation List and flags # # @return [OpenSSL::X509::Store] def create_x509_store store = OpenSSL::X509::Store.new() store.add_file(Puppet[:cacert]) store.add_crl(crl.content) if self.crl store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT if Puppet.settings[:certificate_revocation] store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL | OpenSSL::X509::V_FLAG_CRL_CHECK end store end private :create_x509_store # Utility method which is API for PE license checking. # This is used rather than `verify` because # 1) We have already read the certificate from disk into memory. # To read the certificate from disk again is just wasteful. # 2) Because we're checking a large number of certificates against # a transient CertificateAuthority, we can relatively safely cache # the X509 Store that actually does the verification. # # Long running instances of CertificateAuthority will certainly # want to use `verify` because it will recreate the X509 Store with # the absolutely latest CRL. # # Additionally, this method explicitly returns a boolean whereas # `verify` will raise an error if the certificate has been revoked. # # @author Jeff Weiss # @api Puppet Enterprise Licensing # # @param cert [Puppet::SSL::Certificate] the certificate to check validity of # # @return [Boolean] true if signed, false if unsigned or revoked def certificate_is_alive?(cert) x509_store(:cache => true).verify(cert.content) end # Verify a given host's certificate. The certname is passed in, and # the indirector will be used to locate the actual contents of the # certificate with that name. # # @param name [String] certificate name to verify # # @raise [ArgumentError] if the certificate name cannot be found # (i.e. doesn't exist or is unsigned) # @raise [CertificateVerficationError] if the certificate has been revoked # # @return [Boolean] true if signed, there are no cases where false is returned def verify(name) unless cert = Puppet::SSL::Certificate.indirection.find(name) raise ArgumentError, "Could not find a certificate for #{name}" end store = x509_store raise CertificateVerificationError.new(store.error), store.error_string unless store.verify(cert.content) end def fingerprint(name, md = :SHA256) unless cert = Puppet::SSL::Certificate.indirection.find(name) || Puppet::SSL::CertificateRequest.indirection.find(name) raise ArgumentError, "Could not find a certificate or csr for #{name}" end cert.fingerprint(md) end # List the waiting certificate requests. def waiting? Puppet::SSL::CertificateRequest.indirection.search("*").collect { |r| r.name } end # @api private class AutosignAlways def allowed?(csr) true end end # @api private class AutosignNever def allowed?(csr) false end end # @api private class AutosignConfig def initialize(config_file) @config = config_file end def allowed?(csr) autosign_store.allowed?(csr.name, '127.1.1.1') end private def autosign_store auth = Puppet::Network::AuthStore.new Puppet::FileSystem.each_line(@config) do |line| next if line =~ /^\s*#/ next if line =~ /^\s*$/ auth.allow(line.chomp) end auth end end end puppet/ssl/certificate_factory.rb000064400000021354147634041270013235 0ustar00require 'puppet/ssl' # This class encapsulates the logic of creating and adding extensions to X509 # certificates. # # @api private module Puppet::SSL::CertificateFactory # Create, add extensions to, and sign a new X509 certificate. # # @param cert_type [Symbol] The certificate type to create, which specifies # what extensions are added to the certificate. # One of (:ca, :terminalsubca, :server, :ocsp, :client) # @param csr [OpenSSL::X509::Request] The signing request associated with # the certificate being created. # @param issuer [OpenSSL::X509::Certificate, OpenSSL::X509::Request] An X509 CSR # if this is a self signed certificate, or the X509 certificate of the CA if # this is a CA signed certificate. # @param serial [Integer] The serial number for the given certificate, which # MUST be unique for the given CA. # @param ttl [String] The duration of the validity for the given certificate. # defaults to Puppet[:ca_ttl] # # @api public # # @return [OpenSSL::X509::Certificate] def self.build(cert_type, csr, issuer, serial, ttl = nil) # Work out if we can even build the requested type of certificate. build_extensions = "build_#{cert_type.to_s}_extensions" respond_to?(build_extensions) or raise ArgumentError, "#{cert_type.to_s} is an invalid certificate type!" raise ArgumentError, "Certificate TTL must be an integer" unless ttl.nil? || ttl.is_a?(Fixnum) # set up the certificate, and start building the content. cert = OpenSSL::X509::Certificate.new cert.version = 2 # X509v3 cert.subject = csr.content.subject cert.issuer = issuer.subject cert.public_key = csr.content.public_key cert.serial = serial # Make the certificate valid as of yesterday, because so many people's # clocks are out of sync. This gives one more day of validity than people # might expect, but is better than making every person who has a messed up # clock fail, and better than having every cert we generate expire a day # before the user expected it to when they asked for "one year". cert.not_before = Time.now - (60*60*24) cert.not_after = Time.now + (ttl || Puppet[:ca_ttl]) add_extensions_to(cert, csr, issuer, send(build_extensions)) return cert end private # Add X509v3 extensions to the given certificate. # # @param cert [OpenSSL::X509::Certificate] The certificate to add the # extensions to. # @param csr [OpenSSL::X509::Request] The CSR associated with the given # certificate, which may specify requested extensions for the given cert. # See http://tools.ietf.org/html/rfc2985 Section 5.4.2 Extension request # @param issuer [OpenSSL::X509::Certificate, OpenSSL::X509::Request] An X509 CSR # if this is a self signed certificate, or the X509 certificate of the CA if # this is a CA signed certificate. # @param extensions [Hash | String>] The extensions to # add to the certificate, based on the certificate type being created (CA, # server, client, etc) # # @api private # # @return [void] def self.add_extensions_to(cert, csr, issuer, extensions) ef = OpenSSL::X509::ExtensionFactory.new ef.subject_certificate = cert ef.issuer_certificate = issuer.is_a?(OpenSSL::X509::Request) ? cert : issuer # Extract the requested extensions from the CSR. requested_exts = csr.request_extensions.inject({}) do |hash, re| hash[re["oid"]] = [re["value"], re["critical"]] hash end # Produce our final set of extensions. We deliberately order these to # build the way we want: # 1. "safe" default values, like the comment, that no one cares about. # 2. request extensions, from the CSR # 3. extensions based on the type we are generating # 4. overrides, which we always want to have in their form # # This ordering *is* security-critical, but we want to allow the user # enough rope to shoot themselves in the foot, if they want to ignore our # advice and externally approve a CSR that sets the basicConstraints. # # Swapping the order of 2 and 3 would ensure that you couldn't slip a # certificate through where the CA constraint was true, though, if # something went wrong up there. --daniel 2011-10-11 defaults = { "nsComment" => "Puppet Ruby/OpenSSL Internal Certificate" } # See http://www.openssl.org/docs/apps/x509v3_config.html # for information about the special meanings of 'hash', 'keyid', 'issuer' override = { "subjectKeyIdentifier" => "hash", "authorityKeyIdentifier" => "keyid,issuer" } exts = [defaults, requested_exts, extensions, override]. inject({}) {|ret, val| ret.merge(val) } cert.extensions = exts.map do |oid, val| generate_extension(ef, oid, *val) end end # Woot! We're a CA. def self.build_ca_extensions { # This was accidentally omitted in the previous version of this code: an # effort was made to add it last, but that actually managed to avoid # adding it to the certificate at all. # # We have some sort of bug, which means that when we add it we get a # complaint that the issuer keyid can't be fetched, which breaks all # sorts of things in our test suite and, e.g., bootstrapping the CA. # # http://tools.ietf.org/html/rfc5280#section-4.2.1.1 says that, to be a # conforming CA we MAY omit the field if we are self-signed, which I # think gives us a pass in the specific case. # # It also notes that we MAY derive the ID from the subject and serial # number of the issuer, or from the key ID, and we definitely have the # former data, should we want to restore this... # # Anyway, preserving this bug means we don't risk breaking anything in # the field, even though it would be nice to have. --daniel 2011-10-11 # # "authorityKeyIdentifier" => "keyid:always,issuer:always", "keyUsage" => [%w{cRLSign keyCertSign}, true], "basicConstraints" => ["CA:TRUE", true], } end # We're a terminal CA, probably not self-signed. def self.build_terminalsubca_extensions { "keyUsage" => [%w{cRLSign keyCertSign}, true], "basicConstraints" => ["CA:TRUE,pathlen:0", true], } end # We're a normal server. def self.build_server_extensions { "keyUsage" => [%w{digitalSignature keyEncipherment}, true], "extendedKeyUsage" => [%w{serverAuth clientAuth}, true], "basicConstraints" => ["CA:FALSE", true], } end # Um, no idea. def self.build_ocsp_extensions { "keyUsage" => [%w{nonRepudiation digitalSignature}, true], "extendedKeyUsage" => [%w{serverAuth OCSPSigning}, true], "basicConstraints" => ["CA:FALSE", true], } end # Normal client. def self.build_client_extensions { "keyUsage" => [%w{nonRepudiation digitalSignature keyEncipherment}, true], # We don't seem to use this, but that seems much more reasonable here... "extendedKeyUsage" => [%w{clientAuth emailProtection}, true], "basicConstraints" => ["CA:FALSE", true], "nsCertType" => "client,email", } end # Generate an extension with the given OID, value, and critical state # # @param oid [String] The numeric value or short name of a given OID. X509v3 # extensions must be passed by short name or long name, while custom # extensions may be passed by short name, long name, oid numeric OID. # @param ef [OpenSSL::X509::ExtensionFactory] The extension factory to use # when generating the extension. # @param val [String, Array] The extension value. # @param crit [true, false] Whether the given extension is critical, defaults # to false. # # @return [OpenSSL::X509::Extension] # # @api private def self.generate_extension(ef, oid, val, crit = false) val = val.join(', ') unless val.is_a? String # Enforce the X509v3 rules about subjectAltName being critical: # specifically, it SHOULD NOT be critical if we have a subject, which we # always do. --daniel 2011-10-18 crit = false if oid == "subjectAltName" if Puppet::SSL::Oids.subtree_of?('id-ce', oid) or Puppet::SSL::Oids.subtree_of?('id-pkix', oid) # Attempt to create a X509v3 certificate extension. Standard certificate # extensions may need access to the associated subject certificate and # issuing certificate, so must be created by the OpenSSL::X509::ExtensionFactory # which provides that context. ef.create_ext(oid, val, crit) else # This is not an X509v3 extension which means that the extension # factory cannot generate it. We need to generate the extension # manually. OpenSSL::X509::Extension.new(oid, val, crit) end end end puppet/ssl/configuration.rb000064400000003762147634041270012076 0ustar00require 'puppet/ssl' require 'openssl' module Puppet module SSL # Puppet::SSL::Configuration is intended to separate out the following concerns: # * CA certificates that authenticate peers (ca_auth_file) # * CA certificates that build trust but do not authenticate (ca_chain_file) # * Who clients trust as distinct from who servers trust. We should not # assume one single self signed CA cert for everyone. class Configuration def initialize(localcacert, options={}) if (options[:ca_chain_file] and not options[:ca_auth_file]) raise ArgumentError, "The CA auth chain is required if the chain file is provided" end @localcacert = localcacert @ca_chain_file = options[:ca_chain_file] @ca_auth_file = options[:ca_auth_file] end # The ca_chain_file method is intended to return the PEM bundle of CA certs # establishing trust but not used for peer authentication. def ca_chain_file @ca_chain_file || ca_auth_file end # The ca_auth_file method is intended to return the PEM bundle of CA certs # used to authenticate peer connections. def ca_auth_file @ca_auth_file || @localcacert end ## # ca_auth_certificates returns an Array of OpenSSL::X509::Certificate # instances intended to be used in the connection verify_callback. This # method loads and parses the {#ca_auth_file} from the filesystem. # # @api private # # @return [Array] def ca_auth_certificates @ca_auth_certificates ||= decode_cert_bundle(read_file(ca_auth_file)) end ## # Decode a string of concatenated certificates # # @return [Array] def decode_cert_bundle(bundle_str) re = /-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m pem_ary = bundle_str.scan(re) pem_ary.map do |pem_str| OpenSSL::X509::Certificate.new(pem_str) end end private :decode_cert_bundle # read_file makes testing easier. def read_file(path) File.read(path) end private :read_file end end end puppet/ssl/certificate_request_attributes.rb000064400000002321147634041270015515 0ustar00require 'puppet/ssl' require 'puppet/util/yaml' # This class transforms simple key/value pairs into the equivalent ASN1 # structures. Values may be strings or arrays of strings. # # @api private class Puppet::SSL::CertificateRequestAttributes attr_reader :path, :custom_attributes, :extension_requests def initialize(path) @path = path @custom_attributes = {} @extension_requests = {} end # Attempt to load a yaml file at the given @path. # @return true if we are able to load the file, false otherwise # @raise [Puppet::Error] if there are unexpected attribute keys def load Puppet.info("csr_attributes file loading from #{path}") if Puppet::FileSystem.exist?(path) hash = Puppet::Util::Yaml.load_file(path, {}) if ! hash.is_a?(Hash) raise Puppet::Error, "invalid CSR attributes, expected instance of Hash, received instance of #{hash.class}" end @custom_attributes = hash.delete('custom_attributes') || {} @extension_requests = hash.delete('extension_requests') || {} if not hash.keys.empty? raise Puppet::Error, "unexpected attributes #{hash.keys.inspect} in #{@path.inspect}" end return true end return false end end puppet/ssl/inventory.rb000064400000003100147634041270011246 0ustar00require 'puppet/ssl' require 'puppet/ssl/certificate' # Keep track of all of our known certificates. class Puppet::SSL::Inventory attr_reader :path # Add a certificate to our inventory. def add(cert) cert = cert.content if cert.is_a?(Puppet::SSL::Certificate) Puppet.settings.setting(:cert_inventory).open("a") do |f| f.print format(cert) end end # Format our certificate for output. def format(cert) iso = '%Y-%m-%dT%H:%M:%S%Z' "0x%04x %s %s %s\n" % [cert.serial, cert.not_before.strftime(iso), cert.not_after.strftime(iso), cert.subject] end def initialize @path = Puppet[:cert_inventory] end # Rebuild the inventory from scratch. This should happen if # the file is entirely missing or if it's somehow corrupted. def rebuild Puppet.notice "Rebuilding inventory file" Puppet.settings.setting(:cert_inventory).open('w') do |f| Puppet::SSL::Certificate.indirection.search("*").each do |cert| f.print format(cert.content) end end end # Find the serial number for a given certificate. def serial(name) Puppet.deprecation_warning 'Inventory#serial is deprecated, use Inventory#serials instead.' return nil unless Puppet::FileSystem.exist?(@path) serials(name).first end # Find all serial numbers for a given certificate. If none can be found, returns # an empty array. def serials(name) return [] unless Puppet::FileSystem.exist?(@path) File.readlines(@path).collect do |line| /^(\S+).+\/CN=#{name}$/.match(line) end.compact.map { |m| Integer(m[1]) } end end puppet/ssl/host.rb000064400000027057147634041270010207 0ustar00require 'puppet/indirector' require 'puppet/ssl' require 'puppet/ssl/key' require 'puppet/ssl/certificate' require 'puppet/ssl/certificate_request' require 'puppet/ssl/certificate_revocation_list' require 'puppet/ssl/certificate_request_attributes' # The class that manages all aspects of our SSL certificates -- # private keys, public keys, requests, etc. class Puppet::SSL::Host # Yay, ruby's strange constant lookups. Key = Puppet::SSL::Key CA_NAME = Puppet::SSL::CA_NAME Certificate = Puppet::SSL::Certificate CertificateRequest = Puppet::SSL::CertificateRequest CertificateRevocationList = Puppet::SSL::CertificateRevocationList extend Puppet::Indirector indirects :certificate_status, :terminus_class => :file, :doc => < :file, :disabled_ca => nil, :file => nil, :rest => :rest} if term = host_map[terminus] self.indirection.terminus_class = term else self.indirection.reset_terminus_class end if cache # This is weird; we don't actually cache our keys, we # use what would otherwise be the cache as our normal # terminus. Key.indirection.terminus_class = cache else Key.indirection.terminus_class = terminus end if cache Certificate.indirection.cache_class = cache CertificateRequest.indirection.cache_class = cache CertificateRevocationList.indirection.cache_class = cache else # Make sure we have no cache configured. puppet master # switches the configurations around a bit, so it's important # that we specify the configs for absolutely everything, every # time. Certificate.indirection.cache_class = nil CertificateRequest.indirection.cache_class = nil CertificateRevocationList.indirection.cache_class = nil end end CA_MODES = { # Our ca is local, so we use it as the ultimate source of information # And we cache files locally. :local => [:ca, :file], # We're a remote CA client. :remote => [:rest, :file], # We are the CA, so we don't have read/write access to the normal certificates. :only => [:ca], # We have no CA, so we just look in the local file store. :none => [:disabled_ca] } # Specify how we expect to interact with our certificate authority. def self.ca_location=(mode) modes = CA_MODES.collect { |m, vals| m.to_s }.join(", ") raise ArgumentError, "CA Mode can only be one of: #{modes}" unless CA_MODES.include?(mode) @ca_location = mode configure_indirection(*CA_MODES[@ca_location]) end # Puppet::SSL::Host is actually indirected now so the original implementation # has been moved into the certificate_status indirector. This method is in-use # in `puppet cert -c `. def self.destroy(name) indirection.destroy(name) end def self.from_data_hash(data) instance = new(data["name"]) if data["desired_state"] instance.desired_state = data["desired_state"] end instance end def self.from_pson(pson) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(pson) end # Puppet::SSL::Host is actually indirected now so the original implementation # has been moved into the certificate_status indirector. This method does not # appear to be in use in `puppet cert -l`. def self.search(options = {}) indirection.search("*", options) end # Is this a ca host, meaning that all of its files go in the CA location? def ca? ca end def key @key ||= Key.indirection.find(name) end # This is the private key; we can create it from scratch # with no inputs. def generate_key @key = Key.new(name) @key.generate begin Key.indirection.save(@key) rescue @key = nil raise end true end def certificate_request @certificate_request ||= CertificateRequest.indirection.find(name) end # Our certificate request requires the key but that's all. def generate_certificate_request(options = {}) generate_key unless key # If this CSR is for the current machine... if name == Puppet[:certname].downcase # ...add our configured dns_alt_names if Puppet[:dns_alt_names] and Puppet[:dns_alt_names] != '' options[:dns_alt_names] ||= Puppet[:dns_alt_names] elsif Puppet::SSL::CertificateAuthority.ca? and fqdn = Facter.value(:fqdn) and domain = Facter.value(:domain) options[:dns_alt_names] = "puppet, #{fqdn}, puppet.#{domain}" end end csr_attributes = Puppet::SSL::CertificateRequestAttributes.new(Puppet[:csr_attributes]) if csr_attributes.load options[:csr_attributes] = csr_attributes.custom_attributes options[:extension_requests] = csr_attributes.extension_requests end @certificate_request = CertificateRequest.new(name) @certificate_request.generate(key.content, options) begin CertificateRequest.indirection.save(@certificate_request) rescue @certificate_request = nil raise end true end def certificate unless @certificate generate_key unless key # get the CA cert first, since it's required for the normal cert # to be of any use. return nil unless Certificate.indirection.find("ca", :fail_on_404 => true) unless ca? return nil unless @certificate = Certificate.indirection.find(name) validate_certificate_with_key end @certificate end def validate_certificate_with_key raise Puppet::Error, "No certificate to validate." unless certificate raise Puppet::Error, "No private key with which to validate certificate with fingerprint: #{certificate.fingerprint}" unless key unless certificate.content.check_private_key(key.content) raise Puppet::Error, < name } my_state = state result[:state] = my_state result[:desired_state] = desired_state if desired_state thing_to_use = (my_state == 'requested') ? certificate_request : my_cert # this is for backwards-compatibility # we should deprecate it and transition people to using # pson[:fingerprints][:default] # It appears that we have no internal consumers of this api # --jeffweiss 30 aug 2012 result[:fingerprint] = thing_to_use.fingerprint # The above fingerprint doesn't tell us what message digest algorithm was used # No problem, except that the default is changing between 2.7 and 3.0. Also, as # we move to FIPS 140-2 compliance, MD5 is no longer allowed (and, gasp, will # segfault in rubies older than 1.9.3) # So, when we add the newer fingerprints, we're explicit about the hashing # algorithm used. # --jeffweiss 31 july 2012 result[:fingerprints] = {} result[:fingerprints][:default] = thing_to_use.fingerprint suitable_message_digest_algorithms.each do |md| result[:fingerprints][md] = thing_to_use.fingerprint md end result[:dns_alt_names] = thing_to_use.subject_alt_names result end # eventually we'll probably want to move this somewhere else or make it # configurable # --jeffweiss 29 aug 2012 def suitable_message_digest_algorithms [:SHA1, :SHA256, :SHA512] end # Attempt to retrieve a cert, if we don't already have one. def wait_for_cert(time) begin return if certificate generate return if certificate rescue SystemExit,NoMemoryError raise rescue Exception => detail Puppet.log_exception(detail, "Could not request certificate: #{detail.message}") if time < 1 puts "Exiting; failed to retrieve certificate and waitforcert is disabled" exit(1) else sleep(time) end retry end if time < 1 puts "Exiting; no certificate found and waitforcert is disabled" exit(1) end while true sleep time begin break if certificate Puppet.notice "Did not receive certificate" rescue StandardError => detail Puppet.log_exception(detail, "Could not request certificate: #{detail.message}") end end end def state if certificate_request return 'requested' end begin Puppet::SSL::CertificateAuthority.new.verify(name) return 'signed' rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError return 'revoked' end end end require 'puppet/ssl/certificate_authority' puppet/util/rubygems.rb000064400000003431147634041270011231 0ustar00require 'puppet/util' module Puppet::Util::RubyGems # Base/factory class for rubygems source. These classes introspec into # rubygems to in order to list where the rubygems system will look for files # to load. class Source class << self # @api private def has_rubygems? # Gems are not actually available when Bundler is loaded, even # though the Gem constant is defined. This is because Bundler # loads in rubygems, but then removes the custom require that # rubygems installs. So when Bundler is around we have to act # as though rubygems is not, e.g. we shouldn't be able to load # a gem that Bundler doesn't want us to see. defined? ::Gem and not defined? ::Bundler end # @api private def source if has_rubygems? Gem::Specification.respond_to?(:latest_specs) ? Gems18Source : OldGemsSource else NoGemsSource end end def new(*args) object = source.allocate object.send(:initialize, *args) object end end end # For RubyGems >= 1.8.0 # @api private class Gems18Source < Source def directories # `require 'mygem'` will consider and potentally load # prerelease gems, so we need to match that behavior. Gem::Specification.latest_specs(true).collect do |spec| File.join(spec.full_gem_path, 'lib') end end def clear_paths Gem.clear_paths end end # RubyGems < 1.8.0 # @api private class OldGemsSource < Source def directories @paths ||= Gem.latest_load_paths end def clear_paths Gem.clear_paths end end # @api private class NoGemsSource < Source def directories [] end def clear_paths; end end end puppet/util/run_mode.rb000064400000003455147634041270011212 0ustar00require 'etc' module Puppet module Util class RunMode def initialize(name) @name = name.to_sym end attr :name def self.[](name) @run_modes ||= {} if Puppet.features.microsoft_windows? @run_modes[name] ||= WindowsRunMode.new(name) else @run_modes[name] ||= UnixRunMode.new(name) end end def master? name == :master end def agent? name == :agent end def user? name == :user end def run_dir "$vardir/run" end def log_dir "$vardir/log" end private ## # select the system or the user directory depending on the context of # this process. The most common use is determining filesystem path # values for confdir and vardir. The intended semantics are: # {http://projects.puppetlabs.com/issues/16637 #16637} for Puppet 3.x # # @todo this code duplicates {Puppet::Settings#which\_configuration\_file} # as described in {http://projects.puppetlabs.com/issues/16637 #16637} def which_dir( system, user ) File.expand_path(if Puppet.features.root? then system else user end) end end class UnixRunMode < RunMode def conf_dir which_dir("/etc/puppet", "~/.puppet") end def var_dir which_dir("/var/lib/puppet", "~/.puppet/var") end end class WindowsRunMode < RunMode def conf_dir which_dir(File.join(windows_common_base("etc")), "~/.puppet") end def var_dir which_dir(File.join(windows_common_base("var")), "~/.puppet/var") end private def windows_common_base(*extra) [Dir::COMMON_APPDATA, "PuppetLabs", "puppet"] + extra end end end end puppet/util/platform.rb000064400000001234147634041270011217 0ustar00module Puppet module Util module Platform def windows? # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard # library uses that to test what platform it's on. In some places we # would use Puppet.features.microsoft_windows?, but this method can be # used to determine the behavior of the underlying system without # requiring features to be initialized and without side effect. !!File::ALT_SEPARATOR end module_function :windows? def default_paths return [] if windows? %w{/usr/sbin /sbin} end module_function :default_paths end end end puppet/util/watched_file.rb000064400000002352147634041270012013 0ustar00require 'puppet/util/watcher' # Monitor a given file for changes on a periodic interval. Changes are detected # by looking for a change in the file ctime. class Puppet::Util::WatchedFile # @!attribute [r] filename # @return [String] The fully qualified path to the file. attr_reader :filename # @param filename [String] The fully qualified path to the file. # @param timer [Puppet::Util::Watcher::Timer] The polling interval for checking for file # changes. Setting the timeout to a negative value will treat the file as # always changed. Defaults to `Puppet[:filetimeout]` def initialize(filename, timer = Puppet::Util::Watcher::Timer.new(Puppet[:filetimeout])) @filename = filename @timer = timer @info = Puppet::Util::Watcher::PeriodicWatcher.new( Puppet::Util::Watcher::Common.file_ctime_change_watcher(@filename), timer) end # @return [true, false] If the file has changed since it was last checked. def changed? @info.changed? end # Allow this to be used as the name of the file being watched in various # other methods (such as Puppet::FileSystem.exist?) def to_str @filename end def to_s "" end end puppet/util/resource_template.rb000064400000004072147634041270013120 0ustar00require 'puppet/util' require 'puppet/util/logging' require 'erb' # A template wrapper that evaluates a template in the # context of a resource, allowing the resource attributes # to be looked up from within the template. # This provides functionality essentially equivalent to # the language's template() function. You pass your file # path and the resource you want to use into the initialization # method, then call result on the instance, and you get back # a chunk of text. # The resource's parameters are available as instance variables # (as opposed to the language, where we use a method_missing trick). # For example, say you have a resource that generates a file. You would # need to implement the following style of `generate` method: # # def generate # template = Puppet::Util::ResourceTemplate.new("/path/to/template", self) # # return Puppet::Type.type(:file).new :path => "/my/file", # :content => template.evaluate # end # # This generated file gets added to the catalog (which is what `generate` does), # and its content is the result of the template. You need to use instance # variables in your template, so if your template just needs to have the name # of the generating resource, it would just have: # # <%= @name %> # # Since the ResourceTemplate class sets as instance variables all of the resource's # parameters. # # Note that this example uses the generating resource as its source of # parameters, which is generally most useful, since it allows you to configure # the generated resource via the generating resource. class Puppet::Util::ResourceTemplate include Puppet::Util::Logging def evaluate set_resource_variables ERB.new(File.read(@file), 0, "-").result(binding) end def initialize(file, resource) raise ArgumentError, "Template #{file} does not exist" unless Puppet::FileSystem.exist?(file) @file = file @resource = resource end private def set_resource_variables @resource.to_hash.each do |param, value| var = "@#{param.to_s}" instance_variable_set(var, value) end end end puppet/util/errors.rb000064400000007463147634041270010721 0ustar00# Some helper methods for throwing and populating errors. # # @api public module Puppet::Util::Errors # Throw a Puppet::DevError with the specified message. Used for unknown or # internal application failures. # # @param msg [String] message used in raised error # @raise [Puppet::DevError] always raised with the supplied message def devfail(msg) self.fail(Puppet::DevError, msg) end # Add line and file info to the supplied exception if info is available from # this object, is appropriately populated and the supplied exception supports # it. When other is supplied, the backtrace will be copied to the error # object and the 'original' will be dropped from the error. # # @param error [Exception] exception that is populated with info # @param other [Exception] original exception, source of backtrace info # @return [Exception] error parameter def adderrorcontext(error, other = nil) error.line ||= self.line if error.respond_to?(:line=) and self.respond_to?(:line) and self.line error.file ||= self.file if error.respond_to?(:file=) and self.respond_to?(:file) and self.file error.original ||= other if error.respond_to?(:original=) error.set_backtrace(other.backtrace) if other and other.respond_to?(:backtrace) # It is not meaningful to keep the wrapped exception since its backtrace has already # been adopted by the error. (The instance variable is private for good reasons). error.instance_variable_set(:@original, nil) error end # Return a human-readable string of this object's file and line attributes, # if set. # # @return [String] description of file and line def error_context if file and line " at #{file}:#{line}" elsif line " at line #{line}" elsif file " in #{file}" else "" end end # Wrap a call in such a way that we always throw the right exception and keep # as much context as possible. # # @param options [Hash] options used to create error # @option options [Class] :type error type to raise, defaults to # Puppet::DevError # @option options [String] :message message to use in error, default mentions # the name of this class # @raise [Puppet::Error] re-raised with extra context if the block raises it # @raise [Error] of type options[:type], when the block raises other # exceptions def exceptwrap(options = {}) options[:type] ||= Puppet::DevError begin return yield rescue Puppet::Error => detail raise adderrorcontext(detail) rescue => detail message = options[:message] || "#{self.class} failed with error #{detail.class}: #{detail}" error = options[:type].new(message) # We can't use self.fail here because it always expects strings, # not exceptions. raise adderrorcontext(error, detail) end retval end # Throw an error, defaulting to a Puppet::Error. # # @overload fail(message, ..) # Throw a Puppet::Error with a message concatenated from the given # arguments. # @param [String] message error message(s) # @overload fail(error_klass, message, ..) # Throw an exception of type error_klass with a message concatenated from # the given arguments. # @param [Class] type of error # @param [String] message error message(s) # @overload fail(error_klass, message, ..) # Throw an exception of type error_klass with a message concatenated from # the given arguments. # @param [Class] type of error # @param [String] message error message(s) # @param [Exception] original exception, source of backtrace info def fail(*args) if args[0].is_a?(Class) type = args.shift else type = Puppet::Error end other = args.count > 1 ? args.pop : nil error = adderrorcontext(type.new(args.join(" ")), other) raise error end end puppet/util/checksums.rb000064400000010362147634041270011362 0ustar00require 'digest/md5' require 'digest/sha1' # A stand-alone module for calculating checksums # in a generic way. module Puppet::Util::Checksums # @deprecated # In Puppet 4 we should switch this to `module_function` to make these methods # private when this class is included. extend self # It's not a good idea to use some of these in some contexts: for example, I # wouldn't try bucketing a file using the :none checksum type. def known_checksum_types [:sha256, :sha256lite, :md5, :md5lite, :sha1, :sha1lite, :mtime, :ctime, :none] end class FakeChecksum def <<(*args) self end end # Is the provided string a checksum? def checksum?(string) # 'sha256lite'.length == 10 string =~ /^\{(\w{3,10})\}\S+/ end # Strip the checksum type from an existing checksum def sumdata(checksum) checksum =~ /^\{(\w+)\}(.+)/ ? $2 : nil end # Strip the checksum type from an existing checksum def sumtype(checksum) checksum =~ /^\{(\w+)\}/ ? $1 : nil end # Calculate a checksum using Digest::SHA256. def sha256(content) require 'digest/sha2' Digest::SHA256.hexdigest(content) end def sha256lite(content) sha256(content[0..511]) end def sha256_file(filename, lite = false) require 'digest/sha2' digest = Digest::SHA256.new checksum_file(digest, filename, lite) end def sha256lite_file(filename) sha256_file(filename, true) end def sha256_stream(&block) require 'digest/sha2' digest = Digest::SHA256.new yield digest digest.hexdigest end def sha256_hex_length 64 end alias :sha256lite_stream :sha256_stream alias :sha256lite_hex_length :sha256_hex_length # Calculate a checksum using Digest::MD5. def md5(content) Digest::MD5.hexdigest(content) end # Calculate a checksum of the first 500 chars of the content using Digest::MD5. def md5lite(content) md5(content[0..511]) end # Calculate a checksum of a file's content using Digest::MD5. def md5_file(filename, lite = false) digest = Digest::MD5.new checksum_file(digest, filename, lite) end # Calculate a checksum of the first 500 chars of a file's content using Digest::MD5. def md5lite_file(filename) md5_file(filename, true) end def md5_stream(&block) digest = Digest::MD5.new yield digest digest.hexdigest end def md5_hex_length 32 end alias :md5lite_stream :md5_stream alias :md5lite_hex_length :md5_hex_length # Return the :mtime timestamp of a file. def mtime_file(filename) Puppet::FileSystem.stat(filename).send(:mtime) end # by definition this doesn't exist # but we still need to execute the block given def mtime_stream noop_digest = FakeChecksum.new yield noop_digest nil end def mtime(content) "" end # Calculate a checksum using Digest::SHA1. def sha1(content) Digest::SHA1.hexdigest(content) end # Calculate a checksum of the first 500 chars of the content using Digest::SHA1. def sha1lite(content) sha1(content[0..511]) end # Calculate a checksum of a file's content using Digest::SHA1. def sha1_file(filename, lite = false) digest = Digest::SHA1.new checksum_file(digest, filename, lite) end # Calculate a checksum of the first 500 chars of a file's content using Digest::SHA1. def sha1lite_file(filename) sha1_file(filename, true) end def sha1_stream digest = Digest::SHA1.new yield digest digest.hexdigest end def sha1_hex_length 40 end alias :sha1lite_stream :sha1_stream alias :sha1lite_hex_length :sha1_hex_length # Return the :ctime of a file. def ctime_file(filename) Puppet::FileSystem.stat(filename).send(:ctime) end alias :ctime_stream :mtime_stream def ctime(content) "" end # Return a "no checksum" def none_file(filename) "" end def none_stream noop_digest = FakeChecksum.new yield noop_digest "" end def none(content) "" end private # Perform an incremental checksum on a file. def checksum_file(digest, filename, lite = false) buffer = lite ? 512 : 4096 File.open(filename, 'rb') do |file| while content = file.read(buffer) digest << content break if lite end end digest.hexdigest end end puppet/util/pson.rb000064400000001210147634041270010344 0ustar00# A simple module to provide consistency between how we use PSON and how # ruby expects it to be used. Basically, we don't want to require # that the sender specify a class. # Ruby wants everyone to provide a 'type' field, and the PSON support # requires such a field to track the class down. Because we use our URL to # figure out what class we're working on, we don't need that, and we don't want # our consumers and producers to need to know anything about our internals. module Puppet::Util::Pson def pson_create(pson) raise ArgumentError, "No data provided in pson data" unless pson['data'] from_data_hash(pson['data']) end end puppet/util/queue.rb000064400000012023147634041270010515 0ustar00 require 'puppet/indirector' require 'puppet/util/instance_loader' # Implements a message queue client type plugin registry for use by the indirector facility. # Client modules for speaking a particular protocol (e.g. Stomp::Client for Stomp message # brokers, Memcached for Starling and Sparrow, etc.) register themselves with this module. # # Client classes are expected to live under the Puppet::Util::Queue namespace and corresponding # directory; the attempted use of a client via its typename (see below) will cause Puppet::Util::Queue # to attempt to load the corresponding plugin if it is not yet loaded. The client class registers itself # with Puppet::Util::Queue and should use the same type name as the autloader expects for the plugin file. # class Puppet::Util::Queue::SpecialMagicalClient < Messaging::SpecialMagic # ... # Puppet::Util::Queue.register_queue_type_class(self) # end # # This module reduces the rightmost segment of the class name into a pretty symbol that will # serve as the queuing client's name. Which means that the "SpecialMagicalClient" above will # be named :special_magical_client within the registry. # # Another class/module may mix-in this module, and may then make use of the registered clients. # class Queue::Fue # # mix it in at the class object level rather than instance level # extend ::Puppet::Util::Queue # end # # Queue::Fue instances can get a message queue client through the registry through the mixed-in method # +client+, which will return a class-wide singleton client instance, determined by +client_class+. # # The client plugins are expected to implement an interface similar to that of Stomp::Client: # * new should return a connected, ready-to-go client instance. Note that no arguments are passed in. # * publish_message(queue, message) should publish the _message_ to the specified _queue_. # * subscribe(queue) _block_ subscribes to _queue_ and executes _block_ upon receiving a message. # * _queue_ names are simple names independent of the message broker or client library. No "/queue/" prefixes like in Stomp::Client. module Puppet::Util::Queue extend Puppet::Util::InstanceLoader instance_load :queue_clients, 'puppet/util/queue' # Adds a new class/queue-type pair to the registry. The _type_ argument is optional; if not provided, # _type_ defaults to a lowercased, underscored symbol programmatically derived from the rightmost # namespace of klass.name. # # # register with default name +:you+ # register_queue_type(Foo::You) # # # register with explicit queue type name +:myself+ # register_queue_type(Foo::Me, :myself) # # If the type is already registered, an exception is thrown. No checking is performed of _klass_, # however; a given class could be registered any number of times, as long as the _type_ differs with # each registration. def self.register_queue_type(klass, type = nil) type ||= queue_type_from_class(klass) raise Puppet::Error, "Queue type #{type} is already registered" if instance_hash(:queue_clients).include?(type) instance_hash(:queue_clients)[type] = klass end # Given a queue type symbol, returns the associated +Class+ object. If the queue type is unknown # (meaning it hasn't been registered with this module), an exception is thrown. def self.queue_type_to_class(type) c = loaded_instance :queue_clients, type raise Puppet::Error, "Queue type #{type} is unknown." unless c c end # Given a class object _klass_, returns the programmatic default queue type name symbol for _klass_. # The algorithm is as shown in earlier examples; the last namespace segment of _klass.name_ is taken # and converted from mixed case to underscore-separated lowercase, and interned. # queue_type_from_class(Foo) -> :foo # queue_type_from_class(Foo::Too) -> :too # queue_type_from_class(Foo::ForYouTwo) -> :for_you_too # # The implicit assumption here, consistent with Puppet's approach to plugins in general, # is that all your client modules live in the same namespace, such that reduction to # a flat namespace of symbols is reasonably safe. def self.queue_type_from_class(klass) # convert last segment of classname from studly caps to lower case with underscores, and symbolize klass.name.split('::').pop.sub(/^[A-Z]/) {|c| c.downcase}.gsub(/[A-Z]/) {|c| '_' + c.downcase }.intern end # The class object for the client to be used, determined by queue configuration # settings. # Looks to the :queue_type configuration entry in the running application for # the default queue type to use. def client_class Puppet::Util::Queue.queue_type_to_class(Puppet[:queue_type]) end # Returns (instantiating as necessary) the singleton queue client instance, according to the # client_class. No arguments go to the client class constructor, meaning its up to the client class # to know how to determine its queue message source (presumably through Puppet configuration data). def client @client ||= client_class.new end end puppet/util/suidmanager.rb000064400000015740147634041270011701 0ustar00require 'facter' require 'puppet/util/warnings' require 'forwardable' require 'etc' module Puppet::Util::SUIDManager include Puppet::Util::Warnings extend Forwardable # Note groups= is handled specially due to a bug in OS X 10.6, 10.7, # and probably upcoming releases... to_delegate_to_process = [ :euid=, :euid, :egid=, :egid, :uid=, :uid, :gid=, :gid, :groups ] to_delegate_to_process.each do |method| def_delegator Process, method module_function method end def osx_maj_ver return @osx_maj_ver unless @osx_maj_ver.nil? @osx_maj_ver = Facter.value('macosx_productversion_major') || false end module_function :osx_maj_ver def groups=(grouplist) begin return Process.groups = grouplist rescue Errno::EINVAL => e #We catch Errno::EINVAL as some operating systems (OS X in particular) can # cause troubles when using Process#groups= to change *this* user / process # list of supplementary groups membership. This is done via Ruby's function # "static VALUE proc_setgroups(VALUE obj, VALUE ary)" which is effectively # a wrapper for "int setgroups(size_t size, const gid_t *list)" (part of SVr4 # and 4.3BSD but not in POSIX.1-2001) that fails and sets errno to EINVAL. # # This does not appear to be a problem with Ruby but rather an issue on the # operating system side. Therefore we catch the exception and look whether # we run under OS X or not -- if so, then we acknowledge the problem and # re-throw the exception otherwise. if osx_maj_ver and not osx_maj_ver.empty? return true else raise e end end end module_function :groups= def self.root? return Process.uid == 0 unless Puppet.features.microsoft_windows? require 'puppet/util/windows/user' Puppet::Util::Windows::User.admin? end # Methods to handle changing uid/gid of the running process. In general, # these will noop or fail on Windows, and require root to change to anything # but the current uid/gid (which is a noop). # Runs block setting euid and egid if provided then restoring original ids. # If running on Windows or without root, the block will be run with the # current euid/egid. def asuser(new_uid=nil, new_gid=nil) return yield if Puppet.features.microsoft_windows? return yield unless root? return yield unless new_uid or new_gid old_euid, old_egid = self.euid, self.egid begin change_privileges(new_uid, new_gid, false) yield ensure change_privileges(new_uid ? old_euid : nil, old_egid, false) end end module_function :asuser # If `permanently` is set, will permanently change the uid/gid of the # process. If not, it will only set the euid/egid. If only uid is supplied, # the primary group of the supplied gid will be used. If only gid is # supplied, only gid will be changed. This method will fail if used on # Windows. def change_privileges(uid=nil, gid=nil, permanently=false) return unless uid or gid unless gid uid = convert_xid(:uid, uid) gid = Etc.getpwuid(uid).gid end change_group(gid, permanently) change_user(uid, permanently) if uid end module_function :change_privileges # Changes the egid of the process if `permanently` is not set, otherwise # changes gid. This method will fail if used on Windows, or attempting to # change to a different gid without root. def change_group(group, permanently=false) gid = convert_xid(:gid, group) raise Puppet::Error, "No such group #{group}" unless gid if permanently Process::GID.change_privilege(gid) else Process.egid = gid end end module_function :change_group # As change_group, but operates on uids. If changing user permanently, # supplementary groups will be set the to default groups for the new uid. def change_user(user, permanently=false) uid = convert_xid(:uid, user) raise Puppet::Error, "No such user #{user}" unless uid if permanently # If changing uid, we must be root. So initgroups first here. initgroups(uid) Process::UID.change_privilege(uid) else # We must be root to initgroups, so initgroups before dropping euid if # we're root, otherwise elevate euid before initgroups. # change euid (to root) first. if Process.euid == 0 initgroups(uid) Process.euid = uid else Process.euid = uid initgroups(uid) end end end module_function :change_user # Make sure the passed argument is a number. def convert_xid(type, id) map = {:gid => :group, :uid => :user} raise ArgumentError, "Invalid id type #{type}" unless map.include?(type) ret = Puppet::Util.send(type, id) if ret == nil raise Puppet::Error, "Invalid #{map[type]}: #{id}" end ret end module_function :convert_xid # Initialize primary and supplemental groups to those of the target user. We # take the UID and manually look up their details in the system database, # including username and primary group. This method will fail on Windows, or # if used without root to initgroups of another user. def initgroups(uid) pwent = Etc.getpwuid(uid) Process.initgroups(pwent.name, pwent.gid) end module_function :initgroups # Run a command and capture the output # Parameters: # [command] the command to execute # [new_uid] (optional) a userid to run the command as # [new_gid] (optional) a groupid to run the command as # [options] (optional, defaults to {}) a hash of option key/value pairs; currently supported: # :override_locale (defaults to true) a flag indicating whether or puppet should temporarily override the # system locale for the duration of the command. If true, the locale will be set to 'C' to ensure consistent # output / formatting from the command, which makes it much easier to parse the output. If false, the system # locale will be respected. # :custom_environment (default {}) -- a hash of key/value pairs to set as environment variables for the duration # of the command def run_and_capture(command, new_uid=nil, new_gid=nil, options = {}) Puppet.deprecation_warning("Puppet::Util::SUIDManager.run_and_capture is deprecated; please use Puppet::Util::Execution.execute instead.") # specifying these here rather than in the method signature to allow callers to pass in a partial # set of overrides without affecting the default values for options that they don't pass in default_options = { :override_locale => true, :custom_environment => {}, } options = default_options.merge(options) output = Puppet::Util::Execution.execute(command, :failonfail => false, :combine => true, :uid => new_uid, :gid => new_gid, :override_locale => options[:override_locale], :custom_environment => options[:custom_environment]) [output, $CHILD_STATUS.dup] end module_function :run_and_capture end puppet/util/zaml.rb000064400000026725147634041270010352 0ustar00# encoding: UTF-8 # # The above encoding line is a magic comment to set the default source encoding # of this file for the Ruby interpreter. It must be on the first or second # line of the file if an interpreter is in use. In Ruby 1.9 and later, the # source encoding determines the encoding of String and Regexp objects created # from this source file. This explicit encoding is important becuase otherwise # Ruby will pick an encoding based on LANG or LC_CTYPE environment variables. # These may be different from site to site so it's important for us to # establish a consistent behavior. For more information on M17n please see: # http://links.puppetlabs.com/understanding_m17n # ZAML -- A partial replacement for YAML, writen with speed and code clarity # in mind. ZAML fixes one YAML bug (loading Exceptions) and provides # a replacement for YAML.dump unimaginatively called ZAML.dump, # which is faster on all known cases and an order of magnitude faster # with complex structures. # # http://github.com/hallettj/zaml # # ## License (from upstream) # # Copyright (c) 2008-2009 ZAML contributers # # This program is dual-licensed under the GNU General Public License # version 3 or later and under the Apache License, version 2.0. # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or (at your # option) any later version; or under the terms of the Apache License, # Version 2.0. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License and the Apache License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see # . # # You may obtain a copy of the Apache License at # . require 'yaml' class ZAML VERSION = "0.1.3" # # Class Methods # def self.dump(stuff, where='') z = new stuff.to_zaml(z) where << z.to_s end # # Instance Methods # def initialize @result = [] @indent = nil @structured_key_prefix = nil @previously_emitted_object = {} @next_free_label_number = 0 emit('--- ') end def nested(tail=' ') old_indent = @indent @indent = "#{@indent || "\n"}#{tail}" yield @indent = old_indent end class Label # # YAML only wants objects in the datastream once; if the same object # occurs more than once, we need to emit a label ("&idxxx") on the # first occurrence and then emit a back reference (*idxxx") on any # subsequent occurrence(s). # # To accomplish this we keeps a hash (by object id) of the labels of # the things we serialize as we begin to serialize them. The labels # initially serialize as an empty string (since most objects are only # going to be be encountered once), but can be changed to a valid # (by assigning it a number) the first time it is subsequently used, # if it ever is. Note that we need to do the label setup BEFORE we # start to serialize the object so that circular structures (in # which we will encounter a reference to the object as we serialize # it can be handled). # attr_accessor :this_label_number def initialize(obj,indent) @indent = indent @this_label_number = nil @obj = obj # prevent garbage collection so that object id isn't reused end def to_s @this_label_number ? ('&id%03d%s' % [@this_label_number, @indent]) : '' end def reference @reference ||= '*id%03d' % @this_label_number end end def label_for(obj) @previously_emitted_object[obj.object_id] end def new_label_for(obj) label = Label.new(obj,(Hash === obj || Array === obj) ? "#{@indent || "\n"} " : ' ') @previously_emitted_object[obj.object_id] = label label end def first_time_only(obj) if label = label_for(obj) label.this_label_number ||= (@next_free_label_number += 1) emit(label.reference) else with_structured_prefix(obj) do emit(new_label_for(obj)) yield end end end def with_structured_prefix(obj) if @structured_key_prefix unless obj.is_a?(String) and obj !~ /\n/ emit(@structured_key_prefix) @structured_key_prefix = nil end end yield end def emit(s) @result << s @recent_nl = false unless s.kind_of?(Label) end def nl(s = nil) emit(@indent || "\n") unless @recent_nl emit(s) if s @recent_nl = true end def to_s @result.join end def prefix_structured_keys(x) @structured_key_prefix = x yield nl unless @structured_key_prefix @structured_key_prefix = nil end end ################################################################ # # Behavior for custom classes # ################################################################ class Object # Users of this method need to do set math consistently with the # result. Since #instance_variables returns strings in 1.8 and symbols # on 1.9, standardize on symbols if RUBY_VERSION[0,3] == '1.8' def to_yaml_properties instance_variables.map(&:to_sym) end else def to_yaml_properties instance_variables end end def yaml_property_munge(x) x end def zamlized_class_name(root) cls = self.class "!ruby/#{root.name.downcase}#{cls == root ? '' : ":#{cls.respond_to?(:name) ? cls.name : cls}"}" end def to_zaml(z) z.first_time_only(self) { z.emit(zamlized_class_name(Object)) z.nested { instance_variables = to_yaml_properties if instance_variables.empty? z.emit(" {}") else instance_variables.each { |v| z.nl v.to_s[1..-1].to_zaml(z) # Remove leading '@' z.emit(': ') yaml_property_munge(instance_variable_get(v)).to_zaml(z) } end } } end end ################################################################ # # Behavior for built-in classes # ################################################################ class NilClass def to_zaml(z) z.emit('') # NOTE: blank turns into nil in YAML.load end end class Symbol def to_zaml(z) z.emit("!ruby/sym ") to_s.to_zaml(z) end end class TrueClass def to_zaml(z) z.emit('true') end end class FalseClass def to_zaml(z) z.emit('false') end end class Numeric def to_zaml(z) z.emit(self) end end class Regexp def to_zaml(z) z.first_time_only(self) { z.emit("#{zamlized_class_name(Regexp)} #{inspect}") } end end class Exception def to_zaml(z) z.emit(zamlized_class_name(Exception)) z.nested { z.nl("message: ") message.to_zaml(z) } end # # Monkey patch for buggy Exception restore in YAML # # This makes it work for now but is not very future-proof; if things # change we'll most likely want to remove this. To mitigate the risks # as much as possible, we test for the bug before appling the patch. # if respond_to? :yaml_new and yaml_new(self, :tag, "message" => "blurp").message != "blurp" def self.yaml_new( klass, tag, val ) o = YAML.object_maker( klass, {} ).exception(val.delete( 'message')) val.each_pair do |k,v| o.instance_variable_set("@#{k}", v) end o end end end class String ZAML_ESCAPES = { "\a" => "\\a", "\e" => "\\e", "\f" => "\\f", "\n" => "\\n", "\r" => "\\r", "\t" => "\\t", "\v" => "\\v" } def to_zaml(z) z.with_structured_prefix(self) do case when self == '' z.emit('""') when self.to_ascii8bit !~ /\A(?: # ?: non-capturing group (grouping with no back references) [\x09\x0A\x0D\x20-\x7E] # ASCII | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 )*\z/mnx # Emit the binary tag, then recurse. Ruby splits BASE64 output at the 60 # character mark when packing strings, and we can wind up a multi-line # string here. We could reimplement the multi-line string logic, # but why would we - this does just as well for producing solid output. z.emit("!binary ") [self].pack("m*").to_zaml(z) # Only legal UTF-8 characters can make it this far, so we are safe # against emitting something dubious. That means we don't need to mess # about, just emit them directly. --daniel 2012-07-14 when ((self =~ /\A[a-zA-Z\/][-\[\]_\/.a-zA-Z0-9]*\z/) and (self !~ /^(?:true|false|yes|no|on|null|off)$/i)) # simple string literal, safe to emit unquoted. z.emit(self) when (self =~ /\n/ and self !~ /\A\s/ and self !~ /\s\z/) # embedded newline, split line-wise in quoted string block form. if self[-1..-1] == "\n" then z.emit('|+') else z.emit('|-') end z.nested { split("\n",-1).each { |line| z.nl; z.emit(line) } } else # ...though we still have to escape unsafe characters. escaped = gsub(/[\\"\x00-\x1F]/) do |c| ZAML_ESCAPES[c] || "\\x#{c[0].ord.to_s(16)}" end z.emit("\"#{escaped}\"") end end end # Return a guranteed ASCII-8BIT encoding for Ruby 1.9 This is a helper # method for other methods that perform regular expressions against byte # sequences deliberately rather than dealing with characters. # The method may or may not return a new instance. if String.method_defined?(:encoding) ASCII_ENCODING = Encoding.find("ASCII-8BIT") def to_ascii8bit if self.encoding == ASCII_ENCODING self else self.dup.force_encoding(ASCII_ENCODING) end end else def to_ascii8bit self end end end class Hash def to_zaml(z) z.first_time_only(self) { z.nested { if empty? z.emit('{}') else each_pair { |k, v| z.nl z.prefix_structured_keys('? ') { k.to_zaml(z) } z.emit(': ') v.to_zaml(z) } end } } end end class Array def to_zaml(z) z.first_time_only(self) { z.nested { if empty? z.emit('[]') else each { |v| z.nl('- '); v.to_zaml(z) } end } } end end class Time def to_zaml(z) # 2008-12-06 10:06:51.373758 -07:00 ms = ("%0.6f" % (usec * 1e-6))[2..-1] offset = "%+0.2i:%0.2i" % [utc_offset / 3600.0, (utc_offset / 60) % 60] z.emit(self.strftime("%Y-%m-%d %H:%M:%S.#{ms} #{offset}")) end end class Date def to_zaml(z) z.emit(strftime('%Y-%m-%d')) end end class Range def to_zaml(z) z.first_time_only(self) { z.emit(zamlized_class_name(Range)) z.nested { z.nl z.emit('begin: ') z.emit(first) z.nl z.emit('end: ') z.emit(last) z.nl z.emit('excl: ') z.emit(exclude_end?) } } end end puppet/util/command_line/trollop.rb000064400000070307147634041270013522 0ustar00## lib/trollop.rb -- trollop command-line processing library ## Author:: William Morgan (mailto: wmorgan-trollop@masanjin.net) ## Copyright:: Copyright 2007 William Morgan ## License:: the same terms as ruby itself ## ## 2012-03: small changes made by cprice (chris@puppetlabs.com); ## patch submitted for upstream consideration: ## https://gitorious.org/trollop/mainline/merge_requests/9 ## 2012-08: namespace changes made by Jeff McCune (jeff@puppetlabs.com) ## moved Trollop into Puppet::Util::CommandLine to prevent monkey ## patching the upstream trollop library if also loaded. require 'date' module Puppet module Util class CommandLine module Trollop VERSION = "1.16.2" ## Thrown by Parser in the event of a commandline error. Not needed if ## you're using the Trollop::options entry. class CommandlineError < StandardError; end ## Thrown by Parser if the user passes in '-h' or '--help'. Handled ## automatically by Trollop#options. class HelpNeeded < StandardError; end ## Thrown by Parser if the user passes in '-h' or '--version'. Handled ## automatically by Trollop#options. class VersionNeeded < StandardError; end ## Regex for floating point numbers FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/ ## Regex for parameters PARAM_RE = /^-(-|\.$|[^\d\.])/ ## The commandline parser. In typical usage, the methods in this class ## will be handled internally by Trollop::options. In this case, only the ## #opt, #banner and #version, #depends, and #conflicts methods will ## typically be called. ## ## If you want to instantiate this class yourself (for more complicated ## argument-parsing logic), call #parse to actually produce the output hash, ## and consider calling it from within ## Trollop::with_standard_exception_handling. class Parser ## The set of values that indicate a flag option when passed as the ## +:type+ parameter of #opt. FLAG_TYPES = [:flag, :bool, :boolean] ## The set of values that indicate a single-parameter (normal) option when ## passed as the +:type+ parameter of #opt. ## ## A value of +io+ corresponds to a readable IO resource, including ## a filename, URI, or the strings 'stdin' or '-'. SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date] ## The set of values that indicate a multiple-parameter option (i.e., that ## takes multiple space-separated values on the commandline) when passed as ## the +:type+ parameter of #opt. MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates] ## The complete set of legal values for the +:type+ parameter of #opt. TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc: ## The values from the commandline that were not interpreted by #parse. attr_reader :leftovers ## The complete configuration hashes for each option. (Mainly useful ## for testing.) attr_reader :specs ## A flag that determines whether or not to attempt to automatically generate "short" options if they are not ## explicitly specified. attr_accessor :create_default_short_options ## A flag that determines whether or not to raise an error if the parser is passed one or more ## options that were not registered ahead of time. If 'true', then the parser will simply ## ignore options that it does not recognize. attr_accessor :ignore_invalid_options ## A flag indicating whether or not the parser should attempt to handle "--help" and ## "--version" specially. If 'false', it will treat them just like any other option. attr_accessor :handle_help_and_version ## Initializes the parser, and instance-evaluates any block given. def initialize *a, &b @version = nil @leftovers = [] @specs = {} @long = {} @short = {} @order = [] @constraints = [] @stop_words = [] @stop_on_unknown = false #instance_eval(&b) if b # can't take arguments cloaker(&b).bind(self).call(*a) if b end ## Define an option. +name+ is the option name, a unique identifier ## for the option that you will use internally, which should be a ## symbol or a string. +desc+ is a string description which will be ## displayed in help messages. ## ## Takes the following optional arguments: ## ## [+:long+] Specify the long form of the argument, i.e. the form with two dashes. If unspecified, will be automatically derived based on the argument name by turning the +name+ option into a string, and replacing any _'s by -'s. ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+. ## [+:type+] Require that the argument take a parameter or parameters of type +type+. For a single parameter, the value can be a member of +SINGLE_ARG_TYPES+, or a corresponding Ruby class (e.g. +Integer+ for +:int+). For multiple-argument parameters, the value can be any member of +MULTI_ARG_TYPES+ constant. If unset, the default argument type is +:flag+, meaning that the argument does not take a parameter. The specification of +:type+ is not necessary if a +:default+ is given. ## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Trollop::options) will have a +nil+ value for this key unless the argument is given on the commandline. The argument type is derived automatically from the class of the default value given, so specifying a +:type+ is not necessary if a +:default+ is given. (But see below for an important caveat when +:multi+: is specified too.) If the argument is a flag, and the default is set to +true+, then if it is specified on the commandline the value will be +false+. ## [+:required+] If set to +true+, the argument must be provided on the commandline. ## [+:multi+] If set to +true+, allows multiple occurrences of the option on the commandline. Otherwise, only a single instance of the option is allowed. (Note that this is different from taking multiple parameters. See below.) ## ## Note that there are two types of argument multiplicity: an argument ## can take multiple values, e.g. "--arg 1 2 3". An argument can also ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2". ## ## Arguments that take multiple values should have a +:type+ parameter ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+ ## value of an array of the correct type (e.g. [String]). The ## value of this argument will be an array of the parameters on the ## commandline. ## ## Arguments that can occur multiple times should be marked with ## +:multi+ => +true+. The value of this argument will also be an array. ## In contrast with regular non-multi options, if not specified on ## the commandline, the default value will be [], not nil. ## ## These two attributes can be combined (e.g. +:type+ => +:strings+, ## +:multi+ => +true+), in which case the value of the argument will be ## an array of arrays. ## ## There's one ambiguous case to be aware of: when +:multi+: is true and a ## +:default+ is set to an array (of something), it's ambiguous whether this ## is a multi-value argument as well as a multi-occurrence argument. ## In thise case, Trollop assumes that it's not a multi-value argument. ## If you want a multi-value, multi-occurrence argument with a default ## value, you must specify +:type+ as well. def opt name, desc="", opts={} raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name ## fill in :type opts[:type] = # normalize case opts[:type] when :boolean, :bool; :flag when :integer; :int when :integers; :ints when :double; :float when :doubles; :floats when Class case opts[:type].name when 'TrueClass', 'FalseClass'; :flag when 'String'; :string when 'Integer'; :int when 'Float'; :float when 'IO'; :io when 'Date'; :date else raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'" end when nil; nil else raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type]) opts[:type] end ## for options with :multi => true, an array default doesn't imply ## a multi-valued argument. for that you have to specify a :type ## as well. (this is how we disambiguate an ambiguous situation; ## see the docs for Parser#opt for details.) disambiguated_default = if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type] opts[:default].first else opts[:default] end type_from_default = case disambiguated_default when Integer; :int when Numeric; :float when TrueClass, FalseClass; :flag when String; :string when IO; :io when Date; :date when Array if opts[:default].empty? raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'" end case opts[:default][0] # the first element determines the types when Integer; :ints when Numeric; :floats when String; :strings when IO; :ios when Date; :dates else raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'" end when nil; nil else raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'" end raise ArgumentError, ":type specification and default type don't match (default type is #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default opts[:type] = opts[:type] || type_from_default || :flag ## fill in :long opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-") opts[:long] = case opts[:long] when /^--([^-].*)$/ $1 when /^[^-]/ opts[:long] else raise ArgumentError, "invalid long option name #{opts[:long].inspect}" end raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]] ## fill in :short opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none opts[:short] = case opts[:short] when /^-(.)$/; $1 when nil, :none, /^.$/; opts[:short] else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'" end if opts[:short] raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]] raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX end ## fill in :default for flags opts[:default] = false if opts[:type] == :flag && opts[:default].nil? ## autobox :default for :multi (multi-occurrence) arguments opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array) ## fill in :multi opts[:multi] ||= false opts[:desc] ||= desc @long[opts[:long]] = name @short[opts[:short]] = name if opts[:short] && opts[:short] != :none @specs[name] = opts @order << [:opt, name] end ## Sets the version string. If set, the user can request the version ## on the commandline. Should probably be of the form " ## ". def version s=nil; @version = s if s; @version end ## Adds text to the help display. Can be interspersed with calls to ## #opt to build a multi-section help page. def banner s; @order << [:text, s] end alias :text :banner ## Marks two (or more!) options as requiring each other. Only handles ## undirected (i.e., mutual) dependencies. Directed dependencies are ## better modeled with Trollop::die. def depends *syms syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] } @constraints << [:depends, syms] end ## Marks two (or more!) options as conflicting. def conflicts *syms syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] } @constraints << [:conflicts, syms] end ## Defines a set of words which cause parsing to terminate when ## encountered, such that any options to the left of the word are ## parsed as usual, and options to the right of the word are left ## intact. ## ## A typical use case would be for subcommand support, where these ## would be set to the list of subcommands. A subsequent Trollop ## invocation would then be used to parse subcommand options, after ## shifting the subcommand off of ARGV. def stop_on *words @stop_words = [*words].flatten end ## Similar to #stop_on, but stops on any unknown word when encountered ## (unless it is a parameter for an argument). This is useful for ## cases where you don't know the set of subcommands ahead of time, ## i.e., without first parsing the global options. def stop_on_unknown @stop_on_unknown = true end ## Parses the commandline. Typically called by Trollop::options, ## but you can call it directly if you need more control. ## ## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions. def parse cmdline=ARGV vals = {} required = {} if handle_help_and_version opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"] opt :help, "Show this message" unless @specs[:help] || @long["help"] end @specs.each do |sym, opts| required[sym] = true if opts[:required] vals[sym] = opts[:default] vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil end resolve_default_short_options if create_default_short_options ## resolve symbols given_args = {} @leftovers = each_arg cmdline do |arg, params| sym = case arg when /^-([^-])$/ @short[$1] when /^--no-([^-]\S*)$/ @long["[no-]#{$1}"] when /^--([^-]\S*)$/ @long[$1] ? @long[$1] : @long["[no-]#{$1}"] else raise CommandlineError, "invalid argument syntax: '#{arg}'" end unless sym next 0 if ignore_invalid_options raise CommandlineError, "unknown argument '#{arg}'" unless sym end if given_args.include?(sym) && !@specs[sym][:multi] raise CommandlineError, "option '#{arg}' specified multiple times" end given_args[sym] ||= {} given_args[sym][:arg] = arg given_args[sym][:params] ||= [] # The block returns the number of parameters taken. num_params_taken = 0 unless params.nil? if SINGLE_ARG_TYPES.include?(@specs[sym][:type]) given_args[sym][:params] << params[0, 1] # take the first parameter num_params_taken = 1 elsif MULTI_ARG_TYPES.include?(@specs[sym][:type]) given_args[sym][:params] << params # take all the parameters num_params_taken = params.size end end num_params_taken end if handle_help_and_version ## check for version and help args raise VersionNeeded if given_args.include? :version raise HelpNeeded if given_args.include? :help end ## check constraint satisfaction @constraints.each do |type, syms| constraint_sym = syms.find { |sym| given_args[sym] } next unless constraint_sym case type when :depends syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym } when :conflicts syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) } end end required.each do |sym, val| raise CommandlineError, "option --#{@specs[sym][:long]} must be specified" unless given_args.include? sym end ## parse parameters given_args.each do |sym, given_data| arg = given_data[:arg] params = given_data[:params] opts = @specs[sym] raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag vals["#{sym}_given".intern] = true # mark argument as specified on the commandline case opts[:type] when :flag if arg =~ /^--no-/ and sym.to_s =~ /^--\[no-\]/ vals[sym] = opts[:default] else vals[sym] = !opts[:default] end when :int, :ints vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } } when :float, :floats vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } } when :string, :strings vals[sym] = params.map { |pg| pg.map { |p| p.to_s } } when :io, :ios vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } } when :date, :dates vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } } end if SINGLE_ARG_TYPES.include?(opts[:type]) unless opts[:multi] # single parameter vals[sym] = vals[sym][0][0] else # multiple options, each with a single parameter vals[sym] = vals[sym].map { |p| p[0] } end elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi] vals[sym] = vals[sym][0] # single option, with multiple parameters end # else: multiple options, with multiple parameters opts[:callback].call(vals[sym]) if opts.has_key?(:callback) end ## modify input in place with only those ## arguments we didn't process cmdline.clear @leftovers.each { |l| cmdline << l } ## allow openstruct-style accessors class << vals def method_missing(m, *args) self[m] || self[m.to_s] end end vals end def parse_date_parameter param, arg #:nodoc: begin begin time = Chronic.parse(param) rescue NameError # chronic is not available end time ? Date.new(time.year, time.month, time.day) : Date.parse(param) rescue ArgumentError raise CommandlineError, "option '#{arg}' needs a date", $!.backtrace end end ## Print the help message to +stream+. def educate stream=$stdout width # just calculate it now; otherwise we have to be careful not to # call this unless the cursor's at the beginning of a line. left = {} @specs.each do |name, spec| left[name] = "--#{spec[:long]}" + (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") + case spec[:type] when :flag; "" when :int; " " when :ints; " " when :string; " " when :strings; " " when :float; " " when :floats; " " when :io; " " when :ios; " " when :date; " " when :dates; " " end end leftcol_width = left.values.map { |s| s.length }.max || 0 rightcol_start = leftcol_width + 6 # spaces unless @order.size > 0 && @order.first.first == :text stream.puts "#@version\n" if @version stream.puts "Options:" end @order.each do |what, opt| if what == :text stream.puts wrap(opt) next end spec = @specs[opt] stream.printf " %#{leftcol_width}s: ", left[opt] desc = spec[:desc] + begin default_s = case spec[:default] when $stdout; "" when $stdin; "" when $stderr; "" when Array spec[:default].join(", ") else spec[:default].to_s end if spec[:default] if spec[:desc] =~ /\.$/ " (Default: #{default_s})" else " (default: #{default_s})" end else "" end end stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start) end end def width #:nodoc: @width ||= if $stdout.tty? begin require 'curses' Curses::init_screen x = Curses::cols Curses::close_screen x rescue Exception 80 end else 80 end end def wrap str, opts={} # :nodoc: if str == "" [""] else str.split("\n").map { |s| wrap_line s, opts }.flatten end end ## The per-parser version of Trollop::die (see that for documentation). def die arg, msg if msg $stderr.puts "Error: argument --#{@specs[arg][:long]} #{msg}." else $stderr.puts "Error: #{arg}." end $stderr.puts "Try --help for help." exit(-1) end private ## yield successive arg, parameter pairs def each_arg args remains = [] i = 0 until i >= args.length if @stop_words.member? args[i] remains += args[i .. -1] return remains end case args[i] when /^--$/ # arg terminator remains += args[(i + 1) .. -1] return remains when /^--(\S+?)=(.*)$/ # long argument with equals yield "--#{$1}", [$2] i += 1 when /^--(\S+)$/ # long argument params = collect_argument_parameters(args, i + 1) unless params.empty? num_params_taken = yield args[i], params unless num_params_taken if @stop_on_unknown remains += args[i + 1 .. -1] return remains else remains += params end end i += 1 + num_params_taken else # long argument no parameter yield args[i], nil i += 1 end when /^-(\S+)$/ # one or more short arguments shortargs = $1.split(//) shortargs.each_with_index do |a, j| if j == (shortargs.length - 1) params = collect_argument_parameters(args, i + 1) unless params.empty? num_params_taken = yield "-#{a}", params unless num_params_taken if @stop_on_unknown remains += args[i + 1 .. -1] return remains else remains += params end end i += 1 + num_params_taken else # argument no parameter yield "-#{a}", nil i += 1 end else yield "-#{a}", nil end end else if @stop_on_unknown remains += args[i .. -1] return remains else remains << args[i] i += 1 end end end remains end def parse_integer_parameter param, arg raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/ param.to_i end def parse_float_parameter param, arg raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE param.to_f end def parse_io_parameter param, arg case param when /^(stdin|-)$/i; $stdin else require 'open-uri' begin open param rescue SystemCallError => e raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}", e.backtrace end end end def collect_argument_parameters args, start_at params = [] pos = start_at while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do params << args[pos] pos += 1 end params end def resolve_default_short_options @order.each do |type, name| next unless type == :opt opts = @specs[name] next if opts[:short] c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) } if c # found a character to use opts[:short] = c @short[c] = name end end end def wrap_line str, opts={} prefix = opts[:prefix] || 0 width = opts[:width] || (self.width - 1) start = 0 ret = [] until start > str.length nextt = if start + width >= str.length str.length else x = str.rindex(/\s/, start + width) x = str.index(/\s/, start) if x && x < start x || str.length end ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt] start = nextt + 1 end ret end ## instance_eval but with ability to handle block arguments ## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html def cloaker &b (class << self; self; end).class_eval do define_method :cloaker_, &b meth = instance_method :cloaker_ remove_method :cloaker_ meth end end end ## The easy, syntactic-sugary entry method into Trollop. Creates a Parser, ## passes the block to it, then parses +args+ with it, handling any errors or ## requests for help or version information appropriately (and then exiting). ## Modifies +args+ in place. Returns a hash of option values. ## ## The block passed in should contain zero or more calls to +opt+ ## (Parser#opt), zero or more calls to +text+ (Parser#text), and ## probably a call to +version+ (Parser#version). ## ## The returned block contains a value for every option specified with ## +opt+. The value will be the value given on the commandline, or the ## default value if the option was not specified on the commandline. For ## every option specified on the commandline, a key "