diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 000000000..72479d6dd --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,512 @@ +AllCops: + Include: + - 'lib/**/*.rb' + - 'ext/**/*.rb' + Exclude: + - '**/*.erb' + - 'acceptance/**/*' + - 'autotest/**/*' + - 'spec/**/*' + - 'tasks/**/*' + - 'lib/puppet/parser/parser.rb' + - 'lib/puppet/pops/parser/eparser.rb' + - 'lib/puppet/external/nagios/parser.rb' + +Lint/ConditionPosition: + Enabled: true + +Lint/ElseLayout: + Enabled: true + +Lint/UnreachableCode: + Enabled: true + +Lint/UselessComparison: + Enabled: true + +# MAYBE useful - no return inside ensure block. +Lint/EnsureReturn: + Enabled: false + +# MAYBE useful - errors when rescue {} happens. +Lint/HandleExceptions: + Enabled: false + +# MAYBE useful - catches while 1 +Lint/LiteralInCondition: + Enabled: false + +# MAYBE useful - but too many instances +Lint/ShadowingOuterLocalVariable: + Enabled: false + +# Can catch complicated strings. +Lint/LiteralInInterpolation: + Enabled: true + + +# DISABLED really useless. Detects return as last statement. +Style/RedundantReturn: + Enabled: false + +# DISABLED since the instances do not seem to indicate any specific errors. +Lint/AmbiguousOperator: + Enabled: false + +# DISABLED since for all the checked, we are basically checking nil +# TODO: Change the checking so that if the variable being assigned to has +# a value ALREADY, then raise an error. +Lint/AssignmentInCondition: + Enabled: false + +# DISABLED - not useful +Style/SpaceBeforeComment: + Enabled: false + +# DISABLED - not useful +Style/HashSyntax: + Enabled: false + +# USES: as shortcut for non nil&valid checking a = x() and a.empty? +# DISABLED - not useful +Style/AndOr: + Enabled: false + +# DISABLED - not useful +Style/RedundantSelf: + Enabled: false + +# DISABLED - not useful +Style/MethodLength: + Enabled: false + +# DISABLED - not useful +Style/WhileUntilModifier: + Enabled: false + +# DISABLED - the offender is just haskell envy +Lint/AmbiguousRegexpLiteral: + Enabled: false + +# DISABLED +Lint/Eval: + Enabled: false +# DISABLED +Lint/BlockAlignment: + Enabled: false + +# DISABLED +Lint/DefEndAlignment: + Enabled: false + +# DISABLED +Lint/EndAlignment: + Enabled: false + +# DISABLED +Lint/DeprecatedClassMethods: + Enabled: false + +# DISABLED +Lint/Loop: + Enabled: false + +# DISABLED +Lint/ParenthesesAsGroupedExpression: + Enabled: false + +Lint/RescueException: + Enabled: false + +Lint/StringConversionInInterpolation: + Enabled: false + +Lint/UnusedBlockArgument: + Enabled: false + +Lint/UnusedMethodArgument: + Enabled: false + +# DISABLED - TODO +Lint/UselessAccessModifier: + Enabled: false + +# DISABLED - TODO +Lint/UselessAssignment: + Enabled: false + +# DISABLED - TODO +Lint/Void: + Enabled: false + +Style/AccessModifierIndentation: + Enabled: false + +Style/AccessorMethodName: + Enabled: false + +Style/Alias: + Enabled: false + +Style/AlignArray: + Enabled: false + +Style/AlignHash: + Enabled: false + +Style/AlignParameters: + Enabled: false + +Style/BlockNesting: + Enabled: false + +Style/AsciiComments: + Enabled: false + +Style/Attr: + Enabled: false + +Style/Blocks: + Enabled: false + +Style/BracesAroundHashParameters: + Enabled: false + +Style/CaseEquality: + Enabled: false + +Style/CaseIndentation: + Enabled: false + +Style/CharacterLiteral: + Enabled: false + +Style/ClassAndModuleCamelCase: + Enabled: false + +Style/ClassAndModuleChildren: + Enabled: false + +Style/ClassCheck: + Enabled: false + +Style/ClassLength: + Enabled: false + +Style/ClassMethods: + Enabled: false + +Style/ClassVars: + Enabled: false + +Style/WhenThen: + Enabled: false + + +# DISABLED - not useful +Style/WordArray: + Enabled: false + +Style/UnneededPercentQ: + Enabled: false + +Style/Tab: + Enabled: false + +Style/SpaceBeforeSemicolon: + Enabled: false + +Style/TrailingBlankLines: + Enabled: false + +Style/SpaceInsideBlockBraces: + Enabled: false + +Style/SpaceInsideBrackets: + Enabled: false + +Style/SpaceInsideHashLiteralBraces: + Enabled: false + +Style/SpaceInsideParens: + Enabled: false + +Style/LeadingCommentSpace: + Enabled: false + +Style/SingleSpaceBeforeFirstArg: + Enabled: false + +Style/SpaceAfterColon: + Enabled: false + +Style/SpaceAfterComma: + Enabled: false + +Style/SpaceAfterControlKeyword: + Enabled: false + +Style/SpaceAfterMethodName: + Enabled: false + +Style/SpaceAfterNot: + Enabled: false + +Style/SpaceAfterSemicolon: + Enabled: false + +Style/SpaceAroundEqualsInParameterDefault: + Enabled: false + +Style/SpaceAroundOperators: + Enabled: false + +Style/SpaceBeforeBlockBraces: + Enabled: false + +Style/SpaceBeforeComma: + Enabled: false + + +Style/CollectionMethods: + Enabled: false + +Style/CommentIndentation: + Enabled: false + +Style/ColonMethodCall: + Enabled: false + +Style/CommentAnnotation: + Enabled: false + +Style/CyclomaticComplexity: + Enabled: false + +Style/ConstantName: + Enabled: false + +Style/Documentation: + Enabled: false + +Style/DefWithParentheses: + Enabled: false + +Style/DeprecatedHashMethods: + Enabled: false + +Style/DotPosition: + Enabled: false + +# DISABLED - used for converting to bool +Style/DoubleNegation: + Enabled: false + +Style/EachWithObject: + Enabled: false + +Style/EmptyLineBetweenDefs: + Enabled: false + +Style/IndentArray: + Enabled: false + +Style/IndentHash: + Enabled: false + +Style/IndentationConsistency: + Enabled: false + +Style/IndentationWidth: + Enabled: false + +Style/EmptyLines: + Enabled: false + +Style/EmptyLinesAroundAccessModifier: + Enabled: false + +Style/EmptyLinesAroundBody: + Enabled: false + +Style/EmptyLiteral: + Enabled: false + +Style/LineLength: + Enabled: false + +Style/MethodCallParentheses: + Enabled: false + +Style/MethodDefParentheses: + Enabled: false + +Style/LineEndConcatenation: + Enabled: false + +Style/TrailingWhitespace: + Enabled: false + +Style/StringLiterals: + Enabled: false + +Style/TrailingComma: + Enabled: false + +Style/GlobalVars: + Enabled: false + +Style/GuardClause: + Enabled: false + +Style/IfUnlessModifier: + Enabled: false + +Style/MultilineIfThen: + Enabled: false + +Style/NegatedIf: + Enabled: false + +Style/NegatedWhile: + Enabled: false + +Style/Next: + Enabled: false + +Style/SingleLineBlockParams: + Enabled: false + +Style/SingleLineMethods: + Enabled: false + +Style/SpecialGlobalVars: + Enabled: false + + +Style/TrivialAccessors: + Enabled: false + +Style/UnlessElse: + Enabled: false + +Style/UnneededPercentX: + Enabled: false + +Style/VariableInterpolation: + Enabled: false + +Style/VariableName: + Enabled: false + +Style/WhileUntilDo: + Enabled: false + +Style/EvenOdd: + Enabled: false + +Style/FileName: + Enabled: false + +Style/For: + Enabled: false + +Style/Lambda: + Enabled: false + +Style/MethodName: + Enabled: false + +Style/MultilineTernaryOperator: + Enabled: false + +Style/NestedTernaryOperator: + Enabled: false + +Style/NilComparison: + Enabled: false + +Style/FormatString: + Enabled: false + +Style/MultilineBlockChain: + Enabled: false + +Style/Semicolon: + Enabled: false + +Style/SignalException: + Enabled: false + +Style/NonNilCheck: + Enabled: false + +Style/Not: + Enabled: false + +Style/NumericLiterals: + Enabled: false + +Style/OneLineConditional: + Enabled: false + +Style/OpMethod: + Enabled: false + +Style/ParenthesesAroundCondition: + Enabled: false + +Style/PercentLiteralDelimiters: + Enabled: false + +Style/PerlBackrefs: + Enabled: false + +Style/PredicateName: + Enabled: false + +Style/RedundantException: + Enabled: false + +Style/SelfAssignment: + Enabled: false + + +Style/Proc: + Enabled: false + +Style/RaiseArgs: + Enabled: false + +Style/RedundantBegin: + Enabled: false + +Style/RescueModifier: + Enabled: false + +Style/RegexpLiteral: + Enabled: false + +Lint/UnderscorePrefixedVariableName: + Enabled: false + +Style/ParameterLists: + Enabled: false + +Lint/RequireParentheses: + Enabled: false + +Lint/SpaceBeforeFirstArg: + Enabled: false + +Style/ModuleFunction: + Enabled: false + +Lint/Debugger: + Enabled: false + +Style/IfWithSemicolon: + Enabled: false + +Style/Encoding: + Enabled: false diff --git a/.travis.yml b/.travis.yml index e3f8e696e..b851cd74c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,24 @@ language: ruby bundler_args: --without development -script: "bundle exec rake \"parallel:spec[2]\"" +script: + - "bundle exec rake $CHECK" notifications: email: false rvm: - 2.1.1 - 2.0.0 - 1.9.3 - 1.8.7-p374 + +env: + - "CHECK=parallel:spec\\[2\\]" + - "CHECK=rubocop" + +matrix: + exclude: + - rvm: 2.0.0 + env: "CHECK=rubocop" + - rvm: 1.9.3 + env: "CHECK=rubocop" + - rvm: 1.8.7-p374 + env: "CHECK=rubocop" diff --git a/Gemfile b/Gemfile index 712795846..52ff658c5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,102 +1,104 @@ source ENV['GEM_SOURCE'] || "https://rubygems.org" def location_for(place, fake_version = nil) if place =~ /^(git[:@][^#]*)#(.*)/ [fake_version, { :git => $1, :branch => $2, :require => false }].compact elsif place =~ /^file:\/\/(.*)/ ['>= 0', { :path => File.expand_path($1), :require => false }] else [place, { :require => false }] end end # C Ruby (MRI) or Rubinius, but NOT Windows platforms :ruby do gem 'pry', :group => :development gem 'yard', :group => :development gem 'redcarpet', '~> 2.0', :group => :development gem "racc", "1.4.9", :group => :development # To enable the augeas feature, use this gem. # Note that it is a native gem, so the augeas headers/libs # are neeed. #gem 'ruby-augeas', :group => :development end if !ENV['PUPPET_LOADED'] gem "puppet", :path => File.dirname(__FILE__), :require => false end gem "facter", *location_for(ENV['FACTER_LOCATION'] || ['> 1.6', '< 3']) gem "hiera", *location_for(ENV['HIERA_LOCATION'] || '~> 1.0') gem "rake", "10.1.1", :require => false group(:development, :test) do gem "rspec", "~> 2.14.0", :require => false # Mocha is not compatible across minor version changes; because of this only # versions matching ~> 0.10.5 are supported. All other versions are unsupported # and can be expected to fail. gem "mocha", "~> 0.10.5", :require => false gem "yarjuf", "~> 1.0" # json-schema does not support windows, so omit it from the platforms list # json-schema uses multi_json, but chokes with multi_json 1.7.9, so prefer 1.7.7 gem "multi_json", "1.7.7", :require => false, :platforms => [:ruby, :jruby] gem "json-schema", "2.1.1", :require => false, :platforms => [:ruby, :jruby] + + gem "rubocop", :platforms => [:ruby] unless RUBY_VERSION =~ /^1.8/ end group(:development) do if RUBY_PLATFORM != 'java' case RUBY_VERSION when /^1.8/ gem 'ruby-prof', "~> 0.13.1", :require => false else gem 'ruby-prof', :require => false end end end group(:extra) do gem "rack", "~> 1.4", :require => false gem "activerecord", '~> 3.2', :require => false gem "couchrest", '~> 1.0', :require => false gem "net-ssh", '~> 2.1', :require => false gem "puppetlabs_spec_helper", :require => false # rest-client is used only by couchrest, so when # that dependency goes away, this one can also gem "rest-client", '1.6.7', :require => false gem "stomp", :require => false gem "tzinfo", :require => false case RUBY_PLATFORM when 'java' gem "jdbc-sqlite3", :require => false gem "msgpack-jruby", :require => false else gem "sqlite3", :require => false gem "msgpack", :require => false end end require 'yaml' data = YAML.load_file(File.join(File.dirname(__FILE__), 'ext', 'project_data.yaml')) bundle_platforms = data['bundle_platforms'] x64_platform = Gem::Platform.local.cpu == 'x64' data['gem_platform_dependencies'].each_pair do |gem_platform, info| next if gem_platform == 'x86-mingw32' && x64_platform next if gem_platform == 'x64-mingw32' && !x64_platform if bundle_deps = info['gem_runtime_dependencies'] bundle_platform = bundle_platforms[gem_platform] or raise "Missing bundle_platform" platform(bundle_platform.intern) do bundle_deps.each_pair do |name, version| gem(name, version, :require => false) end end end end if File.exists? "#{__FILE__}.local" eval(File.read("#{__FILE__}.local"), binding) end # vim:filetype=ruby diff --git a/Rakefile b/Rakefile index 3bb695abe..d0d277850 100644 --- a/Rakefile +++ b/Rakefile @@ -1,68 +1,80 @@ # Rakefile for Puppet -*- ruby -*- RAKE_ROOT = File.dirname(__FILE__) # We need access to the Puppet.version method $LOAD_PATH.unshift(File.expand_path("lib")) require 'puppet/version' $LOAD_PATH << File.join(RAKE_ROOT, 'tasks') begin require 'rubygems' require 'rubygems/package_task' rescue LoadError # Users of older versions of Rake (0.8.7 for example) will not necessarily # have rubygems installed, or the newer rubygems package_task for that # matter. require 'rake/packagetask' require 'rake/gempackagetask' end require 'rake' Dir['tasks/**/*.rake'].each { |t| load t } begin load File.join(RAKE_ROOT, 'ext', 'packaging', 'packaging.rake') rescue LoadError end build_defs_file = 'ext/build_defaults.yaml' if File.exist?(build_defs_file) begin require 'yaml' @build_defaults ||= YAML.load_file(build_defs_file) rescue Exception => e STDERR.puts "Unable to load yaml from #{build_defs_file}:" STDERR.puts e end @packaging_url = @build_defaults['packaging_url'] @packaging_repo = @build_defaults['packaging_repo'] raise "Could not find packaging url in #{build_defs_file}" if @packaging_url.nil? raise "Could not find packaging repo in #{build_defs_file}" if @packaging_repo.nil? namespace :package do desc "Bootstrap packaging automation, e.g. clone into packaging repo" task :bootstrap do if File.exist?("ext/#{@packaging_repo}") puts "It looks like you already have ext/#{@packaging_repo}. If you don't like it, blow it away with package:implode." else cd 'ext' do %x{git clone #{@packaging_url}} end end end desc "Remove all cloned packaging automation" task :implode do rm_rf "ext/#{@packaging_repo}" end end end task :default do sh %{rake -T} end task :spec do sh %{rspec #{ENV['TEST'] || ENV['TESTS'] || 'spec'}} end + +desc 'run static analysis with rubocop' +task(:rubocop) do + if RUBY_VERSION !~ /1.8/ + require 'rubocop' + cli = RuboCop::CLI.new + cli.run(%w(-D -f s)) + else + puts "Rubocop is disabled in ruby 1.8" + end +end + diff --git a/ext/nagios/check_puppet.rb b/ext/nagios/check_puppet.rb index e614b3c77..36d8fa79c 100755 --- a/ext/nagios/check_puppet.rb +++ b/ext/nagios/check_puppet.rb @@ -1,123 +1,123 @@ #!/usr/bin/env ruby require 'optparse' require 'sys/proctable' include Sys class CheckPuppet VERSION = '0.1' script_name = File.basename($0) # default options OPTIONS = { :statefile => "/var/lib/puppet/state/state.yaml", :process => "puppetd", :interval => 30, } o = OptionParser.new do |o| o.set_summary_indent(' ') o.banner = "Usage: #{script_name} [OPTIONS]" o.define_head "The check_puppet Nagios plug-in checks that specified Puppet process is running and the state file is no older than specified interval." o.separator "" o.separator "Mandatory arguments to long options are mandatory for short options too." o.on( "-s", "--statefile=statefile", String, "The state file", - "Default: #{OPTIONS[:statefile]}") { |OPTIONS[:statefile]| } + "Default: #{OPTIONS[:statefile]}") { |op| OPTIONS[:statefile] = op } o.on( "-p", "--process=processname", String, "The process to check", - "Default: #{OPTIONS[:process]}") { |OPTIONS[:process]| } + "Default: #{OPTIONS[:process]}") { |op| OPTIONS[:process] = op } o.on( "-i", "--interval=value", Integer, - "Default: #{OPTIONS[:interval]} minutes") { |OPTIONS[:interval]| } + "Default: #{OPTIONS[:interval]} minutes") { |op| OPTIONS[:interval] = op } o.separator "" o.on_tail("-h", "--help", "Show this help message.") do puts o exit end o.parse!(ARGV) end def check_proc unless ProcTable.ps.find { |p| p.name == OPTIONS[:process]} @proc = 2 else @proc = 0 end end def check_state # Set variables curt = Time.now intv = OPTIONS[:interval] * 60 # Check file time begin @modt = File.mtime("#{OPTIONS[:statefile]}") rescue @file = 3 end diff = (curt - @modt).to_i if diff > intv @file = 2 else @file = 0 end end def output_status case @file when 0 state = "state file status okay updated on " + @modt.strftime("%m/%d/%Y at %H:%M:%S") when 2 state = "state fille is not up to date and is older than #{OPTIONS[:interval]} minutes" when 3 state = "state file status unknown" end case @proc when 0 process = "process #{OPTIONS[:process]} is running" when 2 process = "process #{OPTIONS[:process]} is not running" end case when (@proc == 2 or @file == 2) status = "CRITICAL" exitcode = 2 when (@proc == 0 and @file == 0) status = "OK" exitcode = 0 else status = "UNKNOWN" exitcode = 3 end puts "PUPPET #{status}: #{process}, #{state}" exit(exitcode) end end cp = CheckPuppet.new cp.check_proc cp.check_state cp.output_status diff --git a/ext/puppetlisten/puppetlisten.rb b/ext/puppetlisten/puppetlisten.rb index c7308753a..3f4a275d8 100755 --- a/ext/puppetlisten/puppetlisten.rb +++ b/ext/puppetlisten/puppetlisten.rb @@ -1,75 +1,77 @@ #! /usr/bin/env ruby # this is a daemon which accepts non standard (within puppet normal intervals) puppet configruation run request # uses SSL for communication based on the puppet infrastructure # ohadlevy@gmail.com port = 8139 cmd = "puppetd -o -v --no-daemonize" require 'puppet/sslcertificates/support' require 'socket' require 'facter' # load puppet configuration, needed to find SSL certificates Puppet.initialize_settings # set the SSL environment ctx = OpenSSL::SSL::SSLContext.new ctx.key = OpenSSL::PKey::RSA.new(File::read(Puppet[:hostprivkey])) ctx.cert = OpenSSL::X509::Certificate.new(File::read(Puppet[:hostcert])) ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT ctx.ca_file = Puppet[:localcacert] # find which hosts are allowed to trigger us allowed_servers = Array.new runner = false; File.open(Puppet[:authconfig]).each do |line| case line - when /^\s*#/: next # skip comments - when /^\s*$/: next # skip blank lines - when /\[puppetrunner\]/: # puppetrunner section + when /^\s*#/ + next # skip comments + when /^\s*$/ + next # skip blank lines + when /\[puppetrunner\]/ # puppetrunner section runner=true - when /^\s*(\w+)\s+(.+)$/: + when /^\s*(\w+)\s+(.+)$/ var = $1 value = $2 case var - when "allow": + when "allow" value.split(/\s*,\s*/).each { |val| allowed_servers << val puts "allowing #{val} access" } if runner==true end else runner=false end end # be a daemon sock = TCPServer.new(port) ssls = OpenSSL::SSL::SSLServer.new(sock, ctx) loop do begin ns = ssls.accept # start SSL session af, port, host, ip = ns.peeraddr print "connection from #{host+"("+ip+")"} " if allowed_servers.include?(host) #TODO add support for tags and other command line arguments puts "accepted" ns.puts "Executing #{cmd} on #{Facter.fqdn}.\n*******OUTPUT********\n\n" IO.popen(cmd) do |f| while line = f.gets ns.puts line end end ns.puts "\n*********DONE**********" else ns.puts "denied\n" puts "denied" end ns.close rescue ns.close next end end diff --git a/lib/puppet.rb b/lib/puppet.rb index 2a52e9887..cafc0ef77 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,277 +1,266 @@ require '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 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(*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) 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) 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' diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index a37cfe43c..cb9f3714a 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -1,2084 +1,2071 @@ module Puppet def self.default_diffargs if (Facter.value(:kernel) == "AIX" && Facter.value(:kernelmajversion) == "5300") "" else "-u" end end ############################################################################################ # NOTE: For information about the available values for the ":type" property of settings, # see the docs for Settings.define_settings ############################################################################################ AS_DURATION = %q{This setting can be a time interval in seconds (30 or 30s), minutes (30m), hours (6h), days (2d), or years (5y).} STORECONFIGS_ONLY = %q{This setting is only used by the ActiveRecord storeconfigs and inventory backends, which are deprecated.} # This is defined first so that the facter implementation is replaced before other setting defaults are evaluated. define_settings(:main, :cfacter => { :default => false, :type => :boolean, :desc => 'Whether or not to use the native facter (cfacter) implementation instead of the Ruby one (facter). Defaults to false.', :hook => proc do |value| return unless value raise ArgumentError, 'facter has already evaluated facts.' if Facter.instance_variable_get(:@collection) raise ArgumentError, 'cfacter version 0.2.0 or later is not installed.' unless Puppet.features.cfacter? CFacter.initialize end } ) define_settings(:main, :confdir => { :default => nil, :type => :directory, :desc => "The main Puppet configuration directory. The default for this setting is calculated based on the user. If the process is running as root or the user that Puppet is supposed to run as, it defaults to a system directory, but if it's running as any other user, it defaults to being in the user's home directory.", }, :vardir => { :default => nil, :type => :directory, :owner => "service", :group => "service", :desc => "Where Puppet stores dynamic and growing data. The default for this setting is calculated specially, like `confdir`_.", }, ### NOTE: this setting is usually being set to a symbol value. We don't officially have a ### setting type for that yet, but we might want to consider creating one. :name => { :default => nil, :desc => "The name of the application, if we are running as one. The default is essentially $0 without the path or `.rb`.", } ) define_settings(:main, :logdir => { :default => nil, :type => :directory, :mode => "0750", :owner => "service", :group => "service", :desc => "The directory in which to store log files", }, :log_level => { :default => 'notice', :type => :enum, :values => ["debug","info","notice","warning","err","alert","emerg","crit"], :desc => "Default logging level for messages from Puppet. Allowed values are: * debug * info * notice * warning * err * alert * emerg * crit ", }, :disable_warnings => { :default => [], :type => :array, :desc => "A comma-separated list of warning types to suppress. If large numbers of warnings are making Puppet's logs too large or difficult to use, you can temporarily silence them with this setting. If you are preparing to upgrade Puppet to a new major version, you should re-enable all warnings for a while. Valid values for this setting are: * `deprecations` --- disables deprecation warnings.", :hook => proc do |value| values = munge(value) valid = %w[deprecations] invalid = values - (values & valid) if not invalid.empty? raise ArgumentError, "Cannot disable unrecognized warning types #{invalid.inspect}. Valid values are #{valid.inspect}." end end } ) define_settings(:main, :priority => { :default => nil, :type => :priority, :desc => "The scheduling priority of the process. Valid values are 'high', 'normal', 'low', or 'idle', which are mapped to platform-specific values. The priority can also be specified as an integer value and will be passed as is, e.g. -5. Puppet must be running as a privileged user in order to increase scheduling priority.", }, :trace => { :default => false, :type => :boolean, :desc => "Whether to print stack traces on some errors", }, :profile => { :default => false, :type => :boolean, :desc => "Whether to enable experimental performance profiling", }, :autoflush => { :default => true, :type => :boolean, :desc => "Whether log files should always flush to disk.", :hook => proc { |value| Log.autoflush = value } }, :syslogfacility => { :default => "daemon", :desc => "What syslog facility to use when logging to syslog. Syslog has a fixed list of valid facilities, and you must choose one of those; you cannot just make one up." }, :statedir => { :default => "$vardir/state", :type => :directory, :mode => "01755", :desc => "The directory where Puppet state is stored. Generally, this directory can be removed without causing harm (although it might result in spurious service restarts)." }, :rundir => { :default => nil, :type => :directory, :mode => "0755", :owner => "service", :group => "service", :desc => "Where Puppet PID files are kept." }, :genconfig => { :default => false, :type => :boolean, :desc => "When true, causes Puppet applications to print an example config file to stdout and exit. The example will include descriptions of each setting, and the current (or default) value of each setting, incorporating any settings overridden on the CLI (with the exception of `genconfig` itself). This setting only makes sense when specified on the command line as `--genconfig`.", }, :genmanifest => { :default => false, :type => :boolean, :desc => "Whether to just print a manifest to stdout and exit. Only makes sense when specified on the command line as `--genmanifest`. Takes into account arguments specified on the CLI.", }, :configprint => { :default => "", :desc => "Print the value of a specific configuration setting. If the name of a setting is provided for this, then the value is printed and puppet exits. Comma-separate multiple values. For a list of all values, specify 'all'.", }, :color => { :default => "ansi", :type => :string, :desc => "Whether to use colors when logging to the console. Valid values are `ansi` (equivalent to `true`), `html`, and `false`, which produces no color. Defaults to false on Windows, as its console does not support ansi colors.", }, :mkusers => { :default => false, :type => :boolean, :desc => "Whether to create the necessary user and group that puppet agent will run as.", }, :manage_internal_file_permissions => { :default => true, :type => :boolean, :desc => "Whether Puppet should manage the owner, group, and mode of files it uses internally", }, :onetime => { :default => false, :type => :boolean, :desc => "Perform one configuration run and exit, rather than spawning a long-running daemon. This is useful for interactively running puppet agent, or running puppet agent from cron.", :short => 'o', }, :path => { :default => "none", :desc => "The shell search path. Defaults to whatever is inherited from the parent process.", :call_hook => :on_define_and_write, :hook => proc do |value| ENV["PATH"] = "" if ENV["PATH"].nil? ENV["PATH"] = value unless value == "none" paths = ENV["PATH"].split(File::PATH_SEPARATOR) Puppet::Util::Platform.default_paths.each do |path| ENV["PATH"] += File::PATH_SEPARATOR + path unless paths.include?(path) end value end }, :libdir => { :type => :directory, :default => "$vardir/lib", :desc => "An extra search path for Puppet. This is only useful for those files that Puppet will load on demand, and is only guaranteed to work for those cases. In fact, the autoload mechanism is responsible for making sure this directory is in Ruby's search path\n", :call_hook => :on_initialize_and_write, :hook => proc do |value| $LOAD_PATH.delete(@oldlibdir) if defined?(@oldlibdir) and $LOAD_PATH.include?(@oldlibdir) @oldlibdir = value $LOAD_PATH << value end }, :ignoreimport => { :default => false, :type => :boolean, :desc => "If true, allows the parser to continue without requiring all files referenced with `import` statements to exist. This setting was primarily designed for use with commit hooks for parse-checking.", }, :environment => { :default => "production", :desc => "The environment Puppet is running in. For clients (e.g., `puppet agent`) this determines the environment itself, which is used to find modules and much more. For servers (i.e., `puppet master`) this provides the default environment for nodes we know nothing about." }, :environmentpath => { :default => "", :desc => "A search path for directory environments, as a list of directories separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.) This setting must have a value set to enable **directory environments.** The recommended value is `$confdir/environments`. For more details, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", :type => :path, }, :always_cache_features => { :type => :boolean, :default => false, :desc => <<-'EOT' Affects how we cache attempts to load Puppet 'features'. If false, then calls to `Puppet.features.?` will always attempt to load the feature (which can be an expensive operation) unless it has already been loaded successfully. This makes it possible for a single agent run to, e.g., install a package that provides the underlying capabilities for a feature, and then later load that feature during the same run (even if the feature had been tested earlier and had not been available). If this setting is set to true, then features will only be checked once, and if they are not available, the negative result is cached and returned for all subsequent attempts to load the feature. This behavior is almost always appropriate for the server, and can result in a significant performance improvement for features that are checked frequently. EOT }, :diff_args => { :default => lambda { default_diffargs }, :desc => "Which arguments to pass to the diff command when printing differences between files. The command to use can be chosen with the `diff` setting.", }, :diff => { :default => (Puppet.features.microsoft_windows? ? "" : "diff"), :desc => "Which diff command to use when printing differences between files. This setting has no default value on Windows, as standard `diff` is not available, but Puppet can use many third-party diff tools.", }, :show_diff => { :type => :boolean, :default => false, :desc => "Whether to log and report a contextual diff when files are being replaced. This causes partial file contents to pass through Puppet's normal logging and reporting system, so this setting should be used with caution if you are sending Puppet's reports to an insecure destination. This feature currently requires the `diff/lcs` Ruby library.", }, :daemonize => { :type => :boolean, :default => (Puppet.features.microsoft_windows? ? false : true), :desc => "Whether to send the process into the background. This defaults to true on POSIX systems, and to false on Windows (where Puppet currently cannot daemonize).", :short => "D", :hook => proc do |value| if value and Puppet.features.microsoft_windows? raise "Cannot daemonize on Windows" end end }, :maximum_uid => { :default => 4294967290, :desc => "The maximum allowed UID. Some platforms use negative UIDs but then ship with tools that do not know how to handle signed ints, so the UIDs show up as huge numbers that can then not be fed back into the system. This is a hackish way to fail in a slightly more useful way when that happens.", }, :route_file => { :default => "$confdir/routes.yaml", :desc => "The YAML file containing indirector route configuration.", }, :node_terminus => { :type => :terminus, :default => "plain", :desc => "Where to find information about nodes.", }, :node_cache_terminus => { :type => :terminus, :default => nil, :desc => "How to store cached nodes. Valid values are (none), 'json', 'msgpack', 'yaml' or write only yaml ('write_only_yaml'). The master application defaults to 'write_only_yaml', all others to none.", }, :data_binding_terminus => { :type => :terminus, :default => "hiera", :desc => "Where to retrive information about data.", }, :hiera_config => { :default => "$confdir/hiera.yaml", :desc => "The hiera configuration file. Puppet only reads this file on startup, so you must restart the puppet master every time you edit it.", :type => :file, }, :binder => { :default => false, :desc => "Turns the binding system on or off. This includes bindings in modules. The binding system aggregates data from modules and other locations and makes them available for lookup. The binding system is experimental and any or all of it may change.", :type => :boolean, }, :binder_config => { :default => nil, :desc => "The binder configuration file. Puppet reads this file on each request to configure the bindings system. If set to nil (the default), a $confdir/binder_config.yaml is optionally loaded. If it does not exists, a default configuration is used. If the setting :binding_config is specified, it must reference a valid and existing yaml file.", :type => :file, }, :catalog_terminus => { :type => :terminus, :default => "compiler", :desc => "Where to get node catalogs. This is useful to change if, for instance, you'd like to pre-compile catalogs and store them in memcached or some other easily-accessed store.", }, :catalog_cache_terminus => { :type => :terminus, :default => nil, :desc => "How to store cached catalogs. Valid values are 'json', 'msgpack' and 'yaml'. The agent application defaults to 'json'." }, :facts_terminus => { :default => 'facter', :desc => "The node facts terminus.", :call_hook => :on_initialize_and_write, :hook => proc do |value| require 'puppet/node/facts' # Cache to YAML if we're uploading facts away if %w[rest inventory_service].include? value.to_s Puppet.info "configuring the YAML fact cache because a remote terminus is active" Puppet::Node::Facts.indirection.cache_class = :yaml end end }, :inventory_terminus => { :type => :terminus, :default => "$facts_terminus", :desc => "Should usually be the same as the facts terminus", }, :default_file_terminus => { :type => :terminus, :default => "rest", :desc => "The default source for files if no server is given in a uri, e.g. puppet:///file. The default of `rest` causes the file to be retrieved using the `server` setting. When running `apply` the default is `file_server`, causing requests to be filled locally." }, :httplog => { :default => "$logdir/http.log", :type => :file, :owner => "root", :mode => "0640", :desc => "Where the puppet agent web server logs.", }, :http_proxy_host => { :default => "none", :desc => "The HTTP proxy host to use for outgoing connections. Note: You may need to use a FQDN for the server hostname when using a proxy. Environment variable http_proxy or HTTP_PROXY will override this value", }, :http_proxy_port => { :default => 3128, :desc => "The HTTP proxy port to use for outgoing connections", }, :http_proxy_user => { :default => "none", :desc => "The user name for an authenticated HTTP proxy. Requires the `http_proxy_host` setting.", }, :http_proxy_password =>{ :default => "none", :hook => proc do |value| if Puppet.settings[:http_proxy_password] =~ /[@!# \/]/ raise "Passwords set in the http_proxy_password setting must be valid as part of a URL, and any reserved characters must be URL-encoded. We received: #{value}" end end, :desc => "The password for the user of an authenticated HTTP proxy. Requires the `http_proxy_user` setting. Note that passwords must be valid when used as part of a URL. If a password contains any characters with special meanings in URLs (as specified by RFC 3986 section 2.2), they must be URL-encoded. (For example, `#` would become `%23`.)", }, :http_keepalive_timeout => { :default => "4s", :type => :duration, :desc => "The maximum amount of time a persistent HTTP connection can remain idle in the connection pool, before it is closed. This timeout should be shorter than the keepalive timeout used on the HTTP server, e.g. Apache KeepAliveTimeout directive. #{AS_DURATION}" }, :http_debug => { :default => false, :type => :boolean, :desc => "Whether to write HTTP request and responses to stderr. This should never be used in a production environment." }, :filetimeout => { :default => "15s", :type => :duration, :desc => "The minimum time to wait between checking for updates in configuration files. This timeout determines how quickly Puppet checks whether a file (such as manifests or templates) has changed on disk. #{AS_DURATION}", }, :environment_timeout => { - :default => "3m", + :default => "unlimited", :type => :ttl, :desc => "The time to live for a cached environment. #{AS_DURATION} This setting can also be set to `unlimited`, which causes the environment to be cached until the master is restarted." }, :queue_type => { :default => "stomp", :desc => "Which type of queue to use for asynchronous processing.", }, :queue_type => { :default => "stomp", :desc => "Which type of queue to use for asynchronous processing.", }, :queue_source => { :default => "stomp://localhost:61613/", :desc => "Which type of queue to use for asynchronous processing. If your stomp server requires authentication, you can include it in the URI as long as your stomp client library is at least 1.1.1", }, :async_storeconfigs => { :default => false, :type => :boolean, :desc => "Whether to use a queueing system to provide asynchronous database integration. Requires that `puppet queue` be running.", :hook => proc do |value| if value # This reconfigures the termini for Node, Facts, and Catalog Puppet.settings.override_default(:storeconfigs, true) # But then we modify the configuration Puppet::Resource::Catalog.indirection.cache_class = :queue Puppet.settings.override_default(:catalog_cache_terminus, :queue) else raise "Cannot disable asynchronous storeconfigs in a running process" end end }, :thin_storeconfigs => { :default => false, :type => :boolean, :desc => "Boolean; whether Puppet should store only facts and exported resources in the storeconfigs database. This will improve the performance of exported resources with the older `active_record` backend, but will disable external tools that search the storeconfigs database. Thinning catalogs is generally unnecessary when using PuppetDB to store catalogs.", :hook => proc do |value| Puppet.settings.override_default(:storeconfigs, true) if value end }, :config_version => { :default => "", :desc => "How to determine the configuration version. By default, it will be the time that the configuration is parsed, but you can provide a shell script to override how the version is determined. The output of this script will be added to every log message in the reports, allowing you to correlate changes on your hosts to the source version on the server. Setting a global value for config_version in puppet.conf is deprecated. Please set a per-environment value in environment.conf instead. For more info, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", :deprecated => :allowed_on_commandline, }, :zlib => { :default => true, :type => :boolean, :desc => "Boolean; whether to use the zlib library", }, :prerun_command => { :default => "", :desc => "A command to run before every agent run. If this command returns a non-zero return code, the entire Puppet run will fail.", }, :postrun_command => { :default => "", :desc => "A command to run after every agent run. If this command returns a non-zero return code, the entire Puppet run will be considered to have failed, even though it might have performed work during the normal run.", }, :freeze_main => { :default => false, :type => :boolean, :desc => "Freezes the 'main' class, disallowing any code to be added to it. This essentially means that you can't have any code outside of a node, class, or definition other than in the site manifest.", }, :stringify_facts => { :default => true, :type => :boolean, :desc => "Flatten fact values to strings using #to_s. Means you can't have arrays or hashes as fact values. (DEPRECATED) This option will be removed in Puppet 4.0.", }, :trusted_node_data => { :default => false, :type => :boolean, :desc => "Stores trusted node data in a hash called $trusted. When true also prevents $trusted from being overridden in any scope.", }, :immutable_node_data => { :default => '$trusted_node_data', :type => :boolean, :desc => "When true, also prevents $trusted and $facts from being overridden in any scope", } ) Puppet.define_settings(:module_tool, :module_repository => { :default => 'https://forgeapi.puppetlabs.com', :desc => "The module repository", }, :module_working_dir => { :default => '$vardir/puppet-module', :desc => "The directory into which module tool data is stored", }, :module_skeleton_dir => { :default => '$module_working_dir/skeleton', :desc => "The directory which the skeleton for module tool generate is stored.", }, :forge_authorization => { :default => nil, :desc => "The authorization key to connect to the Puppet Forge. Leave blank for unauthorized or license based connections", }, :module_groups => { :default => nil, :desc => "Extra module groups to request from the Puppet Forge", } ) Puppet.define_settings( :main, # We have to downcase the fqdn, because the current ssl stuff (as oppsed to in master) doesn't have good facilities for # manipulating naming. :certname => { :default => lambda { Puppet::Settings.default_certname.downcase }, :desc => "The name to use when handling certificates. When a node requests a certificate from the CA puppet master, it uses the value of the `certname` setting as its requested Subject CN. This is the name used when managing a node's permissions in [auth.conf](http://docs.puppetlabs.com/puppet/latest/reference/config_file_auth.html). In most cases, it is also used as the node's name when matching [node definitions](http://docs.puppetlabs.com/puppet/latest/reference/lang_node_definitions.html) and requesting data from an ENC. (This can be changed with the `node_name_value` and `node_name_fact` settings, although you should only do so if you have a compelling reason.) A node's certname is available in Puppet manifests as `$trusted['certname']`. (See [Facts and Built-In Variables](http://docs.puppetlabs.com/puppet/latest/reference/lang_facts_and_builtin_vars.html) for more details.) * For best compatibility, you should limit the value of `certname` to only use letters, numbers, periods, underscores, and dashes. (That is, it should match `/\A[a-z0-9._-]+\Z/`.) * The special value `ca` is reserved, and can't be used as the certname for a normal node. Defaults to the node's fully qualified domain name.", :hook => proc { |value| raise(ArgumentError, "Certificate names must be lower case; see #1168") unless value == value.downcase }}, :certdnsnames => { :default => '', :hook => proc do |value| unless value.nil? or value == '' then Puppet.warning < < { :default => '', :desc => < { :default => "$confdir/csr_attributes.yaml", :type => :file, :desc => < { :default => "$ssldir/certs", :type => :directory, :mode => "0755", :owner => "service", :group => "service", :desc => "The certificate directory." }, :ssldir => { :default => "$confdir/ssl", :type => :directory, :mode => "0771", :owner => "service", :group => "service", :desc => "Where SSL certificates are kept." }, :publickeydir => { :default => "$ssldir/public_keys", :type => :directory, :mode => "0755", :owner => "service", :group => "service", :desc => "The public key directory." }, :requestdir => { :default => "$ssldir/certificate_requests", :type => :directory, :mode => "0755", :owner => "service", :group => "service", :desc => "Where host certificate requests are stored." }, :privatekeydir => { :default => "$ssldir/private_keys", :type => :directory, :mode => "0750", :owner => "service", :group => "service", :desc => "The private key directory." }, :privatedir => { :default => "$ssldir/private", :type => :directory, :mode => "0750", :owner => "service", :group => "service", :desc => "Where the client stores private certificate information." }, :passfile => { :default => "$privatedir/password", :type => :file, :mode => "0640", :owner => "service", :group => "service", :desc => "Where puppet agent stores the password for its private key. Generally unused." }, :hostcsr => { :default => "$ssldir/csr_$certname.pem", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their certificate requests." }, :hostcert => { :default => "$certdir/$certname.pem", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their certificates." }, :hostprivkey => { :default => "$privatekeydir/$certname.pem", :type => :file, :mode => "0640", :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their private key." }, :hostpubkey => { :default => "$publickeydir/$certname.pem", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their public key." }, :localcacert => { :default => "$certdir/ca.pem", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Where each client stores the CA certificate." }, :ssl_client_ca_auth => { :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Certificate authorities who issue server certificates. SSL servers will not be considered authentic unless they possess a certificate issued by an authority listed in this file. If this setting has no value then the Puppet master's CA certificate (localcacert) will be used." }, :ssl_server_ca_auth => { :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Certificate authorities who issue client certificates. SSL clients will not be considered authentic unless they possess a certificate issued by an authority listed in this file. If this setting has no value then the Puppet master's CA certificate (localcacert) will be used." }, :hostcrl => { :default => "$ssldir/crl.pem", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Where the host's certificate revocation list can be found. This is distinct from the certificate authority's CRL." }, :certificate_revocation => { :default => true, :type => :boolean, :desc => "Whether certificate revocation should be supported by downloading a Certificate Revocation List (CRL) to all clients. If enabled, CA chaining will almost definitely not work.", }, :certificate_expire_warning => { :default => "60d", :type => :duration, :desc => "The window of time leading up to a certificate's expiration that a notification will be logged. This applies to CA, master, and agent certificates. #{AS_DURATION}" }, :digest_algorithm => { :default => 'md5', :type => :enum, :values => ["md5", "sha256"], :desc => 'Which digest algorithm to use for file resources and the filebucket. Valid values are md5, sha256. Default is md5.', } ) define_settings( :ca, :ca_name => { :default => "Puppet CA: $certname", :desc => "The name to use the Certificate Authority certificate.", }, :cadir => { :default => "$ssldir/ca", :type => :directory, :owner => "service", :group => "service", :mode => "0755", :desc => "The root directory for the certificate authority." }, :cacert => { :default => "$cadir/ca_crt.pem", :type => :file, :owner => "service", :group => "service", :mode => "0644", :desc => "The CA certificate." }, :cakey => { :default => "$cadir/ca_key.pem", :type => :file, :owner => "service", :group => "service", :mode => "0640", :desc => "The CA private key." }, :capub => { :default => "$cadir/ca_pub.pem", :type => :file, :owner => "service", :group => "service", :mode => "0644", :desc => "The CA public key." }, :cacrl => { :default => "$cadir/ca_crl.pem", :type => :file, :owner => "service", :group => "service", :mode => "0644", :desc => "The certificate revocation list (CRL) for the CA. Will be used if present but otherwise ignored.", }, :caprivatedir => { :default => "$cadir/private", :type => :directory, :owner => "service", :group => "service", :mode => "0750", :desc => "Where the CA stores private certificate information." }, :csrdir => { :default => "$cadir/requests", :type => :directory, :owner => "service", :group => "service", :mode => "0755", :desc => "Where the CA stores certificate requests" }, :signeddir => { :default => "$cadir/signed", :type => :directory, :owner => "service", :group => "service", :mode => "0755", :desc => "Where the CA stores signed certificates." }, :capass => { :default => "$caprivatedir/ca.pass", :type => :file, :owner => "service", :group => "service", :mode => "0640", :desc => "Where the CA stores the password for the private key." }, :serial => { :default => "$cadir/serial", :type => :file, :owner => "service", :group => "service", :mode => "0644", :desc => "Where the serial number for certificates is stored." }, :autosign => { :default => "$confdir/autosign.conf", :type => :autosign, :desc => "Whether (and how) to autosign certificate requests. This setting is only relevant on a puppet master acting as a certificate authority (CA). Valid values are true (autosigns all certificate requests; not recommended), false (disables autosigning certificates), or the absolute path to a file. The file specified in this setting may be either a **configuration file** or a **custom policy executable.** Puppet will automatically determine what it is: If the Puppet user (see the `user` setting) can execute the file, it will be treated as a policy executable; otherwise, it will be treated as a config file. If a custom policy executable is configured, the CA puppet master will run it every time it receives a CSR. The executable will be passed the subject CN of the request _as a command line argument,_ and the contents of the CSR in PEM format _on stdin._ It should exit with a status of 0 if the cert should be autosigned and non-zero if the cert should not be autosigned. If a certificate request is not autosigned, it will persist for review. An admin user can use the `puppet cert sign` command to manually sign it, or can delete the request. For info on autosign configuration files, see [the guide to Puppet's config files](http://docs.puppetlabs.com/guides/configuring.html).", }, :allow_duplicate_certs => { :default => false, :type => :boolean, :desc => "Whether to allow a new certificate request to overwrite an existing certificate.", }, :ca_ttl => { :default => "5y", :type => :duration, :desc => "The default TTL for new certificates. #{AS_DURATION}" }, :req_bits => { :default => 4096, :desc => "The bit length of the certificates.", }, :keylength => { :default => 4096, :desc => "The bit length of keys.", }, :cert_inventory => { :default => "$cadir/inventory.txt", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "The inventory file. This is a text file to which the CA writes a complete listing of all certificates." } ) # Define the config default. define_settings(:application, :config_file_name => { :type => :string, :default => Puppet::Settings.default_config_file_name, :desc => "The name of the puppet config file.", }, :config => { :type => :file, :default => "$confdir/${config_file_name}", :desc => "The configuration file for the current puppet application.", }, :pidfile => { :type => :file, :default => "$rundir/${run_mode}.pid", :desc => "The file containing the PID of a running process. This file is intended to be used by service management frameworks and monitoring systems to determine if a puppet process is still in the process table.", }, :bindaddress => { :default => "0.0.0.0", :desc => "The address a listening server should bind to.", } ) define_settings(:master, :user => { :default => "puppet", :desc => "The user puppet master should run as.", }, :group => { :default => "puppet", :desc => "The group puppet master should run as.", }, :manifestdir => { :default => "$confdir/manifests", :type => :directory, :desc => "Used to build the default value of the `manifest` setting. Has no other purpose. This setting is deprecated.", :deprecated => :completely, }, :manifest => { :default => "$manifestdir/site.pp", :type => :file_or_directory, :desc => "The entry-point manifest for puppet master. This can be one file or a directory of manifests to be evaluated in alphabetical order. Puppet manages this path as a directory if one exists or if the path ends with a / or \\. Setting a global value for `manifest` in puppet.conf is deprecated. Please use directory environments instead. If you need to use something other than the environment's `manifests` directory as the main manifest, you can set `manifest` in environment.conf. For more info, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", :deprecated => :allowed_on_commandline, }, :default_manifest => { :default => "./manifests", :type => :string, :desc => "The default main manifest for directory environments. Any environment that doesn't set the `manifest` setting in its `environment.conf` file will use this manifest. This setting's value can be an absolute or relative path. An absolute path will make all environments default to the same main manifest; a relative path will allow each environment to use its own manifest, and Puppet will resolve the path relative to each environment's main directory. In either case, the path can point to a single file or to a directory of manifests to be evaluated in alphabetical order.", }, :disable_per_environment_manifest => { :default => false, :type => :boolean, :desc => "Whether to disallow an environment-specific main manifest. When set to `true`, Puppet will use the manifest specified in the `default_manifest` setting for all environments. If an environment specifies a different main manifest in its `environment.conf` file, catalog requests for that environment will fail with an error. This setting requires `default_manifest` to be set to an absolute path.", :hook => proc do |value| if value && !Pathname.new(Puppet[:default_manifest]).absolute? raise(Puppet::Settings::ValidationError, "The 'default_manifest' setting must be set to an absolute path when 'disable_per_environment_manifest' is true") end end, }, :code => { :default => "", :desc => "Code to parse directly. This is essentially only used by `puppet`, and should only be set if you're writing your own Puppet executable.", }, - :masterlog => { - :default => "$logdir/puppetmaster.log", - :type => :file, - :owner => "service", - :group => "service", - :mode => "0660", - :desc => "This file is literally never used, although Puppet may create it - as an empty file. For more context, see the `puppetdlog` setting and - puppet master's `--logdest` command line option. - - This setting is deprecated and will be removed in a future version of Puppet.", - :deprecated => :completely - }, :masterhttplog => { :default => "$logdir/masterhttp.log", :type => :file, :owner => "service", :group => "service", :mode => "0660", :create => true, :desc => "Where the puppet master web server saves its access log. This is only used when running a WEBrick puppet master. When puppet master is running under a Rack server like Passenger, that web server will have its own logging behavior." }, :masterport => { :default => 8140, :desc => "The port for puppet master traffic. For puppet master, this is the port to listen on; for puppet agent, this is the port to make requests on. Both applications use this setting to get the port.", }, :node_name => { :default => "cert", :desc => "How the puppet master determines the client's identity and sets the 'hostname', 'fqdn' and 'domain' facts for use in the manifest, in particular for determining which 'node' statement applies to the client. Possible values are 'cert' (use the subject's CN in the client's certificate) and 'facter' (use the hostname that the client reported in its facts)", }, :bucketdir => { :default => "$vardir/bucket", :type => :directory, :mode => "0750", :owner => "service", :group => "service", :desc => "Where FileBucket files are stored." }, :rest_authconfig => { :default => "$confdir/auth.conf", :type => :file, :desc => "The configuration file that defines the rights to the different rest indirections. This can be used as a fine-grained authorization system for `puppet master`.", }, :ca => { :default => true, :type => :boolean, :desc => "Whether the master should function as a certificate authority.", }, :basemodulepath => { :default => "$confdir/modules#{File::PATH_SEPARATOR}/usr/share/puppet/modules", :type => :path, :desc => "The search path for **global** modules. Should be specified as a list of directories separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.) If you are using directory environments, these are the modules that will be used by _all_ environments. Note that the `modules` directory of the active environment will have priority over any global directories. For more info, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html This setting also provides the default value for the deprecated `modulepath` setting, which is used when directory environments are disabled.", }, :modulepath => { :default => "$basemodulepath", :type => :path, :desc => "The search path for modules, as a list of directories separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.) Setting a global value for `modulepath` in puppet.conf is deprecated. Please use directory environments instead. If you need to use something other than the default modulepath of `:$basemodulepath`, you can set `modulepath` in environment.conf. For more info, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", :deprecated => :allowed_on_commandline, }, :ssl_client_header => { :default => "HTTP_X_CLIENT_DN", :desc => "The header containing an authenticated client's SSL DN. This header must be set by the proxy to the authenticated client's SSL DN (e.g., `/CN=puppet.puppetlabs.com`). Puppet will parse out the Common Name (CN) from the Distinguished Name (DN) and use the value of the CN field for authorization. Note that the name of the HTTP header gets munged by the web server common gateway inteface: an `HTTP_` prefix is added, dashes are converted to underscores, and all letters are uppercased. Thus, to use the `X-Client-DN` header, this setting should be `HTTP_X_CLIENT_DN`.", }, :ssl_client_verify_header => { :default => "HTTP_X_CLIENT_VERIFY", :desc => "The header containing the status message of the client verification. This header must be set by the proxy to 'SUCCESS' if the client successfully authenticated, and anything else otherwise. Note that the name of the HTTP header gets munged by the web server common gateway inteface: an `HTTP_` prefix is added, dashes are converted to underscores, and all letters are uppercased. Thus, to use the `X-Client-Verify` header, this setting should be `HTTP_X_CLIENT_VERIFY`.", }, # To make sure this directory is created before we try to use it on the server, we need # it to be in the server section (#1138). :yamldir => { :default => "$vardir/yaml", :type => :directory, :owner => "service", :group => "service", :mode => "0750", :desc => "The directory in which YAML data is stored, usually in a subdirectory."}, :server_datadir => { :default => "$vardir/server_data", :type => :directory, :owner => "service", :group => "service", :mode => "0750", :desc => "The directory in which serialized data is stored, usually in a subdirectory."}, :reports => { :default => "store", :desc => "The list of report handlers to use. When using multiple report handlers, their names should be comma-separated, with whitespace allowed. (For example, `reports = http, tagmail`.) This setting is relevant to puppet master and puppet apply. The puppet master will call these report handlers with the reports it receives from agent nodes, and puppet apply will call them with its own report. (In all cases, the node applying the catalog must have `report = true`.) See the report reference for information on the built-in report handlers; custom report handlers can also be loaded from modules. (Report handlers are loaded from the lib directory, at `puppet/reports/NAME.rb`.)", }, :reportdir => { :default => "$vardir/reports", :type => :directory, :mode => "0750", :owner => "service", :group => "service", :desc => "The directory in which to store reports. Each node gets a separate subdirectory in this directory. This setting is only used when the `store` report processor is enabled (see the `reports` setting)."}, :reporturl => { :default => "http://localhost:3000/reports/upload", :desc => "The URL that reports should be forwarded to. This setting is only used when the `http` report processor is enabled (see the `reports` setting).", }, :fileserverconfig => { :default => "$confdir/fileserver.conf", :type => :file, :desc => "Where the fileserver configuration is stored.", }, :strict_hostname_checking => { :default => false, :desc => "Whether to only search for the complete hostname as it is in the certificate when searching for node information in the catalogs.", } ) define_settings(:metrics, :rrddir => { :type => :directory, :default => "$vardir/rrd", :mode => "0750", :owner => "service", :group => "service", :desc => "The directory where RRD database files are stored. Directories for each reporting host will be created under this directory." }, :rrdinterval => { :default => "$runinterval", :type => :duration, :desc => "How often RRD should expect data. This should match how often the hosts report back to the server. #{AS_DURATION}", } ) define_settings(:device, :devicedir => { :default => "$vardir/devices", :type => :directory, :mode => "0750", :desc => "The root directory of devices' $vardir.", }, :deviceconfig => { :default => "$confdir/device.conf", :desc => "Path to the device config file for puppet device.", } ) define_settings(:agent, :node_name_value => { :default => "$certname", :desc => "The explicit value used for the node name for all requests the agent makes to the master. WARNING: This setting is mutually exclusive with node_name_fact. Changing this setting also requires changes to the default auth.conf configuration on the Puppet Master. Please see http://links.puppetlabs.com/node_name_value for more information." }, :node_name_fact => { :default => "", :desc => "The fact name used to determine the node name used for all requests the agent makes to the master. WARNING: This setting is mutually exclusive with node_name_value. Changing this setting also requires changes to the default auth.conf configuration on the Puppet Master. Please see http://links.puppetlabs.com/node_name_fact for more information.", :hook => proc do |value| if !value.empty? and Puppet[:node_name_value] != Puppet[:certname] raise "Cannot specify both the node_name_value and node_name_fact settings" end end }, :localconfig => { :default => "$statedir/localconfig", :type => :file, :owner => "root", :mode => "0660", :desc => "Where puppet agent caches the local configuration. An extension indicating the cache format is added automatically."}, :statefile => { :default => "$statedir/state.yaml", :type => :file, :mode => "0660", :desc => "Where puppet agent and puppet master store state associated with the running configuration. In the case of puppet master, this file reflects the state discovered through interacting with clients." }, :clientyamldir => { :default => "$vardir/client_yaml", :type => :directory, :mode => "0750", :desc => "The directory in which client-side YAML data is stored." }, :client_datadir => { :default => "$vardir/client_data", :type => :directory, :mode => "0750", :desc => "The directory in which serialized data is stored on the client." }, :classfile => { :default => "$statedir/classes.txt", :type => :file, :owner => "root", :mode => "0640", :desc => "The file in which puppet agent stores a list of the classes associated with the retrieved configuration. Can be loaded in the separate `puppet` executable using the `--loadclasses` option."}, :resourcefile => { :default => "$statedir/resources.txt", :type => :file, :owner => "root", :mode => "0640", :desc => "The file in which puppet agent stores a list of the resources associated with the retrieved configuration." }, :puppetdlog => { :default => "$logdir/puppetd.log", :type => :file, :owner => "root", :mode => "0640", :desc => "The fallback log file. This is only used when the `--logdest` option is not specified AND Puppet is running on an operating system where both the POSIX syslog service and the Windows Event Log are unavailable. (Currently, no supported operating systems match that description.) Despite the name, both puppet agent and puppet master will use this file as the fallback logging destination. For control over logging destinations, see the `--logdest` command line option in the manual pages for puppet master, puppet agent, and puppet apply. You can see man pages by running `puppet --help`, or read them online at http://docs.puppetlabs.com/references/latest/man/." }, :server => { :default => "puppet", :desc => "The puppet master server to which the puppet agent should connect." }, :use_srv_records => { :default => false, :type => :boolean, :desc => "Whether the server will search for SRV records in DNS for the current domain.", }, :srv_domain => { :default => lambda { Puppet::Settings.domain_fact }, :desc => "The domain which will be queried to find the SRV records of servers to use.", }, :ignoreschedules => { :default => false, :type => :boolean, :desc => "Boolean; whether puppet agent should ignore schedules. This is useful for initial puppet agent runs.", }, :default_schedules => { :default => true, :type => :boolean, :desc => "Boolean; whether to generate the default schedule resources. Setting this to false is useful for keeping external report processors clean of skipped schedule resources.", }, :puppetport => { :default => 8139, :desc => "Which port puppet agent listens on.", }, :noop => { :default => false, :type => :boolean, :desc => "Whether to apply catalogs in noop mode, which allows Puppet to partially simulate a normal run. This setting affects puppet agent and puppet apply. When running in noop mode, Puppet will check whether each resource 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` metaparameter](http://docs.puppetlabs.com/references/latest/metaparameter.html#noop) allows you to apply individual resources in noop mode, and will override the global value of the `noop` setting. This means a resource with `noop => false` _will_ be changed if necessary, even when running puppet agent with `noop = true` or `--noop`. (Conversely, a resource with `noop => true` will only be simulated, even when noop mode is globally disabled.)", }, :runinterval => { :default => "30m", :type => :duration, :desc => "How often puppet agent applies the catalog. Note that a runinterval of 0 means \"run continuously\" rather than \"never run.\" If you want puppet agent to never run, you should start it with the `--no-client` option. #{AS_DURATION}", }, :listen => { :default => false, :type => :boolean, :desc => "Whether puppet agent should listen for connections. If this is true, then puppet agent will accept incoming REST API requests, subject to the default ACLs and the ACLs set in the `rest_authconfig` file. Puppet agent can respond usefully to requests on the `run`, `facts`, `certificate`, and `resource` endpoints.", }, :ca_server => { :default => "$server", :desc => "The server to use for certificate authority requests. It's a separate server because it cannot and does not need to horizontally scale.", }, :ca_port => { :default => "$masterport", :desc => "The port to use for the certificate authority.", }, :catalog_format => { :default => "", :desc => "(Deprecated for 'preferred_serialization_format') What format to use to dump the catalog. Only supports 'marshal' and 'yaml'. Only matters on the client, since it asks the server for a specific format.", :hook => proc { |value| if value Puppet.deprecation_warning "Setting 'catalog_format' is deprecated; use 'preferred_serialization_format' instead." Puppet.settings.override_default(:preferred_serialization_format, value) end } }, :preferred_serialization_format => { :default => "pson", :desc => "The preferred means of serializing ruby instances for passing over the wire. This won't guarantee that all instances will be serialized using this method, since not all classes can be guaranteed to support this format, but it will be used for all classes that support it.", }, :report_serialization_format => { :default => "pson", :type => :enum, :values => ["pson", "yaml"], :desc => "The serialization format to use when sending reports to the `report_server`. Possible values are `pson` and `yaml`. This setting affects puppet agent, but not puppet apply (which processes its own reports). This should almost always be set to `pson`. It can be temporarily set to `yaml` to let agents using this Puppet version connect to a puppet master running Puppet 3.0.0 through 3.2.x. Note that this is set to 'yaml' automatically if the agent detects an older master, so should never need to be set explicitly." }, :legacy_query_parameter_serialization => { :default => false, :type => :boolean, :desc => "The serialization format to use when sending file_metadata query parameters. Older versions of puppet master expect certain query parameters to be serialized as yaml, which is deprecated. This should almost always be false. It can be temporarily set to true to let agents using this Puppet version connect to a puppet master running Puppet 3.0.0 through 3.2.x. Note that this is set to true automatically if the agent detects an older master, so should never need to be set explicitly." }, :agent_catalog_run_lockfile => { :default => "$statedir/agent_catalog_run.lock", :type => :string, # (#2888) Ensure this file is not added to the settings catalog. :desc => "A lock file to indicate that a puppet agent catalog run is currently in progress. The file contains the pid of the process that holds the lock on the catalog run.", }, :agent_disabled_lockfile => { :default => "$statedir/agent_disabled.lock", :type => :file, :desc => "A lock file to indicate that puppet agent runs have been administratively disabled. File contains a JSON object with state information.", }, :usecacheonfailure => { :default => true, :type => :boolean, :desc => "Whether to use the cached configuration when the remote configuration will not compile. This option is useful for testing new configurations, where you want to fix the broken configuration rather than reverting to a known-good one.", }, :use_cached_catalog => { :default => false, :type => :boolean, :desc => "Whether to only use the cached catalog rather than compiling a new catalog on every run. Puppet can be run with this enabled by default and then selectively disabled when a recompile is desired.", }, :ignoremissingtypes => { :default => false, :type => :boolean, :desc => "Skip searching for classes and definitions that were missing during a prior compilation. The list of missing objects is maintained per-environment and persists until the environment is cleared or the master is restarted.", }, :ignorecache => { :default => false, :type => :boolean, :desc => "Ignore cache and always recompile the configuration. This is useful for testing new configurations, where the local cache may in fact be stale even if the timestamps are up to date - if the facts change or if the server changes.", }, :dynamicfacts => { :default => "memorysize,memoryfree,swapsize,swapfree", :desc => "(Deprecated) Facts that are dynamic; these facts will be ignored when deciding whether changed facts should result in a recompile. Multiple facts should be comma-separated.", :hook => proc { |value| if value Puppet.deprecation_warning "The dynamicfacts setting is deprecated and will be ignored." end } }, :splaylimit => { :default => "$runinterval", :type => :duration, :desc => "The maximum time to delay before runs. Defaults to being the same as the run interval. #{AS_DURATION}", }, :splay => { :default => false, :type => :boolean, :desc => "Whether to sleep for a pseudo-random (but consistent) amount of time before a run.", }, :clientbucketdir => { :default => "$vardir/clientbucket", :type => :directory, :mode => "0750", :desc => "Where FileBucket files are stored locally." }, :configtimeout => { :default => "2m", :type => :duration, :desc => "How long the client should wait for the configuration to be retrieved before considering it a failure. This can help reduce flapping if too many clients contact the server at one time. #{AS_DURATION}", }, :report_server => { :default => "$server", :desc => "The server to send transaction reports to.", }, :report_port => { :default => "$masterport", :desc => "The port to communicate with the report_server.", }, :inventory_server => { :default => "$server", :desc => "The server to send facts to.", }, :inventory_port => { :default => "$masterport", :desc => "The port to communicate with the inventory_server.", }, :report => { :default => true, :type => :boolean, :desc => "Whether to send reports after every transaction.", }, :lastrunfile => { :default => "$statedir/last_run_summary.yaml", :type => :file, :mode => "0644", :desc => "Where puppet agent stores the last run report summary in yaml format." }, :lastrunreport => { :default => "$statedir/last_run_report.yaml", :type => :file, :mode => "0640", :desc => "Where puppet agent stores the last run report in yaml format." }, :graph => { :default => false, :type => :boolean, :desc => "Whether to create dot graph files for the different configuration graphs. These dot files can be interpreted by tools like OmniGraffle or dot (which is part of ImageMagick).", }, :graphdir => { :default => "$statedir/graphs", :type => :directory, :desc => "Where to store dot-outputted graphs.", }, :http_compression => { :default => false, :type => :boolean, :desc => "Allow http compression in REST communication with the master. This setting might improve performance for agent -> master communications over slow WANs. Your puppet master needs to support compression (usually by activating some settings in a reverse-proxy in front of the puppet master, which rules out webrick). It is harmless to activate this settings if your master doesn't support compression, but if it supports it, this setting might reduce performance on high-speed LANs.", }, :waitforcert => { :default => "2m", :type => :duration, :desc => "How frequently puppet agent should ask for a signed certificate. When starting for the first time, puppet agent will submit a certificate signing request (CSR) to the server named in the `ca_server` setting (usually the puppet master); this may be autosigned, or may need to be approved by a human, depending on the CA server's configuration. Puppet agent cannot apply configurations until its approved certificate is available. Since the certificate may or may not be available immediately, puppet agent will repeatedly try to fetch it at this interval. You can turn off waiting for certificates by specifying a time of 0, in which case puppet agent will exit if it cannot get a cert. #{AS_DURATION}", }, :ordering => { :type => :enum, :values => ["manifest", "title-hash", "random"], - :default => "title-hash", + :default => "manifest", :desc => "How unrelated resources should be ordered when applying a catalog. Allowed values are `title-hash`, `manifest`, and `random`. This setting affects puppet agent and puppet apply, but not puppet master. * `title-hash` (the default) will order resources randomly, but will use the same order across runs and across nodes. * `manifest` will use the order in which the resources were declared in their manifest files. * `random` will order resources randomly and change their order with each run. This can work like a fuzzer for shaking out undeclared dependencies. Regardless of this setting's value, Puppet will always obey explicit dependencies set with the before/require/notify/subscribe metaparameters and the `->`/`~>` chaining arrows; this setting only affects the relative ordering of _unrelated_ resources." } ) define_settings(:inspect, :archive_files => { :type => :boolean, :default => false, :desc => "During an inspect run, whether to archive files whose contents are audited to a file bucket.", }, :archive_file_server => { :default => "$server", :desc => "During an inspect run, the file bucket server to archive files to if archive_files is set.", } ) # Plugin information. define_settings( :main, :plugindest => { :type => :directory, :default => "$libdir", :desc => "Where Puppet should store plugins that it pulls down from the central server.", }, :pluginsource => { :default => "puppet://$server/plugins", :desc => "From where to retrieve plugins. The standard Puppet `file` type is used for retrieval, so anything that is a valid file source can be used here.", }, :pluginfactdest => { :type => :directory, :default => "$vardir/facts.d", :desc => "Where Puppet should store external facts that are being handled by pluginsync", }, :pluginfactsource => { :default => "puppet://$server/pluginfacts", :desc => "Where to retrieve external facts for pluginsync", }, :pluginsync => { :default => true, :type => :boolean, :desc => "Whether plugins should be synced with the central server.", }, :pluginsignore => { :default => ".svn CVS .git", :desc => "What files to ignore when pulling down plugins.", } ) # Central fact information. define_settings( :main, :factpath => { :type => :path, :default => "$vardir/lib/facter#{File::PATH_SEPARATOR}$vardir/facts", :desc => "Where Puppet should look for facts. Multiple directories should be separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.)", :call_hook => :on_initialize_and_write, # Call our hook with the default value, so we always get the value added to facter. :hook => proc do |value| paths = value.split(File::PATH_SEPARATOR) Facter.search(*paths) end } ) define_settings( :tagmail, :tagmap => { :default => "$confdir/tagmail.conf", :desc => "The mapping between reporting tags and email addresses.", }, :sendmail => { :default => which('sendmail') || '', :desc => "Where to find the sendmail binary with which to send email.", }, :reportfrom => { :default => lambda { "report@#{Puppet::Settings.default_certname.downcase}" }, :desc => "The 'from' email address for the reports.", }, :smtpserver => { :default => "none", :desc => "The server through which to send email reports.", }, :smtpport => { :default => 25, :desc => "The TCP port through which to send email reports.", }, :smtphelo => { :default => lambda { Facter.value 'fqdn' }, :desc => "The name by which we identify ourselves in SMTP HELO for reports. If you send to a smtpserver which does strict HELO checking (as with Postfix's `smtpd_helo_restrictions` access controls), you may need to ensure this resolves.", } ) define_settings( :rails, :dblocation => { :default => "$statedir/clientconfigs.sqlite3", :type => :file, :mode => "0660", :owner => "service", :group => "service", :desc => "The sqlite database file. #{STORECONFIGS_ONLY}" }, :dbadapter => { :default => "sqlite3", :desc => "The type of database to use. #{STORECONFIGS_ONLY}", }, :dbmigrate => { :default => false, :type => :boolean, :desc => "Whether to automatically migrate the database. #{STORECONFIGS_ONLY}", }, :dbname => { :default => "puppet", :desc => "The name of the database to use. #{STORECONFIGS_ONLY}", }, :dbserver => { :default => "localhost", :desc => "The database server for caching. Only used when networked databases are used.", }, :dbport => { :default => "", :desc => "The database password for caching. Only used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbuser => { :default => "puppet", :desc => "The database user for caching. Only used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbpassword => { :default => "puppet", :desc => "The database password for caching. Only used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbconnections => { :default => '', :desc => "The number of database connections for networked databases. Will be ignored unless the value is a positive integer. #{STORECONFIGS_ONLY}", }, :dbsocket => { :default => "", :desc => "The database socket location. Only used when networked databases are used. Will be ignored if the value is an empty string. #{STORECONFIGS_ONLY}", }, :railslog => { :default => "$logdir/rails.log", :type => :file, :mode => "0600", :owner => "service", :group => "service", :desc => "Where Rails-specific logs are sent. #{STORECONFIGS_ONLY}" }, :rails_loglevel => { :default => "info", :desc => "The log level for Rails connections. The value must be a valid log level within Rails. Production environments normally use `info` and other environments normally use `debug`. #{STORECONFIGS_ONLY}", } ) define_settings( :couchdb, :couchdb_url => { :default => "http://127.0.0.1:5984/puppet", :desc => "The url where the puppet couchdb database will be created. Only used when `facts_terminus` is set to `couch`.", } ) define_settings( :transaction, :tags => { :default => "", :desc => "Tags to use to find resources. If this is set, then only resources tagged with the specified tags will be applied. Values must be comma-separated.", }, :evaltrace => { :default => false, :type => :boolean, :desc => "Whether each resource should log when it is being evaluated. This allows you to interactively see exactly what is being done.", }, :summarize => { :default => false, :type => :boolean, :desc => "Whether to print a transaction summary.", } ) define_settings( :main, :external_nodes => { :default => "none", :desc => "An external command that can produce node information. The command's output must be a YAML dump of a hash, and that hash must have a `classes` key and/or a `parameters` key, where `classes` is an array or hash and `parameters` is a hash. For unknown nodes, the command should exit with a non-zero exit code. This command makes it straightforward to store your node mapping information in other data sources like databases.", } ) define_settings( :ldap, :ldapssl => { :default => false, :type => :boolean, :desc => "Whether SSL should be used when searching for nodes. Defaults to false because SSL usually requires certificates to be set up on the client side.", }, :ldaptls => { :default => false, :type => :boolean, :desc => "Whether TLS should be used when searching for nodes. Defaults to false because TLS usually requires certificates to be set up on the client side.", }, :ldapserver => { :default => "ldap", :desc => "The LDAP server. Only used if `node_terminus` is set to `ldap`.", }, :ldapport => { :default => 389, :desc => "The LDAP port. Only used if `node_terminus` is set to `ldap`.", }, :ldapstring => { :default => "(&(objectclass=puppetClient)(cn=%s))", :desc => "The search string used to find an LDAP node.", }, :ldapclassattrs => { :default => "puppetclass", :desc => "The LDAP attributes to use to define Puppet classes. Values should be comma-separated.", }, :ldapstackedattrs => { :default => "puppetvar", :desc => "The LDAP attributes that should be stacked to arrays by adding the values in all hierarchy elements of the tree. Values should be comma-separated.", }, :ldapattrs => { :default => "all", :desc => "The LDAP attributes to include when querying LDAP for nodes. All returned attributes are set as variables in the top-level scope. Multiple values should be comma-separated. The value 'all' returns all attributes.", }, :ldapparentattr => { :default => "parentnode", :desc => "The attribute to use to define the parent node.", }, :ldapuser => { :default => "", :desc => "The user to use to connect to LDAP. Must be specified as a full DN.", }, :ldappassword => { :default => "", :desc => "The password to use to connect to LDAP.", }, :ldapbase => { :default => "", :desc => "The search base for LDAP searches. It's impossible to provide a meaningful default here, although the LDAP libraries might have one already set. Generally, it should be the 'ou=Hosts' branch under your main directory.", } ) define_settings(:master, :storeconfigs => { :default => false, :type => :boolean, :desc => "Whether to store each client's configuration, including catalogs, facts, and related data. This also enables the import and export of resources in the Puppet language - a mechanism for exchange resources between nodes. By default this uses ActiveRecord and an SQL database to store and query the data; this, in turn, will depend on Rails being available. You can adjust the backend using the storeconfigs_backend setting.", # Call our hook with the default value, so we always get the libdir set. :call_hook => :on_initialize_and_write, :hook => proc do |value| require 'puppet/node' require 'puppet/node/facts' if value if not Puppet.settings[:async_storeconfigs] Puppet::Resource::Catalog.indirection.cache_class = :store_configs Puppet.settings.override_default(:catalog_cache_terminus, :store_configs) end Puppet::Node::Facts.indirection.cache_class = :store_configs Puppet::Resource.indirection.terminus_class = :store_configs end end }, :storeconfigs_backend => { :type => :terminus, :default => "active_record", :desc => "Configure the backend terminus used for StoreConfigs. By default, this uses the ActiveRecord store, which directly talks to the database from within the Puppet Master process." } ) define_settings(:parser, :templatedir => { :default => "$vardir/templates", :type => :directory, :desc => "Where Puppet looks for template files. Can be a list of colon-separated directories. This setting is deprecated. Please put your templates in modules instead.", :deprecated => :completely, }, :allow_variables_with_dashes => { :default => false, :desc => <<-'EOT' Permit hyphens (`-`) in variable names and issue deprecation warnings about them. This setting **should always be `false`;** setting it to `true` will cause subtle and wide-ranging bugs. It will be removed in a future version. Hyphenated variables caused major problems in the language, but were allowed between Puppet 2.7.3 and 2.7.14. If you used them during this window, we apologize for the inconvenience --- you can temporarily set this to `true` in order to upgrade, and can rename your variables at your leisure. Please revert it to `false` after you have renamed all affected variables. EOT }, :parser => { :default => "current", :desc => <<-'EOT' Selects the parser to use for parsing puppet manifests (in puppet DSL language/'.pp' files). Available choices are `current` (the default) and `future`. The `current` parser means that the released version of the parser should be used. The `future` parser is a "time travel to the future" allowing early exposure to new language features. What these features are will vary from release to release and they may be invididually configurable. Available Since Puppet 3.2. EOT }, :max_errors => { :default => 10, :desc => <<-'EOT' Sets the max number of logged/displayed parser validation errors in case multiple errors have been detected. A value of 0 is the same as a value of 1; a minimum of one error is always raised. The count is per manifest. EOT }, :max_warnings => { :default => 10, :desc => <<-'EOT' Sets the max number of logged/displayed parser validation warnings in case multiple warnings have been detected. A value of 0 blocks logging of warnings. The count is per manifest. EOT }, :max_deprecations => { :default => 10, :desc => <<-'EOT' Sets the max number of logged/displayed parser validation deprecation warnings in case multiple deprecation warnings have been detected. A value of 0 blocks the logging of deprecation warnings. The count is per manifest. EOT }, :strict_variables => { :default => false, :type => :boolean, :desc => <<-'EOT' Makes the parser raise errors when referencing unknown variables. (This does not affect referencing variables that are explicitly set to undef). EOT } ) define_settings(:puppetdoc, :document_all => { :default => false, :type => :boolean, :desc => "Whether to document all resources when using `puppet doc` to generate manifest documentation.", } ) end diff --git a/lib/puppet/dsl.rb b/lib/puppet/dsl.rb deleted file mode 100644 index 97a310436..000000000 --- a/lib/puppet/dsl.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'puppet' - -module Puppet::DSL -end - -require 'puppet/dsl/resource_type_api' -require 'puppet/dsl/resource_api' diff --git a/lib/puppet/dsl/resource_api.rb b/lib/puppet/dsl/resource_api.rb deleted file mode 100644 index d82373b0e..000000000 --- a/lib/puppet/dsl/resource_api.rb +++ /dev/null @@ -1,120 +0,0 @@ -# This module adds functionality to a resource to make it -# capable of evaluating the DSL resource type block and also -# hooking into the scope system. -require 'puppet/resource/type_collection_helper' - -class Puppet::DSL::ResourceAPI - include Puppet::Resource::TypeCollectionHelper - - FUNCTION_MAP = {:acquire => :include} - - attr_reader :scope, :resource, :block - - def environment - scope.environment - end - - def evaluate - set_instance_variables - instance_eval(&block) - end - - def initialize(resource, scope, block) - @scope = scope - @resource = resource - @block = block - end - - # Try to convert a missing method into a resource type or a function. - def method_missing(name, *args) - raise "MethodMissing loop when searching for #{name} with #{args.inspect}" if searching_for_method? - @searching_for_method = true - return create_resource(name, args[0], args[1]) if valid_type?(name) - - name = map_function(name) - - return call_function(name, args) if Puppet::Parser::Functions.function(name) - - super - ensure - @searching_for_method = false - end - - def set_instance_variables - resource.eachparam do |param| - instance_variable_set("@#{param.name}", param.value) - end - @title = resource.title - @name ||= resource.title - end - - def create_resource(type, names, arguments = nil) - names = [names] unless names.is_a?(Array) - - arguments ||= {} - raise ArgumentError, "Resource arguments must be provided as a hash" unless arguments.is_a?(Hash) - - names.collect do |name| - resource = Puppet::Parser::Resource.new(type, name, :scope => scope) - arguments.each do |param, value| - resource[param] = value - end - - resource.exported = true if exporting? - resource.virtual = true if virtualizing? - scope.compiler.add_resource(scope, resource) - resource - end - end - - def call_function(name, args) - return false unless method = Puppet::Parser::Functions.function(name) - scope.send(method, *args) - end - - def export(resources = nil, &block) - if resources - resources.each { |resource| resource.exported = true } - return resources - end - @exporting = true - instance_eval(&block) - ensure - @exporting = false - end - - def virtual(resources = nil, &block) - if resources - resources.each { |resource| resource.virtual = true } - return resources - end - @virtualizing = true - instance_eval(&block) - ensure - @virtualizing = false - end - - def valid_type?(name) - return true if [:class, :node].include?(name) - return true if Puppet::Type.type(name) - return(known_resource_types.definition(name) ? true : false) - end - - private - - def exporting? - @exporting - end - - def map_function(name) - FUNCTION_MAP[name] || name - end - - def searching_for_method? - @searching_for_method - end - - def virtualizing? - @virtualizing - end -end diff --git a/lib/puppet/dsl/resource_type_api.rb b/lib/puppet/dsl/resource_type_api.rb deleted file mode 100644 index 8810d5368..000000000 --- a/lib/puppet/dsl/resource_type_api.rb +++ /dev/null @@ -1,34 +0,0 @@ -require 'puppet/resource/type' - -# Type of the objects inside of which pure ruby manifest files are -# executed. Provides methods for creating defines, hostclasses, and -# nodes. -class Puppet::DSL::ResourceTypeAPI - def initialize - @__created_ast_objects__ = [] - end - - def define(name, *args, &block) - args = args.inject([]) do |result, item| - if item.is_a?(Hash) - item.each { |p, v| result << [p, v] } - else - result << item - end - result - end - @__created_ast_objects__.push Puppet::Parser::AST::Definition.new(name, {:arguments => args}, &block) - nil - end - - def hostclass(name, options = {}, &block) - @__created_ast_objects__.push Puppet::Parser::AST::Hostclass.new(name, options, &block) - nil - end - - def node(name, options = {}, &block) - name = [name] unless name.is_a?(Array) - @__created_ast_objects__.push Puppet::Parser::AST::Node.new(name, options, &block) - nil - end -end diff --git a/lib/puppet/parameter.rb b/lib/puppet/parameter.rb index 59e18de8e..77907ffd9 100644 --- a/lib/puppet/parameter.rb +++ b/lib/puppet/parameter.rb @@ -1,585 +1,572 @@ require 'puppet/util/methodhelper' require 'puppet/util/logging' require 'puppet/util/docs' # The Parameter class is the implementation of a resource's attributes of _parameter_ kind. # The Parameter class is also the base class for {Puppet::Property}, and is used to describe meta-parameters # (parameters that apply to all resource types). # A Parameter (in contrast to a Property) has a single value where a property has both a current and a wanted value. # The Parameter class methods are used to configure and create an instance of Parameter that represents # one particular attribute data type; its valid value(s), and conversion to/from internal form. # # The intention is that a new parameter is created by using the DSL method {Puppet::Type.newparam}, or # {Puppet::Type.newmetaparam} if the parameter should be applicable to all resource types. # # A Parameter that does not specify and valid values (via {newvalues}) accepts any value. # # @see Puppet::Type # @see Puppet::Property # @api public # class Puppet::Parameter include Puppet::Util include Puppet::Util::Errors include Puppet::Util::Logging include Puppet::Util::MethodHelper require 'puppet/parameter/value_collection' class << self include Puppet::Util include Puppet::Util::Docs - # Unused? - # @todo The term "validater" only appears in this location in the Puppet code base. There is `validate` - # which seems to works fine without this attribute declaration. - # @api private - # - attr_reader :validater - - # Unused? - # @todo The term "munger" only appears in this location in the Puppet code base. There is munge and unmunge - # and they seem to work perfectly fine without this attribute declaration. - # @api private - # - attr_reader :munger # @return [Symbol] The parameter name as given when it was created. attr_reader :name # @return [Object] The default value of the parameter as determined by the {defaultto} method, or nil if no # default has been set. attr_reader :default # @comment This somewhat odd documentation construct is because the getter and setter are not # orthogonal; the setter uses varargs and this confuses yard. To overcome the problem both the # getter and the setter are documented here. If this issues is fixed, a todo will be displayed # for the setter method, and the setter documentation can be moved there. # Since the attribute is actually RW it should perhaps instead just be implemented as a setter # and a getter method (and no attr_xxx declaration). # # @!attribute [rw] required_features # @return [Array] The names of the _provider features_ required for this parameter to work. # the returned names are always all lower case symbols. # @overload required_features # Returns the required _provider features_ as an array of lower case symbols # @overload required_features=(*args) # @param *args [Symbol] one or more names of required provider features # Sets the required_provider_features_ from one or more values, or array. The given arguments # are flattened, and internalized. # @api public # @dsl type # attr_reader :required_features # @return [Puppet::Parameter::ValueCollection] The set of valid values (or an empty set that accepts any value). # @api private # attr_reader :value_collection # @return [Boolean] Flag indicating whether this parameter is a meta-parameter or not. attr_accessor :metaparam # Defines how the `default` value of a parameter is computed. # The computation of the parameter's default value is defined by providing a value or a block. # A default of `nil` can not be used. # @overload defaultto(value) # Defines the default value with a literal value # @param value [Object] the literal value to use as the default value # @overload defaultto({|| ... }) # Defines that the default value is produced by the given block. The given block # should produce the default value. # @raise [Puppet::DevError] if value is nil, and no block is given. # @return [void] # @see Parameter.default # @dsl type # @api public # def defaultto(value = nil, &block) if block define_method(:default, &block) else if value.nil? raise Puppet::DevError, "Either a default value or block must be provided" end define_method(:default) do value end end end # Produces a documentation string. # If an enumeration of _valid values_ has been defined, it is appended to the documentation # for this parameter specified with the {desc} method. # @return [String] Returns a documentation string. # @api public # def doc @doc ||= "" unless defined?(@addeddocvals) @doc = Puppet::Util::Docs.scrub(@doc) if vals = value_collection.doc @doc << "\n\n#{vals}" end if f = self.required_features @doc << "\n\nRequires features #{f.flatten.collect { |f| f.to_s }.join(" ")}." end @addeddocvals = true end @doc end # Removes the `default` method if defined. # Has no effect if the default method is not defined. # This method is intended to be used in a DSL scenario where a parameter inherits from a parameter # with a default value that is not wanted in the derived parameter (otherwise, simply do not define # a default value method). # # @return [void] # @see desc # @api public # @dsl type # def nodefault undef_method :default if public_method_defined? :default end # Sets the documentation for this parameter. # @param str [String] The documentation string to set # @return [String] the given `str` parameter # @see doc # @dsl type # @api public # def desc(str) @doc = str end # Initializes the instance variables. # Clears the internal value collection (set of allowed values). # @return [void] # @api private # def initvars @value_collection = ValueCollection.new end # @overload munge {|| ... } # Defines an optional method used to convert the parameter value from DSL/string form to an internal form. # If a munge method is not defined, the DSL/string value is used as is. # @note This adds a method with the name `unsafe_munge` in the created parameter class. Later this method is # called in a context where exceptions will be rescued and handled. # @dsl type # @api public # def munge(&block) # I need to wrap the unsafe version in begin/rescue parameterments, # but if I directly call the block then it gets bound to the # class's context, not the instance's, thus the two methods, # instead of just one. define_method(:unsafe_munge, &block) end # @overload unmunge {|| ... } # Defines an optional method used to convert the parameter value to DSL/string form from an internal form. # If an `unmunge` method is not defined, the internal form is used. # @see munge # @note This adds a method with the name `unmunge` in the created parameter class. # @dsl type # @api public # def unmunge(&block) define_method(:unmunge, &block) end # Sets a marker indicating that this parameter is the _namevar_ (unique identifier) of the type # where the parameter is contained. # This also makes the parameter a required value. The marker can not be unset once it has been set. # @return [void] # @dsl type # @api public # def isnamevar @isnamevar = true @required = true end # @return [Boolean] Returns whether this parameter is the _namevar_ or not. # @api public # def isnamevar? @isnamevar end # Sets a marker indicating that this parameter is required. # Once set, it is not possible to make a parameter optional. # @return [void] # @dsl type # @api public # def isrequired @required = true end # @comment This method is not picked up by yard as it has a different signature than # expected for an attribute (varargs). Instead, this method is documented as an overload # of the attribute required_features. (Not ideal, but better than nothing). # @todo If this text appears in documentation - see comment in source and makes corrections - it means # that an issue in yardoc has been fixed. # def required_features=(*args) @required_features = args.flatten.collect { |a| a.to_s.downcase.intern } end # Returns whether this parameter is required or not. # A parameter is required if a call has been made to the DSL method {isrequired}. # @return [Boolean] Returns whether this parameter is required or not. # @api public # def required? @required end # @overload validate {|| ... } # Defines an optional method that is used to validate the parameter's DSL/string value. # Validation should raise appropriate exceptions, the return value of the given block is ignored. # The easiest way to raise an appropriate exception is to call the method {Puppet::Util::Errors.fail} with # the message as an argument. # To validate the munged value instead, just munge the value (`munge(value)`). # # @return [void] # @dsl type # @api public # def validate(&block) define_method(:unsafe_validate, &block) end # Defines valid values for the parameter (enumeration or regular expressions). # The set of valid values for the parameter can be limited to a (mix of) literal values and # regular expression patterns. # @note Each call to this method adds to the set of valid values # @param names [Symbol, Regexp] The set of valid literal values and/or patterns for the parameter. # @return [void] # @dsl type # @api public # def newvalues(*names) @value_collection.newvalues(*names) end # Makes the given `name` an alias for the given `other` name. # Or said differently, the valid value `other` can now also be referred to via the given `name`. # Aliasing may affect how the parameter's value is serialized/stored (it may store the `other` value # instead of the alias). # @api public # @dsl type # def aliasvalue(name, other) @value_collection.aliasvalue(name, other) end end # Creates instance (proxy) methods that delegates to a class method with the same name. # @api private # def self.proxymethods(*values) values.each { |val| define_method(val) do self.class.send(val) end } end # @!method required? # (see required?) # @!method isnamevar? # (see isnamevar?) # proxymethods("required?", "isnamevar?") # @return [Puppet::Resource] A reference to the resource this parameter is an attribute of (the _associated resource_). attr_accessor :resource # @comment LAK 2007-05-09: Keep the @parent around for backward compatibility. # @return [Puppet::Parameter] A reference to the parameter's parent kept for backwards compatibility. # @api private # attr_accessor :parent # Returns a string representation of the resource's containment path in # the catalog. # @return [String] def path @path ||= '/' + pathbuilder.join('/') end # @return [Integer] Returns the result of calling the same method on the associated resource. def line resource.line end # @return [Integer] Returns the result of calling the same method on the associated resource. def file resource.file end # @return [Integer] Returns the result of calling the same method on the associated resource. def version resource.version end # Initializes the parameter with a required resource reference and optional attribute settings. # The option `:resource` must be specified or an exception is raised. Any additional options passed # are used to initialize the attributes of this parameter by treating each key in the `options` hash as # the name of the attribute to set, and the value as the value to set. # @param options [Hash{Symbol => Object]] Options, where `resource` is required # @option options [Puppet::Resource] :resource The resource this parameter holds a value for. Required. # @raise [Puppet::DevError] If resource is not specified in the options hash. # @api public # @note A parameter should be created via the DSL method {Puppet::Type::newparam} # def initialize(options = {}) options = symbolize_options(options) if resource = options[:resource] self.resource = resource options.delete(:resource) else raise Puppet::DevError, "No resource set for #{self.class.name}" end set_options(options) end # Writes the given `msg` to the log with the loglevel indicated by the associated resource's # `loglevel` parameter. # @todo is loglevel a metaparameter? it is looked up with `resource[:loglevel]` # @return [void] # @api public def log(msg) send_log(resource[:loglevel], msg) end # @return [Boolean] Returns whether this parameter is a meta-parameter or not. def metaparam? self.class.metaparam end # @!attribute [r] name # @return [Symbol] The parameter's name as given when it was created. # @note Since a Parameter defines the name at the class level, each Parameter class must be # unique within a type's inheritance chain. # @comment each parameter class must define the name method, and parameter # instances do not change that name this implicitly means that a given # object can only have one parameter instance of a given parameter # class def name self.class.name end # @return [Boolean] Returns true if this parameter, the associated resource, or overall puppet mode is `noop`. # @todo How is noop mode set for a parameter? Is this of value in DSL to inhibit a parameter? # def noop @noop ||= false tmp = @noop || self.resource.noop || Puppet[:noop] || false #debug "noop is #{tmp}" tmp 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 @resource return [@resource.pathbuilder, self.name] else return [self.name] end end # This is the default implementation of `munge` that simply produces the value (if it is valid). # The DSL method {munge} should be used to define an overriding method if munging is required. # # @api private # def unsafe_munge(value) self.class.value_collection.munge(value) end # Unmunges the value by transforming it from internal form to DSL form. # This is the default implementation of `unmunge` that simply returns the value without processing. # The DSL method {unmunge} should be used to define an overriding method if required. # @return [Object] the unmunged value # def unmunge(value) value end # Munges the value to internal form. # This implementation of `munge` provides exception handling around the specified munging of this parameter. # @note This method should not be overridden. Use the DSL method {munge} to define a munging method # if required. # @param value [Object] the DSL value to munge # @return [Object] the munged (internal) value # def munge(value) begin ret = unsafe_munge(value) rescue Puppet::Error => detail Puppet.debug "Reraising #{detail}" raise rescue => detail raise Puppet::DevError, "Munging failed for value #{value.inspect} in class #{self.name}: #{detail}", detail.backtrace end ret end # This is the default implementation of `validate` that may be overridden by the DSL method {validate}. # If no valid values have been defined, the given value is accepted, else it is validated against # the literal values (enumerator) and/or patterns defined by calling {newvalues}. # # @param value [Object] the value to check for validity # @raise [ArgumentError] if the value is not valid # @return [void] # @api private # def unsafe_validate(value) self.class.value_collection.validate(value) end # Performs validation of the given value against the rules defined by this parameter. # @return [void] # @todo Better description of when the various exceptions are raised.ArgumentError is rescued and # changed into Puppet::Error. # @raise [ArgumentError, TypeError, Puppet::DevError, Puppet::Error] under various conditions # A protected validation method that only ever raises useful exceptions. # @api public # def validate(value) begin unsafe_validate(value) rescue ArgumentError => detail self.fail Puppet::Error, detail.to_s, detail rescue Puppet::Error, TypeError raise rescue => detail raise Puppet::DevError, "Validate method failed for class #{self.name}: #{detail}", detail.backtrace end end # Sets the associated resource to nil. # @todo Why - what is the intent/purpose of this? # @return [nil] # def remove @resource = nil end # @return [Object] Gets the value of this parameter after performing any specified unmunging. def value unmunge(@value) unless @value.nil? end # Sets the given value as the value of this parameter. # @todo This original comment _"All of the checking should possibly be # late-binding (e.g., users might not exist when the value is assigned # but might when it is asked for)."_ does not seem to be correct, the implementation # calls both validate and munge on the given value, so no late binding. # # The given value is validated and then munged (if munging has been specified). The result is store # as the value of this arameter. # @return [Object] The given `value` after munging. # @raise (see #validate) # def value=(value) validate(value) @value = munge(value) end # @return [Puppet::Provider] Returns the provider of the associated resource. # @todo The original comment says = _"Retrieve the resource's provider. # Some types don't have providers, in which case we return the resource object itself."_ # This does not seem to be true, the default implementation that sets this value may be # {Puppet::Type.provider=} which always gets either the name of a provider or an instance of one. # def provider @resource.provider end # @return [Array] Returns an array of the associated resource's symbolic tags (including the parameter itself). # Returns an array of the associated resource's symbolic tags (including the parameter itself). # At a minimun, the array contains the name of the parameter. If the associated resource # has tags, these tags are also included in the array. # @todo The original comment says = _"The properties need to return tags so that logs correctly # collect them."_ what if anything of that is of interest to document. Should tags and their relationship # to logs be described. This is a more general concept. # def tags unless defined?(@tags) @tags = [] # This might not be true in testing @tags = @resource.tags if @resource.respond_to? :tags @tags << self.name.to_s end @tags end # @return [String] The name of the parameter in string form. def to_s name.to_s end # Produces a String with the value formatted for display to a human. # When the parameter value is a: # # * **single valued parameter value** the result is produced on the # form `'value'` where _value_ is the string form of the parameter's value. # # * **Array** the list of values is enclosed in `[]`, and # each produced value is separated by a comma. # # * **Hash** value is output with keys in sorted order enclosed in `{}` with each entry formatted # on the form `'k' => v` where # `k` is the key in string form and _v_ is the value of the key. Entries are comma separated. # # For both Array and Hash this method is called recursively to format contained values. # @note this method does not protect against infinite structures. # # @return [String] The formatted value in string form. # def self.format_value_for_display(value) if value.is_a? Array formatted_values = value.collect {|value| format_value_for_display(value)}.join(', ') "[#{formatted_values}]" elsif value.is_a? Hash # Sorting the hash keys for display is largely for having stable # output to test against, but also helps when scanning for hash # keys, since they will be in ASCIIbetical order. hash = value.keys.sort {|a,b| a.to_s <=> b.to_s}.collect do |k| "'#{k}' => #{format_value_for_display(value[k])}" end.join(', ') "{#{hash}}" else "'#{value}'" end end # @comment Document post_compile_hook here as it does not exist anywhere (called from type if implemented) # @!method post_compile() # @since 3.4.0 # @api public # @abstract A subclass may implement this - it is not implemented in the Parameter class # This method may be implemented by a parameter in order to perform actions during compilation # after all resources have been added to the catalog. # @see Puppet::Type#finish # @see Puppet::Parser::Compiler#finish end require 'puppet/parameter/path' diff --git a/lib/puppet/parser/ast/definition.rb b/lib/puppet/parser/ast/definition.rb index c43422f82..0a81d46fe 100644 --- a/lib/puppet/parser/ast/definition.rb +++ b/lib/puppet/parser/ast/definition.rb @@ -1,17 +1,15 @@ require 'puppet/parser/ast/top_level_construct' class Puppet::Parser::AST::Definition < Puppet::Parser::AST::TopLevelConstruct attr_accessor :context - def initialize(name, context = {}, &ruby_code) + def initialize(name, context = {}) @name = name @context = context - @ruby_code = ruby_code end def instantiate(modname) new_definition = Puppet::Resource::Type.new(:definition, @name, @context.merge(:module_name => modname)) - new_definition.ruby_code = @ruby_code if @ruby_code [new_definition] end end diff --git a/lib/puppet/parser/ast/hostclass.rb b/lib/puppet/parser/ast/hostclass.rb index cab5e4a24..50ac55d98 100644 --- a/lib/puppet/parser/ast/hostclass.rb +++ b/lib/puppet/parser/ast/hostclass.rb @@ -1,29 +1,27 @@ require 'puppet/parser/ast/top_level_construct' class Puppet::Parser::AST::Hostclass < Puppet::Parser::AST::TopLevelConstruct attr_accessor :name, :context - def initialize(name, context = {}, &ruby_code) + def initialize(name, context = {}) @context = context @name = name - @ruby_code = ruby_code end def instantiate(modname) new_class = Puppet::Resource::Type.new(:hostclass, @name, @context.merge(:module_name => modname)) - new_class.ruby_code = @ruby_code if @ruby_code all_types = [new_class] if code code.each do |nested_ast_node| if nested_ast_node.respond_to? :instantiate all_types += nested_ast_node.instantiate(modname) end end end return all_types end def code() @context[:code] end end diff --git a/lib/puppet/parser/ast/node.rb b/lib/puppet/parser/ast/node.rb index fd6443327..15820ebdf 100644 --- a/lib/puppet/parser/ast/node.rb +++ b/lib/puppet/parser/ast/node.rb @@ -1,25 +1,23 @@ require 'puppet/parser/ast/top_level_construct' class Puppet::Parser::AST::Node < Puppet::Parser::AST::TopLevelConstruct attr_accessor :names, :context - def initialize(names, context = {}, &ruby_code) + def initialize(names, context = {}) raise ArgumentError, "names should be an array" unless names.is_a? Array if context[:parent] msg = "Deprecation notice: Node inheritance is not supported in Puppet >= 4.0.0. See http://links.puppetlabs.com/puppet-node-inheritance-deprecation" Puppet.puppet_deprecation_warning(msg, :key => "node-inheritance-#{names.join}", :file => context[:file], :line => context[:line]) end @names = names @context = context - @ruby_code = ruby_code end def instantiate(modname) @names.collect do |name| new_node = Puppet::Resource::Type.new(:node, name, @context.merge(:module_name => modname)) - new_node.ruby_code = @ruby_code if @ruby_code new_node end end end diff --git a/lib/puppet/parser/e4_parser_adapter.rb b/lib/puppet/parser/e4_parser_adapter.rb index 00911b5d5..d5e635339 100644 --- a/lib/puppet/parser/e4_parser_adapter.rb +++ b/lib/puppet/parser/e4_parser_adapter.rb @@ -1,81 +1,72 @@ require 'puppet/pops' module Puppet; module Parser; end; end; # Adapts an egrammar/eparser to respond to the public API of the classic parser # and makes use of the new evaluator. # class Puppet::Parser::E4ParserAdapter # Empty adapter fulfills watch_file contract without doing anything. # @api private class NullFileWatcher def watch_file(file) #nop end end # @param file_watcher [#watch_file] something that can watch a file def initialize(file_watcher = nil) @file_watcher = file_watcher || NullFileWatcher.new @file = '' @string = '' @use = :unspecified @@evaluating_parser ||= Puppet::Pops::Parser::EvaluatingParser.new() end def file=(file) @file = file @use = :file # watch if possible, but only if the file is something worth watching @file_watcher.watch_file(file) if !file.nil? && file != '' end def parse(string = nil) self.string= string if string - if @file =~ /\.rb$/ && @use != :string - # Will throw an error - parse_ruby_file - end - parse_result = if @use == :string # Parse with a source_file to set in created AST objects (it was either given, or it may be unknown # if caller did not set a file and the present a string. # @@evaluating_parser.parse_string(@string, @file || "unknown-source-location") else @@evaluating_parser.parse_file(@file) end # the parse_result may be # * empty / nil (no input) # * a Model::Program # * a Model::Expression # model = parse_result.nil? ? nil : parse_result.current args = {} Puppet::Pops::Model::AstTransformer.new(@file).merge_location(args, model) ast_code = if model.is_a? Puppet::Pops::Model::Program Puppet::Parser::AST::PopsBridge::Program.new(model, args) else args[:value] = model Puppet::Parser::AST::PopsBridge::Expression.new(args) end # Create the "main" class for the content - this content will get merged with all other "main" content Puppet::Parser::AST::Hostclass.new('', :code => ast_code) end def string=(string) @string = string @use = :string end - - def parse_ruby_file - raise Puppet::ParseError, "Ruby DSL is no longer supported. Attempt to parse #{@file}" - end end diff --git a/lib/puppet/parser/functions/search.rb b/lib/puppet/parser/functions/search.rb deleted file mode 100644 index 8055e38b1..000000000 --- a/lib/puppet/parser/functions/search.rb +++ /dev/null @@ -1,12 +0,0 @@ -Puppet::Parser::Functions::newfunction(:search, :arity => -2, :doc => "Add another namespace for this class to search. - This allows you to create classes with sets of definitions and add - those classes to another class's search path. - - Deprecated in Puppet 3.7.0, to be removed in Puppet 4.0.0.") do |vals| - - Puppet.deprecation_warning("The 'search' function is deprecated. See http://links.puppetlabs.com/search-function-deprecation") - - vals.each do |val| - add_namespace(val) - end -end diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb index 963e4d106..a34b213d0 100644 --- a/lib/puppet/parser/parser_support.rb +++ b/lib/puppet/parser/parser_support.rb @@ -1,195 +1,179 @@ # I pulled this into a separate file, because I got # tired of rebuilding the parser.rb file all the time. require 'forwardable' class Puppet::Parser::Parser extend Forwardable require 'puppet/parser/functions' require 'puppet/parser/files' require 'puppet/resource/type_collection' require 'puppet/resource/type_collection_helper' require 'puppet/resource/type' require 'monitor' AST = Puppet::Parser::AST include Puppet::Resource::TypeCollectionHelper attr_reader :version, :environment attr_accessor :files attr_accessor :lexer # Add context to a message; useful for error messages and such. def addcontext(message, obj = nil) obj ||= @lexer message += " on line #{obj.line}" if file = obj.file message += " in file #{file}" end message end # Create an AST array containing a single element def aryfy(arg) ast AST::ASTArray, :children => [arg] end # Create an AST block containing a single element def block(arg) ast AST::BlockExpression, :children => [arg] end # Create an AST object, and automatically add the file and line information if # available. def ast(klass, hash = {}) klass.new ast_context(klass.use_docs, hash[:line]).merge(hash) end def ast_context(include_docs = false, ast_line = nil) result = { :line => ast_line || lexer.line, :file => lexer.file } result[:doc] = lexer.getcomment(result[:line]) if include_docs result end # The fully qualifed name, with the full namespace. def classname(name) [@lexer.namespace, name].join("::").sub(/^::/, '') end def clear initvars end # Raise a Parse error. def error(message, options = {}) if @lexer.expected message += "; expected '%s'" end except = Puppet::ParseError.new(message) except.line = options[:line] || @lexer.line except.file = options[:file] || @lexer.file raise except end def_delegators :@lexer, :file, :string= def file=(file) unless Puppet::FileSystem.exist?(file) unless file =~ /\.pp$/ file = file + ".pp" end end raise Puppet::AlreadyImportedError, "Import loop detected for #{file}" if known_resource_types.watching_file?(file) watch_file(file) @lexer.file = file end def_delegators :known_resource_types, :hostclass, :definition, :node, :nodes? def_delegators :known_resource_types, :find_hostclass, :find_definition def_delegators :known_resource_types, :watch_file, :version def import(file) deprecation_location_text = if @lexer.file && @lexer.line " at #{@lexer.file}:#{@lexer.line}" elsif @lexer.file " in file #{@lexer.file}" elsif @lexer.line " at #{@lexer.line}" end Puppet.deprecation_warning("The use of 'import' is deprecated#{deprecation_location_text}. See http://links.puppetlabs.com/puppet-import-deprecation") if @lexer.file # use a path relative to the file doing the importing dir = File.dirname(@lexer.file) else # otherwise assume that everything needs to be from where the user is # executing this command. Normally, this would be in a "puppet apply -e" dir = Dir.pwd end known_resource_types.loader.import(file, dir) end def initialize(env) @environment = env initvars end # Initialize or reset all of our variables. def initvars @lexer = Puppet::Parser::Lexer.new end # Split an fq name into a namespace and name def namesplit(fullname) ary = fullname.split("::") n = ary.pop || "" ns = ary.join("::") return ns, n end def on_error(token,value,stack) if token == 0 # denotes end of file value = 'end of file' else value = "'#{value[:value]}'" end error = "Syntax error at #{value}" if brace = @lexer.expected error += "; expected '#{brace}'" end except = Puppet::ParseError.new(error) except.line = @lexer.line except.file = @lexer.file if @lexer.file raise except end # how should I do error handling here? def parse(string = nil) - if self.file =~ /\.rb$/ - main = parse_ruby_file - else - self.string = string if string - begin - @yydebug = false - main = yyparse(@lexer,:scan) - rescue Puppet::ParseError => except - except.line ||= @lexer.line - except.file ||= @lexer.file - except.pos ||= @lexer.pos - raise except - rescue => except - raise Puppet::ParseError.new(except.message, @lexer.file, @lexer.line, nil, except) - end + self.string = string if string + begin + @yydebug = false + main = yyparse(@lexer,:scan) + rescue Puppet::ParseError => except + except.line ||= @lexer.line + except.file ||= @lexer.file + except.pos ||= @lexer.pos + raise except + rescue => except + raise Puppet::ParseError.new(except.message, @lexer.file, @lexer.line, nil, except) end # Store the results as the top-level class. return Puppet::Parser::AST::Hostclass.new('', :code => main) ensure @lexer.clear end - - def parse_ruby_file - Puppet.deprecation_warning("Use of the Ruby DSL is deprecated.") - - # Execute the contents of the file inside its own "main" object so - # that it can call methods in the resource type API. - main_object = Puppet::DSL::ResourceTypeAPI.new - main_object.instance_eval(File.read(self.file)) - - # Then extract any types that were created. - Puppet::Parser::AST::BlockExpression.new :children => main_object.instance_eval { @__created_ast_objects__ } - end end diff --git a/lib/puppet/parser/type_loader.rb b/lib/puppet/parser/type_loader.rb index 1357f948d..dc995e53b 100644 --- a/lib/puppet/parser/type_loader.rb +++ b/lib/puppet/parser/type_loader.rb @@ -1,152 +1,152 @@ require 'find' require 'forwardable' require 'puppet/node/environment' require 'puppet/parser/parser_factory' class Puppet::Parser::TypeLoader extend Forwardable # Import manifest files that match a given file glob pattern. # # @param pattern [String] the file glob to apply when determining which files # to load # @param dir [String] base directory to use when the file is not # found in a module # @api private def import(pattern, dir) return if Puppet[:ignoreimport] modname, files = Puppet::Parser::Files.find_manifests_in_modules(pattern, environment) if files.empty? abspat = File.expand_path(pattern, dir) - file_pattern = abspat + (File.extname(abspat).empty? ? '{.pp,.rb}' : '' ) + file_pattern = abspat + (File.extname(abspat).empty? ? '.pp' : '' ) files = Dir.glob(file_pattern).uniq.reject { |f| FileTest.directory?(f) } modname = nil if files.empty? raise_no_files_found(pattern) end end load_files(modname, files) end # Load all of the manifest files in all known modules. # @api private def import_all # And then load all files from each module, but (relying on system # behavior) only load files from the first module of a given name. E.g., # given first/foo and second/foo, only files from first/foo will be loaded. environment.modules.each do |mod| load_files(mod.name, mod.all_manifests) end end def_delegator :environment, :known_resource_types def initialize(env) self.environment = env end def environment @environment end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = Puppet.lookup(:environments).get(env) else @environment = env end end # Try to load the object with the given fully qualified name. def try_load_fqname(type, fqname) return nil if fqname == "" # special-case main. files_to_try_for(fqname).each do |filename| begin imported_types = import_from_modules(filename) if result = imported_types.find { |t| t.type == type and t.name == fqname } Puppet.debug "Automatically imported #{fqname} from #{filename} into #{environment}" return result end rescue Puppet::ImportError => detail # I'm not convienced we should just drop these errors, but this # preserves existing behaviours. end end # Nothing found. return nil end def parse_file(file) Puppet.debug("importing '#{file}' in environment #{environment}") parser = Puppet::Parser::ParserFactory.parser(environment) parser.file = file return parser.parse end private def import_from_modules(pattern) modname, files = Puppet::Parser::Files.find_manifests_in_modules(pattern, environment) if files.empty? raise_no_files_found(pattern) end load_files(modname, files) end def raise_no_files_found(pattern) raise Puppet::ImportError, "No file(s) found for import of '#{pattern}'" end def load_files(modname, files) @loaded ||= {} loaded_asts = [] files.reject { |file| @loaded[file] }.each do |file| # NOTE: This ugly implementation will be replaced in Puppet 3.5. # The implementation now makes use of a global variable because the context support is # not available until Puppet 3.5. # The use case is that parsing for the purpose of searching for information # should not abort. There is currently one such use case in indirector/resourcetype/parser # if Puppet.lookup(:squelch_parse_errors) {|| false } begin loaded_asts << parse_file(file) rescue => e # Resume from errors so that all parseable files would # still be parsed. Mark this file as loaded so that # it would not be parsed next time (handle it as if # it was successfully parsed). Puppet.debug("Unable to parse '#{file}': #{e.message}") end else loaded_asts << parse_file(file) end @loaded[file] = true end loaded_asts.collect do |ast| known_resource_types.import_ast(ast, modname) end.flatten end # Return a list of all file basenames that should be tried in order # to load the object with the given fully qualified name. def files_to_try_for(qualified_name) qualified_name.split('::').inject([]) do |paths, name| add_path_for_name(paths, name) end end def add_path_for_name(paths, name) if paths.empty? [name] else paths.unshift(File.join(paths.first, name)) end end end diff --git a/lib/puppet/pops/binder/config/binder_config.rb b/lib/puppet/pops/binder/config/binder_config.rb index 3e83e4afb..625aeef96 100644 --- a/lib/puppet/pops/binder/config/binder_config.rb +++ b/lib/puppet/pops/binder/config/binder_config.rb @@ -1,107 +1,107 @@ module Puppet::Pops::Binder::Config # Class holding the Binder Configuration # The configuration is obtained from the file 'binder_config.yaml' # that must reside in the root directory of the site # @api public # class BinderConfig # The layering configuration is an array of layers from most to least significant. # Each layer is represented by a Hash containing :name and :include and optionally :exclude # # @return [Array, Hash>] # @api public # attr_reader :layering_config # @return ] ({}) optional mapping of bindings-scheme to handler class name attr_reader :scheme_extensions # @return [String] the loaded config file attr_accessor :config_file DEFAULT_LAYERS = [ { 'name' => 'site', 'include' => [ 'confdir:/default?optional'] }, { 'name' => 'modules', 'include' => [ 'module:/*::default'] }, ] DEFAULT_SCHEME_EXTENSIONS = {} def default_config() # This is hardcoded now, but may be a user supplied default configuration later {'version' => 1, 'layers' => default_layers } end def confdir() Puppet.settings[:confdir] end # Creates a new Config. The configuration is loaded from the file 'binder_config.yaml' which # is expected to be found in confdir. # # @param diagnostics [DiagnosticProducer] collector of diagnostics # @api public # def initialize(diagnostics) @config_file = Puppet.settings[:binder_config] # if file is stated, it must exist # otherwise it is optional $confdir/binder_conf.yaml # and if that fails, the default case @config_file when NilClass # use the config file if it exists rootdir = confdir if rootdir.is_a?(String) expanded_config_file = File.expand_path(File.join(rootdir, '/binder_config.yaml')) if Puppet::FileSystem.exist?(expanded_config_file) @config_file = expanded_config_file end else raise ArgumentError, "No Puppet settings 'confdir', or it is not a String" end when String unless Puppet::FileSystem.exist?(@config_file) raise ArgumentError, "Cannot find the given binder configuration file '#{@config_file}'" end else raise ArgumentError, "The setting binder_config is expected to be a String, got: #{@config_file.class.name}." end unless @config_file.is_a?(String) && Puppet::FileSystem.exist?(@config_file) @config_file = nil # use defaults end validator = BinderConfigChecker.new(diagnostics) begin data = @config_file ? YAML.load_file(@config_file) : default_config() validator.validate(data, @config_file) rescue Errno::ENOENT diagnostics.accept(Issues::CONFIG_FILE_NOT_FOUND, @config_file) rescue Errno::ENOTDIR diagnostics.accept(Issues::CONFIG_FILE_NOT_FOUND, @config_file) rescue ::SyntaxError => e diagnostics.accept(Issues::CONFIG_FILE_SYNTAX_ERROR, @config_file, :detail => e.message) end unless diagnostics.errors? - @layering_config = data['layers'] or default_layers - @scheme_extensions = (data['extensions'] and data['extensions']['scheme_handlers'] or default_scheme_extensions) + @layering_config = data['layers'] || default_layers + @scheme_extensions = (data['extensions'] && data['extensions']['scheme_handlers'] || default_scheme_extensions) else @layering_config = [] @scheme_extensions = {} end end # The default_xxx methods exists to make it easier to do mocking in tests. # @api private def default_layers DEFAULT_LAYERS end # @api private def default_scheme_extensions DEFAULT_SCHEME_EXTENSIONS end end end diff --git a/lib/puppet/pops/binder/config/binder_config_checker.rb b/lib/puppet/pops/binder/config/binder_config_checker.rb index c573502a6..628d29bf2 100644 --- a/lib/puppet/pops/binder/config/binder_config_checker.rb +++ b/lib/puppet/pops/binder/config/binder_config_checker.rb @@ -1,142 +1,142 @@ module Puppet::Pops::Binder::Config # Validates the consistency of a Binder::BinderConfig class BinderConfigChecker # Create an instance with a diagnostic producer that will receive the result during validation # @param diagnostics [DiagnosticProducer] The producer that will receive the diagnostic # @api public # def initialize(diagnostics) @diagnostics = diagnostics t = Puppet::Pops::Types @type_calculator = t::TypeCalculator.new() @array_of_string_type = t::TypeFactory.array_of(t::TypeFactory.string()) end # Validate the consistency of the given data. Diagnostics will be emitted to the DiagnosticProducer # that was set when this checker was created # # @param data [Object] The data read from the config file # @param config_file [String] The full path of the file. Used in error messages # @api public # def validate(data, config_file) @unique_layer_names = Set.new() if data.is_a?(Hash) check_top_level(data, config_file) else accept(Issues::CONFIG_IS_NOT_HASH, config_file) end end private def accept(issue, semantic, options = {}) @diagnostics.accept(issue, semantic, options) end def check_top_level(data, config_file) if layers = (data['layers'] || data[:layers]) check_layers(layers, config_file) else accept(Issues::CONFIG_LAYERS_MISSING, config_file) end - if version = (data['version'] or data[:version]) + if version = (data['version'] || data[:version]) accept(Issues::CONFIG_WRONG_VERSION, config_file, {:expected => 1, :actual => version}) unless version == 1 else accept(Issues::CONFIG_VERSION_MISSING, config_file) end if extensions = data['extensions'] check_extensions(extensions, config_file) end end def check_layers(layers, config_file) unless layers.is_a?(Array) accept(Issues::LAYERS_IS_NOT_ARRAY, config_file, :klass => data.class) else layers.each {|layer| check_layer(layer, config_file) } end end def check_layer(layer, config_file) unless layer.is_a?(Hash) accept(Issues::LAYER_IS_NOT_HASH, config_file, :klass => layer.class) return end layer.each_pair do |k, v| case k when 'name' unless v.is_a?(String) accept(Issues::LAYER_NAME_NOT_STRING, config_file, :class_name => v.class.name) end unless @unique_layer_names.add?(v) accept(Issues::DUPLICATE_LAYER_NAME, config_file, :name => v.to_s ) end when 'include' check_bindings_references('include', v, config_file) when 'exclude' check_bindings_references('exclude', v, config_file) when Symbol accept(Issues::LAYER_ATTRIBUTE_IS_SYMBOL, config_file, :name => k.to_s) else accept(Issues::UNKNOWN_LAYER_ATTRIBUTE, config_file, :name => k.to_s ) end end end # references to bindings is a single String URI, or an array of String URI # @param kind [String] 'include' or 'exclude' (used in issue messages) # @param value [String, Array] one or more String URI binding references # @param config_file [String] reference to the loaded config file # def check_bindings_references(kind, value, config_file) return check_reference(value, kind, config_file) if value.is_a?(String) accept(Issues::BINDINGS_REF_NOT_STRING_OR_ARRAY, config_file, :kind => kind ) unless value.is_a?(Array) value.each {|ref| check_reference(ref, kind, config_file) } end # A reference is a URI in string form having a scheme and a path (at least '/') # def check_reference(value, kind, config_file) begin uri = URI.parse(value) unless uri.scheme accept(Issues::MISSING_SCHEME, config_file, :uri => uri) end unless uri.path accept(Issues::REF_WITHOUT_PATH, config_file, :uri => uri, :kind => kind) end rescue InvalidURIError => e accept(Issues::BINDINGS_REF_INVALID_URI, config_file, :msg => e.message) end end def check_extensions(extensions, config_file) unless extensions.is_a?(Hash) accept(Issues::EXTENSIONS_NOT_HASH, config_file, :actual => extensions.class.name) return end # check known extensions extensions.each_key do |key| unless ['scheme_handlers'].include? key accept(Issues::UNKNOWN_EXTENSION, config_file, :extension => key) end end if binding_schemes = extensions['scheme_handlers'] unless binding_schemes.is_a?(Hash) accept(Issues::EXTENSION_BINDING_NOT_HASH, config_file, :extension => 'scheme_handlers', :actual => binding_schemes.class.name) end end end end end diff --git a/lib/puppet/pops/binder/config/issues.rb b/lib/puppet/pops/binder/config/issues.rb index 11e37fb32..b73d550b2 100644 --- a/lib/puppet/pops/binder/config/issues.rb +++ b/lib/puppet/pops/binder/config/issues.rb @@ -1,86 +1,90 @@ module Puppet::Pops::Binder::Config::Issues # (see Puppet::Pops::Issues#issue) def self.issue (issue_code, *args, &block) Puppet::Pops::Issues.issue(issue_code, *args, &block) end CONFIG_FILE_NOT_FOUND = issue :CONFIG_FILE_NOT_FOUND do "The binder configuration file: #{semantic} can not be found." end CONFIG_FILE_SYNTAX_ERROR = issue :CONFIG_FILE_SYNTAX_ERROR, :detail do "Syntax error in configuration file: #{detail}" end CONFIG_IS_NOT_HASH = issue :CONFIG_IS_NOT_HASH do "The configuration file '#{semantic}' has no hash at the top level" end CONFIG_LAYERS_MISSING = issue :CONFIG_LAYERS_MISSING do "The configuration file '#{semantic}' has no 'layers' entry in the top level hash" end + CONFIG_CATEGORIES_MISSING = issue :CONFIG_CATEGORIES_MISSING do + "The configuration file '#{semantic}' has no 'categories' entry in the top level hash" + end + CONFIG_VERSION_MISSING = issue :CONFIG_VERSION_MISSING do "The configuration file '#{semantic}' has no 'version' entry in the top level hash" end LAYERS_IS_NOT_ARRAY = issue :LAYERS_IS_NOT_ARRAY, :klass do "The configuration file '#{semantic}' should contain a 'layers' key with an Array value, got: #{klass.name}" end LAYER_IS_NOT_HASH = issue :LAYER_IS_NOT_HASH, :klass do "The configuration file '#{semantic}' should contain one hash per layer, got #{klass.name} instead of Hash" end DUPLICATE_LAYER_NAME = issue :DUPLICATE_LAYER_NAME, :name do "Duplicate layer '#{name}' in configuration file #{semantic}" end UNKNOWN_LAYER_ATTRIBUTE = issue :UNKNOWN_LAYER_ATTRIBUTE, :name do "Unknown layer attribute '#{name}' in configuration file #{semantic}" end BINDINGS_REF_NOT_STRING_OR_ARRAY = issue :BINDINGS_REF_NOT_STRING_OR_ARRAY, :kind do "Configuration file #{semantic} has bindings reference in '#{kind}' that is neither a String nor an Array." end MISSING_SCHEME = issue :MISSING_SCHEME, :uri do "Configuration file #{semantic} contains a bindings reference: '#{uri}' without scheme." end UNKNOWN_REF_SCHEME = issue :UNKNOWN_REF_SCHEME, :uri, :kind do "Configuration file #{semantic} contains a bindings reference: '#{kind}' => '#{uri}' with unknown scheme" end REF_WITHOUT_PATH = issue :REF_WITHOUT_PATH, :uri, :kind do "Configuration file #{semantic} contains a bindings reference: '#{kind}' => '#{uri}' without path" end BINDINGS_REF_INVALID_URI = issue :BINDINGS_REF_INVALID_URI, :msg do "Configuration file #{semantic} contains a bindings reference: '#{kind}' => invalid uri, msg: '#{msg}'" end LAYER_ATTRIBUTE_IS_SYMBOL = issue :LAYER_ATTRIBUTE_IS_SYMBOL, :name do "Configuration file #{semantic} contains a layer attribute '#{name}' that is a Symbol (should be String)" end LAYER_NAME_NOT_STRING = issue :LAYER_NAME_NOT_STRING, :class_name do "Configuration file #{semantic} contains a layer name that is not a String, got a: '#{class_name}'" end CONFIG_WRONG_VERSION = issue :CONFIG_WRONG_VERSION, :expected, :actual do "The configuration file '#{semantic}' has unsupported 'version', expected: #{expected}, but got: #{actual}." end EXTENSIONS_NOT_HASH = issue :EXTENSIONS_NOT_HASH, :actual do "The configuration file '#{semantic}' contains 'extensions', expected: Hash, but got: #{actual}." end EXTENSION_BINDING_NOT_HASH = issue :EXTENSION_BINDING_NOT_HASH, :extension, :actual do "The configuration file '#{semantic}' contains '#{extension}', expected: Hash, but got: #{actual}." end UNKNOWN_EXTENSION = issue :UNKNOWN_EXTENSION, :extension do "The configuration file '#{semantic}' contains the unknown extension: #{extension}." end end diff --git a/lib/puppet/pops/model/ast_tree_dumper.rb b/lib/puppet/pops/model/ast_tree_dumper.rb index 00c4ae40a..9008b1ccd 100644 --- a/lib/puppet/pops/model/ast_tree_dumper.rb +++ b/lib/puppet/pops/model/ast_tree_dumper.rb @@ -1,386 +1,386 @@ require 'puppet/parser/ast' # Dumps a Pops::Model in reverse polish notation; i.e. LISP style # The intention is to use this for debugging output # TODO: BAD NAME - A DUMP is a Ruby Serialization # class Puppet::Pops::Model::AstTreeDumper < Puppet::Pops::Model::TreeDumper AST = Puppet::Parser::AST Model = Puppet::Pops::Model def dump_LiteralFloat o o.value.to_s end def dump_LiteralInteger o case o.radix when 10 o.value.to_s when 8 "0%o" % o.value when 16 "0x%X" % o.value else "bad radix:" + o.value.to_s end end def dump_Expression(o) "(pops-expression #{Puppet::Pops::Model::ModelTreeDumper.new().dump(o.value)})" end def dump_Factory o do_dump(o.current) end def dump_ArithmeticOperator o [o.operator.to_s, do_dump(o.lval), do_dump(o.rval)] end def dump_Relationship o [o.arrow.to_s, do_dump(o.left), do_dump(o.right)] end # Hostname is tricky, it is either a bare word, a string, or default, or regular expression # Least evil, all strings except default are quoted def dump_HostName o result = do_dump o.value unless o.value.is_a? AST::Regex result = result == "default" ? ":default" : "'#{result}'" end result end # x[y] prints as (slice x y) def dump_HashOrArrayAccess o var = o.variable.is_a?(String) ? "$#{o.variable}" : do_dump(o.variable) ["slice", var, do_dump(o.key)] end # The AST Collection knows about exported or virtual query, not the query. def dump_Collection o result = ["collect", do_dump(o.type), :indent, :break] if o.form == :virtual q = ["<| |>"] else q = ["<<| |>>"] end q << do_dump(o.query) unless is_nop?(o.query) q << :indent result << q o.override do |ao| result << :break << do_dump(ao) end result += [:dedent, :dedent ] result end def dump_CollExpr o operator = case o.oper when 'and' '&&' when 'or' '||' else o.oper end [operator, do_dump(o.test1), do_dump(o.test2)] end def dump_ComparisonOperator o [o.operator.to_s, do_dump(o.lval), do_dump(o.rval)] end def dump_Boolean o o.to_s end def dump_BooleanOperator o operator = o.operator == 'and' ? '&&' : '||' [operator, do_dump(o.lval), do_dump(o.rval)] end def dump_InOperator o ["in", do_dump(o.lval), do_dump(o.rval)] end # $x = ... # $x += ... # def dump_VarDef o operator = o.append ? "+=" : "=" [operator, '$' + do_dump(o.name), do_dump(o.value)] end # Produces (name => expr) or (name +> expr) def dump_ResourceParam o operator = o.add ? "+>" : "=>" [do_dump(o.param), operator, do_dump(o.value)] end def dump_Array o o.collect {|e| do_dump(e) } end def dump_ASTArray o ["[]"] + o.children.collect {|x| do_dump(x)} end def dump_ASTHash o ["{}"] + o.value.sort_by{|k,v| k.to_s}.collect {|x| [do_dump(x[0]), do_dump(x[1])]} # ["{}"] + o.value.collect {|x| [do_dump(x[0]), do_dump(x[1])]} end def dump_MatchOperator o [o.operator.to_s, do_dump(o.lval), do_dump(o.rval)] end # Dump a Ruby String in single quotes unless it is a number. def dump_String o if o.is_a? String o # A Ruby String, not quoted elsif Puppet::Pops::Utils.to_n(o.value) o.value # AST::String that is a number without quotes else "'#{o.value}'" # AST::String that is not a number end end def dump_Lambda o result = ["lambda"] result << ["parameters"] + o.parameters.collect {|p| _dump_ParameterArray(p) } if o.parameters.size() > 0 if o.children == [] result << [] # does not have a lambda body else result << do_dump(o.children) end result end def dump_Default o ":default" end def dump_Undef o ":undef" end # Note this is Regex (the AST kind), not Ruby Regexp def dump_Regex o "/#{o.value.source}/" end def dump_Nop o ":nop" end def dump_NilClass o "()" end def dump_Not o ['!', dump(o.value)] end def dump_Variable o "$#{dump(o.value)}" end def dump_Minus o ['-', do_dump(o.value)] end def dump_BlockExpression o ["block"] + o.children.collect {|x| do_dump(x) } end # Interpolated strings are shown as (cat seg0 seg1 ... segN) def dump_Concat o ["cat"] + o.value.collect {|x| x.is_a?(AST::String) ? " "+do_dump(x) : ["str", do_dump(x)]} end def dump_Hostclass o # ok, this is kind of crazy stuff in the AST, information in a context instead of in AST, and # parameters are in a Ruby Array with each parameter being an Array... # context = o.context args = context[:arguments] parent = context[:parent] result = ["class", o.name] result << ["inherits", parent] if parent result << ["parameters"] + args.collect {|p| _dump_ParameterArray(p) } if args && args.size() > 0 if is_nop?(o.code) result << [] else result << do_dump(o.code) end result end def dump_Name o o.value end def dump_Node o context = o.context parent = context[:parent] code = context[:code] result = ["node"] result << ["matches"] + o.names.collect {|m| do_dump(m) } result << ["parent", do_dump(parent)] if !is_nop?(parent) if is_nop?(code) result << [] else result << do_dump(code) end result end def dump_Definition o # ok, this is even crazier that Hostclass. The name of the define does not have an accessor # and some things are in the context (but not the name). Parameters are called arguments and they # are in a Ruby Array where each parameter is an array of 1 or 2 elements. # context = o.context name = o.instance_variable_get("@name") args = context[:arguments] code = context[:code] result = ["define", name] result << ["parameters"] + args.collect {|p| _dump_ParameterArray(p) } if args && args.size() > 0 if is_nop?(code) result << [] else result << do_dump(code) end result end def dump_ResourceReference o result = ["slice", do_dump(o.type)] if o.title.children.size == 1 result << do_dump(o.title[0]) else result << do_dump(o.title.children) end result end def dump_ResourceOverride o result = ["override", do_dump(o.object), :indent] o.parameters.each do |p| result << :break << do_dump(p) end result << :dedent result end # Puppet AST encodes a parameter as a one or two slot Array. # This is not a polymorph dump method. # def _dump_ParameterArray o if o.size == 2 ["=", o[0], do_dump(o[1])] else o[0] end end def dump_IfStatement o result = ["if", do_dump(o.test), :indent, :break, ["then", :indent, do_dump(o.statements), :dedent]] result += [:break, ["else", :indent, do_dump(o.else), :dedent], :dedent] unless is_nop? o.else result end # Produces (invoke name args...) when not required to produce an rvalue, and # (call name args ... ) otherwise. # def dump_Function o # somewhat ugly as Function hides its "ftype" instance variable result = [o.instance_variable_get("@ftype") == :rvalue ? "call" : "invoke", do_dump(o.name)] o.arguments.collect {|a| result << do_dump(a) } result << do_dump(o.pblock) if o.pblock result end def dump_MethodCall o # somewhat ugly as Method call (does the same as function) and hides its "ftype" instance variable result = [o.instance_variable_get("@ftype") == :rvalue ? "call-method" : "invoke-method", [".", do_dump(o.receiver), do_dump(o.name)]] o.arguments.collect {|a| result << do_dump(a) } result << do_dump(o.lambda) if o.lambda result end def dump_CaseStatement o result = ["case", do_dump(o.test), :indent] o.options.each do |s| result << :break << do_dump(s) end result << :dedent end def dump_CaseOpt o result = ["when"] result << o.value.collect {|x| do_dump(x) } # A bit of trickery to get it into the same shape as Pops output if is_nop?(o.statements) result << ["then", []] # Puppet AST has a nop if there is no body else result << ["then", do_dump(o.statements) ] end result end def dump_ResourceInstance o result = [do_dump(o.title), :indent] o.parameters.each do |p| result << :break << do_dump(p) end result << :dedent result end def dump_ResourceDefaults o result = ["resource-defaults", do_dump(o.type), :indent] o.parameters.each do |p| result << :break << do_dump(p) end result << :dedent result end def dump_Resource o if o.exported form = 'exported-' elsif o.virtual form = 'virtual-' else form = '' end result = [form+"resource", do_dump(o.type), :indent] o.instances.each do |b| result << :break << do_dump(b) end result << :dedent result end def dump_Selector o values = o.values - values = [values] unless values.instance_of? AST::ASTArray or values.instance_of? Array + values = [values] unless values.instance_of?(AST::ASTArray) || values.instance_of?(Array) ["?", do_dump(o.param)] + values.collect {|x| do_dump(x) } end def dump_Object o ['dev-error-no-polymorph-dump-for:', o.class.to_s, o.to_s] end def is_nop? o o.nil? || o.is_a?(Model::Nop) || o.is_a?(AST::Nop) end end diff --git a/lib/puppet/pops/model/model.rb b/lib/puppet/pops/model/model.rb index 9e2a079b4..f8dcd1f4d 100644 --- a/lib/puppet/pops/model/model.rb +++ b/lib/puppet/pops/model/model.rb @@ -1,114 +1,114 @@ # # The Puppet Pops Metamodel Implementation # # The Puppet Pops Metamodel consists of two parts; the metamodel expressed with RGen in model_meta.rb, # and this file which mixes in implementation details. # require 'rgen/metamodel_builder' require 'rgen/ecore/ecore' require 'rgen/ecore/ecore_ext' require 'rgen/ecore/ecore_to_ruby' module Puppet::Pops require 'puppet/pops/model/model_meta' # TODO: See PUP-2978 for possible performance optimization # Mix in implementation into the generated code module Model class PopsObject include Puppet::Pops::Visitable include Puppet::Pops::Adaptable include Puppet::Pops::Containment end class LocatableExpression module ClassModule # Go through the gymnastics of making either value or pattern settable # with synchronization to the other form. A derived value cannot be serialized # and we want to serialize the pattern. When recreating the object we need to # recreate it from the pattern string. # The below sets both values if one is changed. # def locator unless result = getLocator setLocator(result = Puppet::Pops::Parser::Locator.locator(source_text, source_ref(), line_offsets)) end result end end end class SubLocatedExpression module ClassModule def locator unless result = getLocator # Adapt myself to get the Locator for me adapter = Puppet::Pops::Adapters::SourcePosAdapter.adapt(self) # Get the program (root), and deal with case when not contained in a program program = eAllContainers.find {|c| c.is_a?(Program) } source_ref = program.nil? ? '' : program.source_ref # An outer locator is needed since SubLocator only deals with offsets. This outer locator # has 0,0 as origin. outer_locator = Puppet::Pops::Parser::Locator.locator(adpater.extract_text, source_ref, line_offsets) # Create a sublocator that describes an offset from the outer # NOTE: the offset of self is the same as the sublocator's leading_offset result = Puppet::Pops::Parser::Locator::SubLocator.new(outer_locator, leading_line_count, offset, leading_line_offset) setLocator(result) end result end end end class LiteralRegularExpression module ClassModule # Go through the gymnastics of making either value or pattern settable # with synchronization to the other form. A derived value cannot be serialized # and we want to serialize the pattern. When recreating the object we need to # recreate it from the pattern string. # The below sets both values if one is changed. # def value= regexp setValue regexp setPattern regexp.to_s end def pattern= regexp_string setPattern regexp_string setValue Regexp.new(regexp_string) end end end class AbstractResource module ClassModule def virtual_derived form == :virtual || form == :exported end def exported_derived form == :exported end end end class Program < PopsObject module ClassModule def locator unless result = getLocator - setLocator(result = Puppet::Pops::Parser::Locator.locator(source_text, source_ref(), line_offsets)) + setLocator(result = Puppet::Pops::Parser::Locator.locator(source_text, source_ref(), line_offsets, char_offsets)) end result end end end end end diff --git a/lib/puppet/pops/model/model_meta.rb b/lib/puppet/pops/model/model_meta.rb index 246216aa9..788b6949d 100644 --- a/lib/puppet/pops/model/model_meta.rb +++ b/lib/puppet/pops/model/model_meta.rb @@ -1,576 +1,582 @@ # # The Puppet Pops Metamodel # # This module contains a formal description of the Puppet Pops (*P*uppet *OP*eration instruction*S*). # It describes a Metamodel containing DSL instructions, a description of PuppetType and related # classes needed to evaluate puppet logic. # The metamodel resembles the existing AST model, but it is a semantic model of instructions and # the types that they operate on rather than an Abstract Syntax Tree, although closely related. # # The metamodel is anemic (has no behavior) except basic datatype and type # assertions and reference/containment assertions. # The metamodel is also a generalized description of the Puppet DSL to enable the # same metamodel to be used to express Puppet DSL models (instances) with different semantics as # the language evolves. # # The metamodel is concretized by a validator for a particular version of # the Puppet DSL language. # # This metamodel is expressed using RGen. # require 'rgen/metamodel_builder' module Puppet::Pops::Model extend RGen::MetamodelBuilder::ModuleExtension # A base class for modeled objects that makes them Visitable, and Adaptable. # class PopsObject < RGen::MetamodelBuilder::MMBase abstract end # A Positioned object has an offset measured in an opaque unit (representing characters) from the start # of a source text (starting # from 0), and a length measured in the same opaque unit. The resolution of the opaque unit requires the # aid of a Locator instance that knows about the measure. This information is stored in the model's # root node - a Program. # # The offset and length are optional if the source of the model is not from parsed text. # class Positioned < PopsObject abstract has_attr 'offset', Integer has_attr 'length', Integer end # @abstract base class for expressions class Expression < Positioned abstract end # A Nop - the "no op" expression. # @note not really needed since the evaluator can evaluate nil with the meaning of NoOp # @todo deprecate? May be useful if there is the need to differentiate between nil and Nop when transforming model. # class Nop < Expression end # A binary expression is abstract and has a left and a right expression. The order of evaluation # and semantics are determined by the concrete subclass. # class BinaryExpression < Expression abstract # # @!attribute [rw] left_expr # @return [Expression] contains_one_uni 'left_expr', Expression, :lowerBound => 1 contains_one_uni 'right_expr', Expression, :lowerBound => 1 end # An unary expression is abstract and contains one expression. The semantics are determined by # a concrete subclass. # class UnaryExpression < Expression abstract contains_one_uni 'expr', Expression, :lowerBound => 1 end # A class that simply evaluates to the contained expression. # It is of value in order to preserve user entered parentheses in transformations, and # transformations from model to source. # class ParenthesizedExpression < UnaryExpression; end # A boolean not expression, reversing the truth of the unary expr. # class NotExpression < UnaryExpression; end # An arithmetic expression reversing the polarity of the numeric unary expr. # class UnaryMinusExpression < UnaryExpression; end # Unfolds an array (a.k.a 'splat') class UnfoldExpression < UnaryExpression; end OpAssignment = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'=', :'+=', :'-='], :name => 'OpAssignment') # An assignment expression assigns a value to the lval() of the left_expr. # class AssignmentExpression < BinaryExpression has_attr 'operator', OpAssignment, :lowerBound => 1 end OpArithmetic = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'+', :'-', :'*', :'%', :'/', :'<<', :'>>' ], :name => 'OpArithmetic') # An arithmetic expression applies an arithmetic operator on left and right expressions. # class ArithmeticExpression < BinaryExpression has_attr 'operator', OpArithmetic, :lowerBound => 1 end OpRelationship = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'->', :'<-', :'~>', :'<~'], :name => 'OpRelationship') # A relationship expression associates the left and right expressions # class RelationshipExpression < BinaryExpression has_attr 'operator', OpRelationship, :lowerBound => 1 end # A binary expression, that accesses the value denoted by right in left. i.e. typically # expressed concretely in a language as left[right]. # class AccessExpression < Expression contains_one_uni 'left_expr', Expression, :lowerBound => 1 contains_many_uni 'keys', Expression, :lowerBound => 1 end OpComparison = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'==', :'!=', :'<', :'>', :'<=', :'>=' ], :name => 'OpComparison') # A comparison expression compares left and right using a comparison operator. # class ComparisonExpression < BinaryExpression has_attr 'operator', OpComparison, :lowerBound => 1 end OpMatch = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'!~', :'=~'], :name => 'OpMatch') # A match expression matches left and right using a matching operator. # class MatchExpression < BinaryExpression has_attr 'operator', OpMatch, :lowerBound => 1 end # An 'in' expression checks if left is 'in' right # class InExpression < BinaryExpression; end # A boolean expression applies a logical connective operator (and, or) to left and right expressions. # class BooleanExpression < BinaryExpression abstract end # An and expression applies the logical connective operator and to left and right expression # and does not evaluate the right expression if the left expression is false. # class AndExpression < BooleanExpression; end # An or expression applies the logical connective operator or to the left and right expression # and does not evaluate the right expression if the left expression is true # class OrExpression < BooleanExpression; end # A literal list / array containing 0:M expressions. # class LiteralList < Expression contains_many_uni 'values', Expression end # A Keyed entry has a key and a value expression. It is typically used as an entry in a Hash. # class KeyedEntry < Positioned contains_one_uni 'key', Expression, :lowerBound => 1 contains_one_uni 'value', Expression, :lowerBound => 1 end # A literal hash is a collection of KeyedEntry objects # class LiteralHash < Expression contains_many_uni 'entries', KeyedEntry end # A block contains a list of expressions # class BlockExpression < Expression contains_many_uni 'statements', Expression end # A case option entry in a CaseStatement # class CaseOption < Expression contains_many_uni 'values', Expression, :lowerBound => 1 contains_one_uni 'then_expr', Expression, :lowerBound => 1 end # A case expression has a test, a list of options (multi values => block map). # One CaseOption may contain a LiteralDefault as value. This option will be picked if nothing # else matched. # class CaseExpression < Expression contains_one_uni 'test', Expression, :lowerBound => 1 contains_many_uni 'options', CaseOption end # A query expression is an expression that is applied to some collection. # The contained optional expression may contain different types of relational expressions depending # on what the query is applied to. # class QueryExpression < Expression abstract contains_one_uni 'expr', Expression, :lowerBound => 0 end # An exported query is a special form of query that searches for exported objects. # class ExportedQuery < QueryExpression end # A virtual query is a special form of query that searches for virtual objects. # class VirtualQuery < QueryExpression end OpAttribute = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'=>', :'+>', ], :name => 'OpAttribute') class AbstractAttributeOperation < Positioned end # An attribute operation sets or appends a value to a named attribute. # class AttributeOperation < AbstractAttributeOperation has_attr 'attribute_name', String, :lowerBound => 1 has_attr 'operator', OpAttribute, :lowerBound => 1 contains_one_uni 'value_expr', Expression, :lowerBound => 1 end # An attribute operation containing an expression that must evaluate to a Hash # class AttributesOperation < AbstractAttributeOperation contains_one_uni 'expr', Expression, :lowerBound => 1 end # An object that collects stored objects from the central cache and returns # them to the current host. Operations may optionally be applied. # class CollectExpression < Expression contains_one_uni 'type_expr', Expression, :lowerBound => 1 contains_one_uni 'query', QueryExpression, :lowerBound => 1 contains_many_uni 'operations', AttributeOperation end class Parameter < Positioned has_attr 'name', String, :lowerBound => 1 contains_one_uni 'value', Expression contains_one_uni 'type_expr', Expression, :lowerBound => 0 has_attr 'captures_rest', Boolean end # Abstract base class for definitions. # class Definition < Expression abstract end # Abstract base class for named and parameterized definitions. class NamedDefinition < Definition abstract has_attr 'name', String, :lowerBound => 1 contains_many_uni 'parameters', Parameter contains_one_uni 'body', Expression end # A resource type definition (a 'define' in the DSL). # class ResourceTypeDefinition < NamedDefinition end # A node definition matches hosts using Strings, or Regular expressions. It may inherit from # a parent node (also using a String or Regular expression). # class NodeDefinition < Definition contains_one_uni 'parent', Expression contains_many_uni 'host_matches', Expression, :lowerBound => 1 contains_one_uni 'body', Expression end class LocatableExpression < Expression has_many_attr 'line_offsets', Integer has_attr 'locator', Object, :lowerBound => 1, :transient => true end # Contains one expression which has offsets reported virtually (offset against the Program's # overall locator). # class SubLocatedExpression < Expression contains_one_uni 'expr', Expression, :lowerBound => 1 # line offset index for contained expressions has_many_attr 'line_offsets', Integer # Number of preceding lines (before the line_offsets) has_attr 'leading_line_count', Integer # The offset of the leading source line (i.e. size of "left margin"). has_attr 'leading_line_offset', Integer # The locator for the sub-locatable's children (not for the sublocator itself) # The locator is not serialized and is recreated on demand from the indexing information # in self. # has_attr 'locator', Object, :lowerBound => 1, :transient => true end # A heredoc is a wrapper around a LiteralString or a ConcatenatedStringExpression with a specification # of syntax. The expectation is that "syntax" has meaning to a validator. A syntax of nil or '' means # "unspecified syntax". # class HeredocExpression < Expression has_attr 'syntax', String contains_one_uni 'text_expr', Expression, :lowerBound => 1 end # A class definition # class HostClassDefinition < NamedDefinition has_attr 'parent_class', String end # i.e {|parameters| body } class LambdaExpression < Expression contains_many_uni 'parameters', Parameter contains_one_uni 'body', Expression end # If expression. If test is true, the then_expr part should be evaluated, else the (optional) # else_expr. An 'elsif' is simply an else_expr = IfExpression, and 'else' is simply else == Block. # a 'then' is typically a Block. # class IfExpression < Expression contains_one_uni 'test', Expression, :lowerBound => 1 contains_one_uni 'then_expr', Expression, :lowerBound => 1 contains_one_uni 'else_expr', Expression end # An if expression with boolean reversed test. # class UnlessExpression < IfExpression end # An abstract call. # class CallExpression < Expression abstract # A bit of a crutch; functions are either procedures (void return) or has an rvalue # this flag tells the evaluator that it is a failure to call a function that is void/procedure # where a value is expected. # has_attr 'rval_required', Boolean, :defaultValueLiteral => "false" contains_one_uni 'functor_expr', Expression, :lowerBound => 1 contains_many_uni 'arguments', Expression contains_one_uni 'lambda', Expression end # A function call where the functor_expr should evaluate to something callable. # class CallFunctionExpression < CallExpression; end # A function call where the given functor_expr should evaluate to the name # of a function. # class CallNamedFunctionExpression < CallExpression; end # A method/function call where the function expr is a NamedAccess and with support for # an optional lambda block # class CallMethodExpression < CallExpression end # Abstract base class for literals. # class Literal < Expression abstract end # A literal value is an abstract value holder. The type of the contained value is # determined by the concrete subclass. # class LiteralValue < Literal abstract end # A Regular Expression Literal. # class LiteralRegularExpression < LiteralValue has_attr 'value', Object, :lowerBound => 1, :transient => true has_attr 'pattern', String, :lowerBound => 1 end # A Literal String # class LiteralString < LiteralValue has_attr 'value', String, :lowerBound => 1 end class LiteralNumber < LiteralValue abstract end # A literal number has a radix of decimal (10), octal (8), or hex (16) to enable string conversion with the input radix. # By default, a radix of 10 is used. # class LiteralInteger < LiteralNumber has_attr 'radix', Integer, :lowerBound => 1, :defaultValueLiteral => "10" has_attr 'value', Integer, :lowerBound => 1 end class LiteralFloat < LiteralNumber has_attr 'value', Float, :lowerBound => 1 end # The DSL `undef`. # class LiteralUndef < Literal; end # The DSL `default` class LiteralDefault < Literal; end # DSL `true` or `false` class LiteralBoolean < LiteralValue has_attr 'value', Boolean, :lowerBound => 1 end # A text expression is an interpolation of an expression. If the embedded expression is # a QualifiedName, it is taken as a variable name and resolved. All other expressions are evaluated. # The result is transformed to a string. # class TextExpression < UnaryExpression; end # An interpolated/concatenated string. The contained segments are expressions. Verbatim sections # should be LiteralString instances, and interpolated expressions should either be # TextExpression instances (if QualifiedNames should be turned into variables), or any other expression # if such treatment is not needed. # class ConcatenatedString < Expression contains_many_uni 'segments', Expression end # A DSL NAME (one or multiple parts separated by '::'). # class QualifiedName < LiteralValue has_attr 'value', String, :lowerBound => 1 end # Represents a parsed reserved word class ReservedWord < LiteralValue has_attr 'word', String, :lowerBound => 1 end # A DSL CLASSREF (one or multiple parts separated by '::' where (at least) the first part starts with an upper case letter). # class QualifiedReference < LiteralValue has_attr 'value', String, :lowerBound => 1 end # A Variable expression looks up value of expr (some kind of name) in scope. # The expression is typically a QualifiedName, or QualifiedReference. # class VariableExpression < UnaryExpression; end # Epp start class EppExpression < Expression # EPP can be specified without giving any parameter specification. # However, the parameters of the lambda in that case are the empty # array, which is the same as when the parameters are explicity # specified as empty. This attribute tracks that difference. has_attr 'parameters_specified', Boolean contains_one_uni 'body', Expression end # A string to render class RenderStringExpression < LiteralString end # An expression to evluate and render class RenderExpression < UnaryExpression end # A resource body describes one resource instance # class ResourceBody < Positioned contains_one_uni 'title', Expression contains_many_uni 'operations', AbstractAttributeOperation end ResourceFormEnum = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:regular, :virtual, :exported ], :name => 'ResourceFormEnum') # An abstract resource describes the form of the resource (regular, virtual or exported) # and adds convenience methods to ask if it is virtual or exported. # All derived classes may not support all forms, and these needs to be validated # class AbstractResource < Expression abstract has_attr 'form', ResourceFormEnum, :lowerBound => 1, :defaultValueLiteral => "regular" has_attr 'virtual', Boolean, :derived => true has_attr 'exported', Boolean, :derived => true end # A resource expression is used to instantiate one or many resource. Resources may optionally # be virtual or exported, an exported resource is always virtual. # class ResourceExpression < AbstractResource contains_one_uni 'type_name', Expression, :lowerBound => 1 contains_many_uni 'bodies', ResourceBody end # A resource defaults sets defaults for a resource type. This class inherits from AbstractResource # but does only support the :regular form (this is intentional to be able to produce better error messages # when illegal forms are applied to a model. # class ResourceDefaultsExpression < AbstractResource contains_one_uni 'type_ref', Expression contains_many_uni 'operations', AbstractAttributeOperation end # A resource override overrides already set values. # class ResourceOverrideExpression < AbstractResource contains_one_uni 'resources', Expression, :lowerBound => 1 contains_many_uni 'operations', AbstractAttributeOperation end # A selector entry describes a map from matching_expr to value_expr. # class SelectorEntry < Positioned contains_one_uni 'matching_expr', Expression, :lowerBound => 1 contains_one_uni 'value_expr', Expression, :lowerBound => 1 end # A selector expression represents a mapping from a left_expr to a matching SelectorEntry. # class SelectorExpression < Expression contains_one_uni 'left_expr', Expression, :lowerBound => 1 contains_many_uni 'selectors', SelectorEntry end # A named access expression looks up a named part. (e.g. $a.b) # class NamedAccessExpression < BinaryExpression; end # A Program is the top level construct returned by the parser # it contains the parsed result in the body, and has a reference to the full source text, - # and its origin. The line_offset's is an array with the start offset of each line. + # and its origin. The line_offset's is an array with the start offset of each line measured + # in bytes or characters (as given by the attribute char_offsets). The `char_offsets` setting + # applies to all offsets recorded in the mode (not just the line_offsets). # + # A model that will be shared across different platforms should use char_offsets true as the byte + # offsets are platform and encoding dependent. + # class Program < PopsObject contains_one_uni 'body', Expression has_many 'definitions', Definition has_attr 'source_text', String has_attr 'source_ref', String has_many_attr 'line_offsets', Integer + has_attr 'char_offsets', Boolean, :defaultValueLiteral => 'false' has_attr 'locator', Object, :lowerBound => 1, :transient => true end end diff --git a/lib/puppet/pops/parser/evaluating_parser.rb b/lib/puppet/pops/parser/evaluating_parser.rb index 596f549bc..11e83163d 100644 --- a/lib/puppet/pops/parser/evaluating_parser.rb +++ b/lib/puppet/pops/parser/evaluating_parser.rb @@ -1,140 +1,140 @@ # Does not support "import" and parsing ruby files # class Puppet::Pops::Parser::EvaluatingParser attr_reader :parser def initialize() @parser = Puppet::Pops::Parser::Parser.new() end def parse_string(s, file_source = 'unknown') @file_source = file_source clear() # Handling of syntax error can be much improved (in general), now it bails out of the parser # and does not have as rich information (when parsing a string), need to update it with the file source # (ideally, a syntax error should be entered as an issue, and not just thrown - but that is a general problem # and an improvement that can be made in the eparser (rather than here). # Also a possible improvement (if the YAML parser returns positions) is to provide correct output of position. # begin assert_and_report(parser.parse_string(s)) rescue Puppet::ParseError => e # TODO: This is not quite right, why does not the exception have the correct file? e.file = @file_source unless e.file.is_a?(String) && !e.file.empty? raise e end end def parse_file(file) @file_source = file clear() assert_and_report(parser.parse_file(file)) end def evaluate_string(scope, s, file_source='unknown') evaluate(scope, parse_string(s, file_source)) end def evaluate_file(file) evaluate(parse_file(file)) end def clear() @acceptor = nil end # Create a closure that can be called in the given scope def closure(model, scope) Puppet::Pops::Evaluator::Closure.new(evaluator, model, scope) end def evaluate(scope, model) return nil unless model evaluator.evaluate(model, scope) end def evaluator @@evaluator ||= Puppet::Pops::Evaluator::EvaluatorImpl.new() @@evaluator end def validate(parse_result) resulting_acceptor = acceptor() validator(resulting_acceptor).validate(parse_result) resulting_acceptor end def acceptor() Puppet::Pops::Validation::Acceptor.new end def validator(acceptor) Puppet::Pops::Validation::ValidatorFactory_4_0.new().validator(acceptor) end def assert_and_report(parse_result) return nil unless parse_result - if parse_result.source_ref.nil? or parse_result.source_ref == '' + if parse_result.source_ref.nil? || parse_result.source_ref == '' parse_result.source_ref = @file_source end validation_result = validate(parse_result) Puppet::Pops::IssueReporter.assert_and_report(validation_result, :emit_warnings => true) parse_result end def quote(x) self.class.quote(x) end # Translates an already parsed string that contains control characters, quotes # and backslashes into a quoted string where all such constructs have been escaped. # Parsing the return value of this method using the puppet parser should yield # exactly the same string as the argument passed to this method # # The method makes an exception for the two character sequences \$ and \s. They # will not be escaped since they have a special meaning in puppet syntax. # # TODO: Handle \uXXXX characters ?? # # @param x [String] The string to quote and "unparse" # @return [String] The quoted string # def self.quote(x) escaped = '"' p = nil x.each_char do |c| case p when nil # do nothing when "\t" escaped << '\\t' when "\n" escaped << '\\n' when "\f" escaped << '\\f' # TODO: \cx is a range of characters - skip for now # when "\c" # escaped << '\\c' when '"' escaped << '\\"' when '\\' escaped << if c == '$' || c == 's'; p; else '\\\\'; end # don't escape \ when followed by s or $ else escaped << p end p = c end escaped << p unless p.nil? escaped << '"' end class EvaluatingEppParser < Puppet::Pops::Parser::EvaluatingParser def initialize() @parser = Puppet::Pops::Parser::EppParser.new() end end end diff --git a/lib/puppet/pops/parser/locator.rb b/lib/puppet/pops/parser/locator.rb index c46c38ee9..b62b69cbc 100644 --- a/lib/puppet/pops/parser/locator.rb +++ b/lib/puppet/pops/parser/locator.rb @@ -1,291 +1,295 @@ # Helper class that keeps track of where line breaks are located and can answer questions about positions. # class Puppet::Pops::Parser::Locator RUBY_1_9_3 = (1 << 16 | 9 << 8 | 3) RUBY_2_0_0 = (2 << 16 | 0 << 8 | 0) RUBYVER_ARRAY = RUBY_VERSION.split(".").collect {|s| s.to_i } RUBYVER = (RUBYVER_ARRAY[0] << 16 | RUBYVER_ARRAY[1] << 8 | RUBYVER_ARRAY[2]) # Computes a symbol representing which ruby runtime this is running on # This implementation will fail if there are more than 255 minor or micro versions of ruby # def self.locator_version if RUBYVER >= RUBY_2_0_0 :ruby20 elsif RUBYVER >= RUBY_1_9_3 :ruby19 else :ruby18 end end LOCATOR_VERSION = locator_version # Constant set to true if multibyte is supported (includes multibyte extended regular expressions) MULTIBYTE = !!(LOCATOR_VERSION == :ruby19 || LOCATOR_VERSION == :ruby20) # Creates, or recreates a Locator. A Locator is created if index is not given (a scan is then # performed of the given source string. # - def self.locator(string, file, index = nil) - case LOCATOR_VERSION - when :ruby20, :ruby19 - Locator19.new(string, file, index) + def self.locator(string, file, index = nil, char_offsets = false) + if(char_offsets) + LocatorForChars.new(string, file, index); else - Locator18.new(string, file, index) + case LOCATOR_VERSION + when :ruby20, :ruby19 + Locator19.new(string, file, index) + else + LocatorForChars.new(string, file, index) + end end end # Returns the file name associated with the string content def file end # Returns the string content def string end # Returns the position on line (first position on a line is 1) def pos_on_line(offset) end # Returns the line number (first line is 1) for the given offset def line_for_offset(offset) end # Returns the offset on line (first offset on a line is 0). # def offset_on_line(offset) end # Returns the character offset for a given reported offset def char_offset(byte_offset) end # Returns the length measured in number of characters from the given start and end reported offset def char_length(offset, end_offset) end # Returns the line index - an array of line offsets for the start position of each line, starting at 0 for # the first line. # def line_index() end # A Sublocator locates a concrete locator (subspace) in a virtual space. # The `leading_line_count` is the (virtual) number of lines preceding the first line in the concrete locator. # The `leading_offset` is the (virtual) byte offset of the first byte in the concrete locator. # The `leading_line_offset` is the (virtual) offset / margin in characters for each line. # # This illustrates characters in the sublocator (`.`) inside the subspace (`X`): # # 1:XXXXXXXX # 2:XXXX.... .. ... .. # 3:XXXX. . .... .. # 4:XXXX............ # # This sublocator would be configured with leading_line_count = 1, # leading_offset=8, and leading_line_offset=4 # # Note that leading_offset must be the same for all lines and measured in characters. # class SubLocator < Puppet::Pops::Parser::Locator attr_reader :locator attr_reader :leading_line_count attr_reader :leading_offset attr_reader :leading_line_offset def self.sub_locator(string, file, leading_line_count, leading_offset, leading_line_offset) self.new(Puppet::Pops::Parser::Locator.locator(string, file), leading_line_count, leading_offset, leading_line_offset) end def initialize(locator, leading_line_count, leading_offset, leading_line_offset) @locator = locator @leading_line_count = leading_line_count @leading_offset = leading_offset @leading_line_offset = leading_line_offset end def file @locator.file end def string @locator.string end # Given offset is offset in the subspace def line_for_offset(offset) @locator.line_for_offset(offset) + @leading_line_count end # Given offset is offset in the subspace def offset_on_line(offset) @locator.offset_on_line(offset) + @leading_line_offset end # Given offset is offset in the subspace def char_offset(offset) effective_line = @locator.line_for_offset(offset) locator.char_offset(offset) + (effective_line * @leading_line_offset) + @leading_offset end # Given offsets are offsets in the subspace def char_length(offset, end_offset) effective_line = @locator.line_for_offset(end_offset) - @locator.line_for_offset(offset) locator.char_length(offset, end_offset) + (effective_line * @leading_line_offset) end def pos_on_line(offset) offset_on_line(offset) +1 end end private class AbstractLocator < Puppet::Pops::Parser::Locator attr_accessor :line_index attr_accessor :string attr_accessor :prev_offset attr_accessor :prev_line attr_reader :string attr_reader :file # Create a locator based on a content string, and a boolean indicating if ruby version support multi-byte strings # or not. # def initialize(string, file, index = nil) @string = string.freeze @file = file.freeze @prev_offset = nil @prev_line = nil @line_index = index - compute_line_index unless !index.nil? + compute_line_index if index.nil? end # Returns the position on line (first position on a line is 1) def pos_on_line(offset) offset_on_line(offset) +1 end def to_location_hash(reported_offset, end_offset) pos = pos_on_line(reported_offset) offset = char_offset(reported_offset) length = char_length(reported_offset, end_offset) start_line = line_for_offset(reported_offset) { :line => start_line, :pos => pos, :offset => offset, :length => length} end # Returns the index of the smallest item for which the item > the given value # This is a min binary search. Although written in Ruby it is only slightly slower than # the corresponding method in C in Ruby 2.0.0 - the main benefit to use this method over # the Ruby C version is that it returns the index (not the value) which means there is not need # to have an additional structure to get the index (or record the index in the structure). This # saves both memory and CPU. It also does not require passing a block that is called since this # method is specialized to search the line index. # def ary_bsearch_i(ary, value) low = 0 high = ary.length mid = nil smaller = false satisfied = false v = nil while low < high do mid = low + ((high - low) / 2) v = (ary[mid] > value) if v == true satisfied = true smaller = true elsif !v smaller = false else raise TypeError, "wrong argument, must be boolean or nil, got '#{v.class}'" end if smaller high = mid else low = mid + 1; end end return nil if low == ary.length return nil if !satisfied return low end # Common impl for 18 and 19 since scanner is byte based def compute_line_index scanner = StringScanner.new(string) result = [0] # first line starts at 0 while scanner.scan_until(/\n/) result << scanner.pos end self.line_index = result.freeze end # Returns the line number (first line is 1) for the given offset def line_for_offset(offset) if prev_offset == offset # use cache return prev_line end if line_nbr = ary_bsearch_i(line_index, offset) # cache prev_offset = offset prev_line = line_nbr return line_nbr end # If not found it is after last # clear cache prev_offset = prev_line = nil return line_index.size end end - class Locator18 < AbstractLocator + class LocatorForChars < AbstractLocator def offset_on_line(offset) line_offset = line_index[ line_for_offset(offset)-1 ] offset - line_offset end def char_offset(char_offset) char_offset end def char_length(offset, end_offset) end_offset - offset end end # This implementation is for Ruby19 and Ruby20. It uses byteslice to get strings from byte based offsets. # For Ruby20 this is faster than using the Stringscanner.charpos method (byteslice outperforms it, when # strings are frozen). # class Locator19 < AbstractLocator # Returns the offset on line (first offset on a line is 0). # Ruby 19 is multibyte but has no character position methods, must use byteslice def offset_on_line(offset) line_offset = line_index[ line_for_offset(offset)-1 ] string.byteslice(line_offset, offset-line_offset).length end # Returns the character offset for a given byte offset # Ruby 19 is multibyte but has no character position methods, must use byteslice def char_offset(byte_offset) string.byteslice(0, byte_offset).length end # Returns the length measured in number of characters from the given start and end byte offseta def char_length(offset, end_offset) string.byteslice(offset, end_offset - offset).length end end end diff --git a/lib/puppet/pops/validation.rb b/lib/puppet/pops/validation.rb index eddd7ab2d..5af51c30c 100644 --- a/lib/puppet/pops/validation.rb +++ b/lib/puppet/pops/validation.rb @@ -1,432 +1,432 @@ # A module with base functionality for validation of a model. # # * **Factory** - an abstract factory implementation that makes it easier to create a new validation factory. # * **SeverityProducer** - produces a severity (:error, :warning, :ignore) for a given Issue # * **DiagnosticProducer** - produces a Diagnostic which binds an Issue to an occurrence of that issue # * **Acceptor** - the receiver/sink/collector of computed diagnostics # * **DiagnosticFormatter** - produces human readable output for a Diagnostic # module Puppet::Pops::Validation # This class is an abstract base implementation of a _model validation factory_ that creates a validator instance # and associates it with a fully configured DiagnosticProducer. # # A _validator_ is responsible for validating a model. There may be different versions of validation available # for one and the same model; e.g. different semantics for different puppet versions, or different types of # validation configuration depending on the context/type of validation that should be performed (static, vs. runtime, etc.). # # This class is abstract and must be subclassed. The subclass must implement the methods # {#label_provider} and {#checker}. It is also expected that the sublcass will override # the severity_producer and configure the issues that should be reported as errors (i.e. if they should be ignored, produce # a warning, or a deprecation warning). # # @abstract Subclass must implement {#checker}, and {#label_provider} # @api public # class Factory # Produces a validator with the given acceptor as the recipient of produced diagnostics. # The acceptor is where detected issues are received (and typically collected). # # @param acceptor [Acceptor] the acceptor is the receiver of all detected issues # @return [#validate] a validator responding to `validate(model)` # # @api public # def validator(acceptor) checker(diagnostic_producer(acceptor)) end # Produces the diagnostics producer to use given an acceptor of issues. # # @param acceptor [Acceptor] the acceptor is the receiver of all detected issues # @return [DiagnosticProducer] a detector of issues # # @api public # def diagnostic_producer(acceptor) Puppet::Pops::Validation::DiagnosticProducer.new(acceptor, severity_producer(), label_provider()) end # Produces the SeverityProducer to use # Subclasses should implement and add specific overrides # # @return [SeverityProducer] a severity producer producing error, warning or ignore per issue # # @api public # def severity_producer Puppet::Pops::Validation::SeverityProducer.new end # Produces the checker to use. # # @abstract # # @api public # def checker(diagnostic_producer) raise NoMethodError, "checker" end # Produces the label provider to use. # # @abstract # # @api public # def label_provider raise NoMethodError, "label_provider" end end # Decides on the severity of a given issue. # The produced severity is one of `:error`, `:warning`, or `:ignore`. # By default, a severity of `:error` is produced for all issues. To configure the severity # of an issue call `#severity=(issue, level)`. # # @return [Symbol] a symbol representing the severity `:error`, `:warning`, or `:ignore` # # @api public # class SeverityProducer @@severity_hash = {:ignore => true, :warning => true, :error => true, :deprecation => true } # Creates a new instance where all issues are diagnosed as :error unless overridden. # @api public # def initialize # If diagnose is not set, the default is returned by the block @severities = Hash.new :error end # Returns the severity of the given issue. # @return [Symbol] severity level :error, :warning, or :ignore # @api public # def severity(issue) assert_issue(issue) @severities[issue] end # @see {#severity} # @api public # def [] issue severity issue end # Override a default severity with the given severity level. # # @param issue [Puppet::Pops::Issues::Issue] the issue for which to set severity # @param level [Symbol] the severity level (:error, :warning, or :ignore). # @api public # def []=(issue, level) raise Puppet::DevError.new("Attempt to set validation severity for something that is not an Issue. (Got #{issue.class})") unless issue.is_a? Puppet::Pops::Issues::Issue raise Puppet::DevError.new("Illegal severity level: #{option}") unless @@severity_hash[level] raise Puppet::DevError.new("Attempt to demote the hard issue '#{issue.issue_code}' to #{level}") unless issue.demotable? || level == :error @severities[issue] = level end # Returns `true` if the issue should be reported or not. # @return [Boolean] this implementation returns true for errors and warnings # # @api public # def should_report? issue diagnose = @severities[issue] diagnose == :error || diagnose == :warning || diagnose == :deprecation end # Checks if the given issue is valid. # @api private # def assert_issue issue raise Puppet::DevError.new("Attempt to get validation severity for something that is not an Issue. (Got #{issue.class})") unless issue.is_a? Puppet::Pops::Issues::Issue end # Checks if the given severity level is valid. # @api private # def assert_severity level raise Puppet::DevError.new("Illegal severity level: #{option}") unless @@severity_hash[level] end end # A producer of diagnostics. # An producer of diagnostics is given each issue occurrence as they are found by a diagnostician/validator. It then produces # a Diagnostic, which it passes on to a configured Acceptor. # # This class exists to aid a diagnostician/validator which will typically first check if a particular issue # will be accepted at all (before checking for an occurrence of the issue; i.e. to perform check avoidance for expensive checks). # A validator passes an instance of Issue, the semantic object (the "culprit"), a hash with arguments, and an optional # exception. The semantic object is used to determine the location of the occurrence of the issue (file/line), and it # sets keys in the given argument hash that may be used in the formatting of the issue message. # class DiagnosticProducer # A producer of severity for a given issue # @return [SeverityProducer] # attr_reader :severity_producer # A producer of labels for objects involved in the issue # @return [LabelProvider] # attr_reader :label_provider # Initializes this producer. # # @param acceptor [Acceptor] a sink/collector of diagnostic results # @param severity_producer [SeverityProducer] the severity producer to use to determine severity of a given issue # @param label_provider [LabelProvider] a provider of model element type to human readable label # def initialize(acceptor, severity_producer, label_provider) @acceptor = acceptor @severity_producer = severity_producer @label_provider = label_provider end def accept(issue, semantic, arguments={}, except=nil) return unless will_accept? issue # Set label provider unless caller provided a special label provider arguments[:label] ||= @label_provider arguments[:semantic] ||= semantic # A detail message is always provided, but is blank by default. # TODO: this support is questionable, it requires knowledge that :detail is special arguments[:detail] ||= '' source_pos = Puppet::Pops::Utils.find_closest_positioned(semantic) file = source_pos ? source_pos.locator.file : nil severity = @severity_producer.severity(issue) @acceptor.accept(Diagnostic.new(severity, issue, file, source_pos, arguments, except)) end def will_accept? issue @severity_producer.should_report? issue end end class Diagnostic attr_reader :severity attr_reader :issue attr_reader :arguments attr_reader :exception attr_reader :file attr_reader :source_pos def initialize severity, issue, file, source_pos, arguments={}, exception=nil @severity = severity @issue = issue @file = file @source_pos = source_pos @arguments = arguments # TODO: Currently unused, the intention is to provide more information (stack backtrace, etc.) when # debugging or similar - this to catch internal problems reported as higher level issues. @exception = exception end end # Formats a diagnostic for output. # Produces a diagnostic output typical for a compiler (suitable for interpretation by tools) # The format is: # `file:line:pos: Message`, where pos, line and file are included if available. # class DiagnosticFormatter def format diagnostic "#{loc(diagnostic)} #{format_severity(diagnostic)}#{format_message(diagnostic)}" end def format_message diagnostic diagnostic.issue.format(diagnostic.arguments) end # This produces "Deprecation notice: " prefix if the diagnostic has :deprecation severity, otherwise "". # The idea is that all other diagnostics are emitted with the methods Puppet.err (or an exception), and # Puppet.warning. # @note Note that it is not a good idea to use Puppet.deprecation_warning as it is for internal deprecation. # def format_severity diagnostic diagnostic.severity == :deprecation ? "Deprecation notice: " : "" end def format_location diagnostic file = diagnostic.file file = (file.is_a?(String) && file.empty?) ? nil : file line = pos = nil if diagnostic.source_pos line = diagnostic.source_pos.line pos = diagnostic.source_pos.pos end if file && line && pos "#{file}:#{line}:#{pos}:" elsif file && line "#{file}:#{line}:" elsif file "#{file}:" else "" end end end # Produces a diagnostic output in the "puppet style", where the location is appended with an "at ..." if the # location is known. # class DiagnosticFormatterPuppetStyle < DiagnosticFormatter def format diagnostic if (location = format_location diagnostic) != "" "#{format_severity(diagnostic)}#{format_message(diagnostic)}#{location}" else format_message(diagnostic) end end # The somewhat (machine) unusable format in current use by puppet. # have to be used here for backwards compatibility. def format_location diagnostic file = diagnostic.file file = (file.is_a?(String) && file.empty?) ? nil : file line = pos = nil if diagnostic.source_pos line = diagnostic.source_pos.line pos = diagnostic.source_pos.pos end if file && line && pos " at #{file}:#{line}:#{pos}" - elsif file and line + elsif file && line " at #{file}:#{line}" elsif line && pos " at line #{line}:#{pos}" elsif line " at line #{line}" elsif file " in #{file}" else "" end end end # An acceptor of diagnostics. # An acceptor of diagnostics is given each issue as they are found by a diagnostician/validator. An # acceptor can collect all found issues, or decide to collect a few and then report, or give up as the first issue # if found. # This default implementation collects all diagnostics in the order they are produced, and can then # answer questions about what was diagnosed. # class Acceptor # All diagnostic in the order they were issued attr_reader :diagnostics # The number of :warning severity issues + number of :deprecation severity issues attr_reader :warning_count # The number of :error severity issues attr_reader :error_count # Initializes this diagnostics acceptor. # By default, the acceptor is configured with a default severity producer. # @param severity_producer [SeverityProducer] the severity producer to use to determine severity of an issue # # TODO add semantic_label_provider # def initialize() @diagnostics = [] @error_count = 0 @warning_count = 0 end # Returns true when errors have been diagnosed. def errors? @error_count > 0 end # Returns true when warnings have been diagnosed. def warnings? @warning_count > 0 end # Returns true when errors and/or warnings have been diagnosed. def errors_or_warnings? errors? || warnings? end # Returns the diagnosed errors in the order thwy were reported. def errors @diagnostics.select {|d| d.severity == :error } end # Returns the diagnosed warnings in the order thwy were reported. # (This includes :warning and :deprecation severity) def warnings @diagnostics.select {|d| d.severity == :warning || d.severity == :deprecation } end def errors_and_warnings @diagnostics.select {|d| d.severity != :ignore } end # Returns the ignored diagnostics in the order thwy were reported (if reported at all) def ignored @diagnostics.select {|d| d.severity == :ignore } end # Add a diagnostic, or all diagnostics from another acceptor to the set of diagnostics # @param diagnostic [Puppet::Pops::Validation::Diagnostic, Puppet::Pops::Validation::Acceptor] diagnostic(s) that should be accepted def accept(diagnostic) if diagnostic.is_a?(Acceptor) diagnostic.diagnostics.each {|d| self.send(d.severity, d)} else self.send(diagnostic.severity, diagnostic) end end # Prunes the contain diagnostics by removing those for which the given block returns true. # The internal statistics is updated as a consequence of removing. # @return [Array 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 diff --git a/lib/puppet/provider/aixobject.rb b/lib/puppet/provider/aixobject.rb index 53132a42e..03317c23e 100644 --- a/lib/puppet/provider/aixobject.rb +++ b/lib/puppet/provider/aixobject.rb @@ -1,392 +1,392 @@ # # Common code for AIX providers. This class implements basic structure for # AIX resources. # Author:: Hector Rivas Gandara # class Puppet::Provider::AixObject < Puppet::Provider desc "Generic AIX resource provider" # The real provider must implement these functions. def lscmd( _value = @resource[:name] ) raise Puppet::Error, "Method not defined #{@resource.class.name} #{@resource.name}: Base AixObject provider doesn't implement lscmd" end def addcmd( _extra_attrs = [] ) raise Puppet::Error, "Method not defined #{@resource.class.name} #{@resource.name}: Base AixObject provider doesn't implement addcmd" end def modifycmd( _attributes_hash = {} ) raise Puppet::Error, "Method not defined #{@resource.class.name} #{@resource.name}: Base AixObject provider doesn't implement modifycmd" end def deletecmd raise Puppet::Error, "Method not defined #{@resource.class.name} #{@resource.name}: Base AixObject provider doesn't implement deletecmd" end # Valid attributes to be managed by this provider. # It is a list of hashes # :aix_attr AIX command attribute name # :puppet_prop Puppet propertie name # :to Optional. Method name that adapts puppet property to aix command value. # :from Optional. Method to adapt aix command line value to puppet property. Optional class << self attr_accessor :attribute_mapping end # Mapping from Puppet property to AIX attribute. def self.attribute_mapping_to if ! @attribute_mapping_to @attribute_mapping_to = {} attribute_mapping.each { |elem| attribute_mapping_to[elem[:puppet_prop]] = { :key => elem[:aix_attr], :method => elem[:to] } } end @attribute_mapping_to end # Mapping from AIX attribute to Puppet property. def self.attribute_mapping_from if ! @attribute_mapping_from @attribute_mapping_from = {} attribute_mapping.each { |elem| attribute_mapping_from[elem[:aix_attr]] = { :key => elem[:puppet_prop], :method => elem[:from] } } end @attribute_mapping_from end # This functions translates a key and value using the given mapping. # Mapping can be nil (no translation) or a hash with this format # {:key => new_key, :method => translate_method} # It returns a list with the pair [key, value] def translate_attr(key, value, mapping) return [key, value] unless mapping return nil unless mapping[key] if mapping[key][:method] new_value = method(mapping[key][:method]).call(value) else new_value = value end [mapping[key][:key], new_value] end # Loads an AIX attribute (key=value) and stores it in the given hash with # puppet semantics. It translates the pair using the given mapping. # # This operation works with each property one by one, # subclasses must reimplement this if more complex operations are needed def load_attribute(key, value, mapping, objectinfo) if mapping.nil? objectinfo[key] = value elsif mapping[key].nil? # is not present in mapping, ignore it. true elsif mapping[key][:method].nil? objectinfo[mapping[key][:key]] = value - elsif + else objectinfo[mapping[key][:key]] = method(mapping[key][:method]).call(value) end return objectinfo end # Gets the given command line argument for the given key and value, # using the given mapping to translate key and value. # All the objectinfo hash (@resource or @property_hash) is passed. # # This operation works with each property one by one, # and default behaviour is return the arguments as key=value pairs. # Subclasses must reimplement this if more complex operations/arguments # are needed # def get_arguments(key, value, mapping, objectinfo) if mapping.nil? new_key = key new_value = value elsif mapping[key].nil? # is not present in mapping, ignore it. new_key = nil new_value = nil elsif mapping[key][:method].nil? new_key = mapping[key][:key] new_value = value - elsif + else new_key = mapping[key][:key] new_value = method(mapping[key][:method]).call(value) end # convert it to string new_value = Array(new_value).join(',') if new_key return [ "#{new_key}=#{new_value}" ] else return [] end end # Convert the provider properties (hash) to AIX command arguments # (list of strings) # This function will translate each value/key and generate the argument using # the get_arguments function. def hash2args(hash, mapping=self.class.attribute_mapping_to) return "" unless hash arg_list = [] hash.each {|key, val| arg_list += self.get_arguments(key, val, mapping, hash) } arg_list end # Parse AIX command attributes from the output of an AIX command, that # which format is a list of space separated of key=value pairs: # "uid=100 groups=a,b,c". # It returns a hash. # # If a mapping is provided, the keys are translated as defined in the # mapping hash. And only values included in mapping will be added # # NOTE: it will ignore the items not including '=' def parse_attr_list(str, mapping=self.class.attribute_mapping_from) properties = {} attrs = [] if str.nil? or (attrs = str.split()).empty? return nil end attrs.each { |i| if i.include? "=" # Ignore if it does not include '=' (key_str, val) = i.split('=') # Check the key if key_str.nil? or key_str.empty? info "Empty key in string 'i'?" continue end key_str.strip! key = key_str.to_sym val.strip! if val properties = self.load_attribute(key, val, mapping, properties) end } properties.empty? ? nil : properties end # Parse AIX command output in a colon separated list of attributes, # This function is useful to parse the output of commands like lsfs -c: # #MountPoint:Device:Vfs:Nodename:Type:Size:Options:AutoMount:Acct # /:/dev/hd4:jfs2::bootfs:557056:rw:yes:no # /home:/dev/hd1:jfs2:::2129920:rw:yes:no # /usr:/dev/hd2:jfs2::bootfs:9797632:rw:yes:no # # If a mapping is provided, the keys are translated as defined in the # mapping hash. And only values included in mapping will be added def parse_colon_list(str, key_list, mapping=self.class.attribute_mapping_from) properties = {} attrs = [] if str.nil? or (attrs = str.split(':')).empty? return nil end attrs.each { |val| key = key_list.shift.downcase.to_sym properties = self.load_attribute(key, val, mapping, properties) } properties.empty? ? nil : properties end # Default parsing function for AIX commands. # It will choose the method depending of the first line. # For the colon separated list it will: # 1. Get keys from first line. # 2. Parse next line. def parse_command_output(output, mapping=self.class.attribute_mapping_from) lines = output.split("\n") # if it begins with #something:... is a colon separated list. if lines[0] =~ /^#.*:/ self.parse_colon_list(lines[1], lines[0][1..-1].split(':'), mapping) else self.parse_attr_list(lines[0], mapping) end end # Retrieve all the information of an existing resource. # It will execute 'lscmd' command and parse the output, using the mapping # 'attribute_mapping_from' to translate the keys and values. def getinfo(refresh = false) if @objectinfo.nil? or refresh == true # Execute lsuser, split all attributes and add them to a dict. begin output = execute(self.lscmd) @objectinfo = self.parse_command_output(output) # All attributtes without translation @objectosinfo = self.parse_command_output(output, nil) rescue Puppet::ExecutionFailure => detail # Print error if needed. FIXME: Do not check the user here. Puppet.debug "aix.getinfo(): Could not find #{@resource.class.name} #{@resource.name}: #{detail}" end end @objectinfo end # Like getinfo, but it will not use the mapping to translate the keys and values. # It might be usefult to retrieve some raw information. def getosinfo(refresh = false) if @objectosinfo.nil? or refresh == true getinfo(refresh) end @objectosinfo || Hash.new end # List all elements of given type. It works for colon separated commands and # list commands. # It returns a list of names. def self.list_all names = [] begin output = execute([self.command(:list), 'ALL']) output = output.split("\n").select{ |line| line != /^#/ } output.each do |line| name = line.split(/[ :]/)[0] names << name if not name.empty? end rescue Puppet::ExecutionFailure => detail # Print error if needed Puppet.debug "aix.list_all(): Could not get all resources of type #{@resource.class.name}: #{detail}" end names end #------------- # Provider API # ------------ # Clear out the cached values. def flush @property_hash.clear if @property_hash @objectinfo.clear if @objectinfo end # Check that the user exists def exists? !!getinfo(true) # !! => converts to bool end # Return all existing instances # The method for returning a list of provider instances. Note that it returns # providers, preferably with values already filled in, not resources. def self.instances objects=[] list_all.each { |entry| objects << new(:name => entry, :ensure => :present) } objects end #- **ensure** # The basic state that the object should be in. Valid values are # `present`, `absent`, `role`. # From ensurable: exists?, create, delete def ensure if exists? :present else :absent end end # Create a new instance of the resource def create if exists? info "already exists" # The object already exists return nil end begin execute(self.addcmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not create #{@resource.class.name} #{@resource.name}: #{detail}", detail.backtrace end end # Delete this instance of the resource def delete unless exists? info "already absent" # the object already doesn't exist return nil end begin execute(self.deletecmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not delete #{@resource.class.name} #{@resource.name}: #{detail}", detail.backtrace end end #-------------------------------- # Call this method when the object is initialized. # It creates getter/setter methods for each property our resource type supports. # If setter or getter already defined it will not be overwritten def self.mk_resource_methods [resource_type.validproperties, resource_type.parameters].flatten.each do |prop| next if prop == :ensure define_method(prop) { get(prop) || :absent} unless public_method_defined?(prop) define_method(prop.to_s + "=") { |*vals| set(prop, *vals) } unless public_method_defined?(prop.to_s + "=") end end # Define the needed getters and setters as soon as we know the resource type def self.resource_type=(resource_type) super mk_resource_methods end # Retrieve a specific value by name. def get(param) (hash = getinfo(false)) ? hash[param] : nil end # Set a property. def set(param, value) @property_hash[param.intern] = value if getinfo().nil? # This is weird... raise Puppet::Error, "Trying to update parameter '#{param}' to '#{value}' for a resource that does not exists #{@resource.class.name} #{@resource.name}: #{detail}" end if value == getinfo()[param.to_sym] return end #self.class.validate(param, value) if cmd = modifycmd({param =>value}) begin execute(cmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}", detail.backtrace end end # Refresh de info. getinfo(true) end def initialize(resource) super @objectinfo = nil @objectosinfo = nil end end diff --git a/lib/puppet/provider/cron/crontab.rb b/lib/puppet/provider/cron/crontab.rb index a59fd23ca..a4e97c910 100644 --- a/lib/puppet/provider/cron/crontab.rb +++ b/lib/puppet/provider/cron/crontab.rb @@ -1,260 +1,297 @@ require 'puppet/provider/parsedfile' Puppet::Type.type(:cron).provide(:crontab, :parent => Puppet::Provider::ParsedFile, :default_target => ENV["USER"] || "root") do commands :crontab => "crontab" text_line :comment, :match => %r{^\s*#}, :post_parse => proc { |record| record[:name] = $1 if record[:line] =~ /Puppet Name: (.+)\s*$/ } text_line :blank, :match => %r{^\s*$} text_line :environment, :match => %r{^\s*\w+=} def self.filetype tabname = case Facter.value(:osfamily) when "Solaris" :suntab when "AIX" :aixtab else :crontab end Puppet::Util::FileType.filetype(tabname) end self::TIME_FIELDS = [:minute, :hour, :monthday, :month, :weekday] record_line :crontab, :fields => %w{time command}, :match => %r{^\s*(@\w+|\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.+)$}, :absent => '*', :block_eval => :instance do def post_parse(record) time = record.delete(:time) if match = /@(\S+)/.match(time) # is there another way to access the constant? Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each { |f| record[f] = :absent } record[:special] = match.captures[0] elsif match = /(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/.match(time) record[:special] = :absent Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.zip(match.captures).each do |field,value| if value == self.absent record[field] = :absent else record[field] = value.split(",") end end else raise Puppet::Error, "Line got parsed as a crontab entry but cannot be handled. Please file a bug with the contents of your crontab" end record end def pre_gen(record) if record[:special] and record[:special] != :absent record[:special] = "@#{record[:special]}" end Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each do |field| if vals = record[field] and vals.is_a?(Array) record[field] = vals.join(",") end end record end def to_line(record) str = "" record[:name] = nil if record[:unmanaged] str = "# Puppet Name: #{record[:name]}\n" if record[:name] if record[:environment] and record[:environment] != :absent str += record[:environment].map {|line| "#{line}\n"}.join('') end if record[:special] and record[:special] != :absent fields = [:special, :command] else fields = Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS + [:command] end str += record.values_at(*fields).map do |field| if field.nil? or field == :absent self.absent else field end end.join(self.joiner) str end end def create if resource.should(:command) then super else resource.err "no command specified, cannot create" end end # Look up a resource with a given name whose user matches a record target # # @api private # # @note This overrides the ParsedFile method for finding resources by name, # so that only records for a given user are matched to resources of the # same user so that orphaned records in other crontabs don't get falsely # matched (#2251) # # @param [Hash] record # @param [Array] resources # # @return [Puppet::Resource, nil] The resource if found, else nil def self.resource_for_record(record, resources) resource = super if resource target = resource[:target] || resource[:user] if record[:target] == target resource end end end # Return the header placed at the top of each generated file, warning # users that modifying this file manually is probably a bad idea. def self.header %{# HEADER: This file was autogenerated at #{Time.now} by puppet. # HEADER: While it can still be managed manually, it is definitely not recommended. # HEADER: Note particularly that the comments starting with 'Puppet Name' should # HEADER: not be deleted, as doing so could cause duplicate cron jobs.\n} end # Regex for finding one vixie cron header. def self.native_header_regex /# DO NOT EDIT THIS FILE.*?Cron version.*?vixie.*?\n/m end # If a vixie cron header is found, it should be dropped, cron will insert # a new one in any case, so we need to avoid duplicates. def self.drop_native_header true end # See if we can match the record against an existing cron job. def self.match(record, resources) # if the record is named, do not even bother (#19876) # except the resource name was implicitly generated (#3220) return false if record[:name] and !record[:unmanaged] resources.each do |name, resource| # Match the command first, since it's the most important one. next unless record[:target] == resource[:target] next unless record[:command] == resource.value(:command) # Now check the time fields compare_fields = self::TIME_FIELDS + [:special] matched = true compare_fields.each do |field| # If the resource does not manage a property (say monthday) it should # always match. If it is the other way around (e.g. resource defines # a should value for :special but the record does not have it, we do # not match next unless resource[field] unless record.include?(field) matched = false break end if record_value = record[field] and resource_value = resource.value(field) # The record translates '*' into absent in the post_parse hook and # the resource type does exactly the opposite (alias :absent to *) next if resource_value == '*' and record_value == :absent next if resource_value == record_value end matched =false break end return resource if matched end false end @name_index = 0 # Collapse name and env records. def self.prefetch_hook(records) name = nil envs = nil result = records.each { |record| case record[:record_type] when :comment if record[:name] name = record[:name] record[:skip] = true # Start collecting env values envs = [] end when :environment # If we're collecting env values (meaning we're in a named cronjob), # store the line and skip the record. if envs envs << record[:line] record[:skip] = true end when :blank # nothing else if name record[:name] = name name = nil else cmd_string = record[:command].gsub(/\s+/, "_") index = ( @name_index += 1 ) record[:name] = "unmanaged:#{cmd_string}-#{ index.to_s }" record[:unmanaged] = true end if envs.nil? or envs.empty? record[:environment] = :absent else # Collect all of the environment lines, and mark the records to be skipped, # since their data is included in our crontab record. record[:environment] = envs # And turn off env collection again envs = nil end end }.reject { |record| record[:skip] } result end def self.to_file(records) text = super # Apparently Freebsd will "helpfully" add a new TZ line to every # single cron line, but not in all cases (e.g., it doesn't do it # on my machine). This is my attempt to fix it so the TZ lines don't # multiply. if text =~ /(^TZ=.+\n)/ tz = $1 text.sub!(tz, '') text = tz + text end text end def user=(user) # we have to mark the target as modified first, to make sure that if # we move a cronjob from userA to userB, userA's crontab will also # be rewritten mark_target_modified @property_hash[:user] = user @property_hash[:target] = user end def user @property_hash[:user] || @property_hash[:target] end + + CRONTAB_DIR = case Facter.value("osfamily") + when "Debian", "HP-UX" + "/var/spool/cron/crontabs" + when /BSD/ + "/var/cron/tabs" + when "Darwin" + "/usr/lib/cron/tabs/" + else + "/var/spool/cron" + end + + # Yield the names of all crontab files stored on the local system. + # + # @note Ignores files that are not writable for the puppet process. + # + # @api private + def self.enumerate_crontabs + Puppet.debug "looking for crontabs in #{CRONTAB_DIR}" + return unless File.readable?(CRONTAB_DIR) + Dir.foreach(CRONTAB_DIR) do |file| + path = "#{CRONTAB_DIR}/#{file}" + yield(file) if File.file?(path) and File.writable?(path) + end + end + + + # Include all plausible crontab files on the system + # in the list of targets (#11383 / PUP-1381) + def self.targets(resources = nil) + targets = super(resources) + enumerate_crontabs do |target| + targets << target + end + targets.uniq + end + end diff --git a/lib/puppet/provider/package/portupgrade.rb b/lib/puppet/provider/package/portupgrade.rb index 5347a7981..ab4889b68 100644 --- a/lib/puppet/provider/package/portupgrade.rb +++ b/lib/puppet/provider/package/portupgrade.rb @@ -1,241 +1,239 @@ # Whole new package, so include pack stuff require 'puppet/provider/package' Puppet::Type.type(:package).provide :portupgrade, :parent => Puppet::Provider::Package do include Puppet::Util::Execution desc "Support for FreeBSD's ports using the portupgrade ports management software. Use the port's full origin as the resource name. eg (ports-mgmt/portupgrade) for the portupgrade port." ## has_features is usually autodetected based on defs below. # has_features :installable, :uninstallable, :upgradeable commands :portupgrade => "/usr/local/sbin/portupgrade", :portinstall => "/usr/local/sbin/portinstall", :portversion => "/usr/local/sbin/portversion", :portuninstall => "/usr/local/sbin/pkg_deinstall", :portinfo => "/usr/sbin/pkg_info" ## Activate this only once approved by someone important. # defaultfor :operatingsystem => :freebsd # Remove unwanted environment variables. %w{INTERACTIVE UNAME}.each do |var| if ENV.include?(var) ENV.delete(var) end end ######## instances sub command (builds the installed packages list) def self.instances Puppet.debug "portupgrade.rb Building packages list from installed ports" # regex to match output from pkg_info regex = %r{^(\S+)-([^-\s]+):(\S+)$} # Corresponding field names fields = [:portname, :ensure, :portorigin] # define Temporary hash used, packages array of hashes hash = Hash.new packages = [] # exec command cmdline = ["-aoQ"] begin output = portinfo(*cmdline) rescue Puppet::ExecutionFailure raise Puppet::Error.new(output, $!) - return nil end # split output and match it and populate temp hash output.split("\n").each { |data| # reset hash to nil for each line hash.clear if match = regex.match(data) # Output matched regex fields.zip(match.captures) { |field, value| hash[field] = value } # populate the actual :name field from the :portorigin # Set :provider to this object name hash[:name] = hash[:portorigin] hash[:provider] = self.name # Add to the full packages listing packages << new(hash) else # unrecognised output from pkg_info Puppet.debug "portupgrade.Instances() - unable to match output: #{data}" end } # return the packages array of hashes return packages end ######## Installation sub command def install Puppet.debug "portupgrade.install() - Installation call on #{@resource[:name]}" # -M: yes, we're a batch, so don't ask any questions cmdline = ["-M BATCH=yes", @resource[:name]] # FIXME: it's possible that portinstall prompts for data so locks up. begin output = portinstall(*cmdline) rescue Puppet::ExecutionFailure raise Puppet::Error.new(output, $!) end if output =~ /\*\* No such / raise Puppet::ExecutionFailure, "Could not find package #{@resource[:name]}" end # No return code required, so do nil to be clean return nil end ######## Latest subcommand (returns the latest version available, or current version if installed is latest) def latest Puppet.debug "portupgrade.latest() - Latest check called on #{@resource[:name]}" # search for latest version available, or return current version. # cmdline = "portversion -v ", returns " " # or "** No matching package found: " cmdline = ["-v", @resource[:name]] begin output = portversion(*cmdline) rescue Puppet::ExecutionFailure raise Puppet::Error.new(output, $!) end # Check: output format. if output =~ /^\S+-([^-\s]+)\s+(\S)\s+(.*)/ installedversion = $1 comparison = $2 otherdata = $3 # Only return a new version number when it's clear that there is a new version # all others return the current version so no unexpected 'upgrades' occur. case comparison when "=", ">" Puppet.debug "portupgrade.latest() - Installed package is latest (#{installedversion})" return installedversion when "<" # "portpkg-1.7_5 < needs updating (port has 1.14)" # "portpkg-1.7_5 < needs updating (port has 1.14) (=> 'newport/pkg') if otherdata =~ /\(port has (\S+)\)/ newversion = $1 Puppet.debug "portupgrade.latest() - Installed version needs updating to (#{newversion})" return newversion else Puppet.debug "portupgrade.latest() - Unable to determine new version from (#{otherdata})" return installedversion end when "?", "!", "#" Puppet.debug "portupgrade.latest() - Comparison Error reported from portversion (#{output})" return installedversion else Puppet.debug "portupgrade.latest() - Unknown code from portversion output (#{output})" return installedversion end else # error: output not parsed correctly, error out with nil. # Seriously - this section should never be called in a perfect world. # as verification that the port is installed has already happened in query. if output =~ /^\*\* No matching package / raise Puppet::ExecutionFailure, "Could not find package #{@resource[:name]}" else # Any other error (dump output to log) raise Puppet::ExecutionFailure, "Unexpected output from portversion: #{output}" end # Just in case we still are running, return nil return nil end # At this point normal operation has finished and we shouldn't have been called. # Error out and let the admin deal with it. raise Puppet::Error, "portversion.latest() - fatal error with portversion: #{output}" - return nil end ###### Query subcommand - return a hash of details if exists, or nil if it doesn't. # Used to make sure the package is installed def query Puppet.debug "portupgrade.query() - Called on #{@resource[:name]}" cmdline = ["-qO", @resource[:name]] begin output = portinfo(*cmdline) rescue Puppet::ExecutionFailure raise Puppet::Error.new(output, $!) end # Check: if output isn't in the right format, return nil if output =~ /^(\S+)-([^-\s]+)/ # Fill in the details hash = Hash.new hash[:portorigin] = self.name hash[:portname] = $1 hash[:ensure] = $2 # If more details are required, then we can do another pkg_info # query here and parse out that output and add to the hash # return the hash to the caller return hash else Puppet.debug "portupgrade.query() - package (#{@resource[:name]}) not installed" return nil end end ####### Uninstall command def uninstall Puppet.debug "portupgrade.uninstall() - called on #{@resource[:name]}" # Get full package name from port origin to uninstall with cmdline = ["-qO", @resource[:name]] begin output = portinfo(*cmdline) rescue Puppet::ExecutionFailure raise Puppet::Error.new(output, $!) end if output =~ /^(\S+)/ # output matches, so uninstall it portuninstall $1 end end ######## Update/upgrade command def update Puppet.debug "portupgrade.update() - called on (#{@resource[:name]})" cmdline = ["-qO", @resource[:name]] begin output = portinfo(*cmdline) rescue Puppet::ExecutionFailure raise Puppet::Error.new(output, $!) end if output =~ /^(\S+)/ # output matches, so upgrade the software cmdline = ["-M BATCH=yes", $1] begin output = portupgrade(*cmdline) rescue Puppet::ExecutionFailure raise Puppet::Error.new(output, $!) end end end ## EOF end diff --git a/lib/puppet/provider/package/rug.rb b/lib/puppet/provider/package/rug.rb index f004b945e..8969a3fc3 100644 --- a/lib/puppet/provider/package/rug.rb +++ b/lib/puppet/provider/package/rug.rb @@ -1,52 +1,51 @@ Puppet::Type.type(:package).provide :rug, :parent => :rpm do desc "Support for suse `rug` package manager." has_feature :versionable commands :rug => "/usr/bin/rug" commands :rpm => "rpm" - defaultfor :operatingsystem => [:suse, :sles] confine :operatingsystem => [:suse, :sles] # Install a package using 'rug'. def install should = @resource.should(:ensure) self.debug "Ensuring => #{should}" wanted = @resource[:name] # XXX: We don't actually deal with epochs here. case should when true, false, Symbol # pass else # Add the package version wanted += "-#{should}" end rug "--quiet", :install, "-y", wanted unless self.query raise Puppet::ExecutionFailure.new( "Could not find package #{self.name}" ) end end # What's the latest package version available? def latest #rug can only get a list of *all* available packages? output = rug "list-updates" if output =~ /#{Regexp.escape @resource[:name]}\s*\|\s*([^\s\|]+)/ return $1 else # rug didn't find updates, pretend the current # version is the latest return @property_hash[:ensure] end end def update # rug install can be used for update, too self.install end end diff --git a/lib/puppet/provider/package/zypper.rb b/lib/puppet/provider/package/zypper.rb index ddfd34084..2fe6cdfca 100644 --- a/lib/puppet/provider/package/zypper.rb +++ b/lib/puppet/provider/package/zypper.rb @@ -1,90 +1,91 @@ Puppet::Type.type(:package).provide :zypper, :parent => :rpm do desc "Support for SuSE `zypper` package manager. Found in SLES10sp2+ and SLES11. This provider supports the `install_options` attribute, which allows command-line flags to be passed to zypper. These options should be specified as a string (e.g. '--flag'), a hash (e.g. {'--flag' => 'value'}), or an array where each element is either a string or a hash." has_feature :versionable, :install_options, :virtual_packages commands :zypper => "/usr/bin/zypper" + defaultfor :operatingsystem => [:suse, :sles, :sled, :opensuse] confine :operatingsystem => [:suse, :sles, :sled, :opensuse] #on zypper versions <1.0, the version option returns 1 #some versions of zypper output on stderr def zypper_version cmd = [self.class.command(:zypper),"--version"] execute(cmd, { :failonfail => false, :combine => true}) end # Install a package using 'zypper'. def install should = @resource.should(:ensure) self.debug "Ensuring => #{should}" wanted = @resource[:name] # XXX: We don't actually deal with epochs here. case should when true, false, Symbol should = nil else # Add the package version wanted = "#{wanted}-#{should}" end #This has been tested with following zypper versions #SLE 10.2: 0.6.104 #SLE 11.0: 1.0.8 #OpenSuse 10.2: 0.6.13 #OpenSuse 11.2: 1.2.8 #Assume that this will work on newer zypper versions #extract version numbers and convert to integers major, minor, patch = zypper_version.scan(/\d+/).map{ |x| x.to_i } self.debug "Detected zypper version #{major}.#{minor}.#{patch}" #zypper version < 1.0 does not support --quiet flag if major < 1 quiet = '--terse' else quiet = '--quiet' end options = [quiet, :install] #zypper 0.6.13 (OpenSuSE 10.2) does not support auto agree with licenses options << '--auto-agree-with-licenses' unless major < 1 and minor <= 6 and patch <= 13 options << '--no-confirm' options << '--name' unless @resource.allow_virtual? || should options += install_options if resource[:install_options] options << wanted zypper *options unless self.query raise Puppet::ExecutionFailure.new( "Could not find package #{self.name}" ) end end # What's the latest package version available? def latest #zypper can only get a list of *all* available packages? output = zypper "list-updates" if output =~ /#{Regexp.escape @resource[:name]}\s*\|.*?\|\s*([^\s\|]+)/ return $1 else # zypper didn't find updates, pretend the current # version is the latest return @property_hash[:ensure] end end def update # zypper install can be used for update, too self.install end end diff --git a/lib/puppet/reference/providers.rb b/lib/puppet/reference/providers.rb index afd31c0a3..7b7334259 100644 --- a/lib/puppet/reference/providers.rb +++ b/lib/puppet/reference/providers.rb @@ -1,119 +1,119 @@ # 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 << "[#{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. " diff --git a/lib/puppet/resource/status.rb b/lib/puppet/resource/status.rb index c923c6fad..e08902f07 100644 --- a/lib/puppet/resource/status.rb +++ b/lib/puppet/resource/status.rb @@ -1,155 +1,211 @@ require 'time' require 'puppet/network/format_support' module Puppet class Resource + + # This class represents the result of evaluating a given resource. It + # contains file and line information about the source, events generated + # while evaluating the resource, timing information, and the status of the + # resource evaluation. + # + # @api private class Status include Puppet::Util::Tagging - include Puppet::Util::Logging include Puppet::Network::FormatSupport - attr_accessor :resource, :node, :file, :line, :current_values, :status, :evaluation_time + # @!attribute [rw] file + # @return [String] The file where `@real_resource` was defined. + attr_accessor :file + + # @!attribute [rw] line + # @return [Integer] The line number in the file where `@real_resource` was defined. + attr_accessor :line + + # @!attribute [rw] evaluation_time + # @return [Float] The time elapsed in sections while evaluating `@real_resource`. + # measured in seconds. + attr_accessor :evaluation_time + # Boolean status types set while evaluating `@real_resource`. STATES = [:skipped, :failed, :failed_to_restart, :restarted, :changed, :out_of_sync, :scheduled] attr_accessor *STATES - attr_reader :source_description, :containment_path, - :default_log_level, :time, :resource, :change_count, - :out_of_sync_count, :resource_type, :title - + # @!attribute [r] source_description + # @return [String] The textual description of the path to `@real_resource` + # based on the containing structures. This is in contrast to + # `@containment_path` which is a list of containment path components. + # @example + # status.source_description #=> "/Stage[main]/Myclass/Exec[date]" + attr_reader :source_description + + # @!attribute [r] containment_path + # @return [Array] A list of resource references that contain + # `@real_resource`. + # @example A normal contained type + # status.containment_path #=> ["Stage[main]", "Myclass", "Exec[date]"] + # @example A whit associated with a class + # status.containment_path #=> ["Whit[Admissible_class[Main]]"] + attr_reader :containment_path + + # @!attribute [r] time + # @return [Time] The time that this status object was created + attr_reader :time + + # @!attribute [r] resource + # @return [String] The resource reference for `@real_resource` + attr_reader :resource + + # @!attribute [r] change_count + # @return [Integer] A count of the successful changes made while + # evaluating `@real_resource`. + attr_reader :change_count + + # @!attribute [r] out_of_sync_count + # @return [Integer] A count of the audited changes made while + # evaluating `@real_resource`. + attr_reader :out_of_sync_count + + # @!attribute [r] resource_type + # @example + # status.resource_type #=> 'Notify' + # @return [String] The class name of `@real_resource` + attr_reader :resource_type + + # @!attribute [r] title + # @return [String] The title of `@real_resource` + attr_reader :title + + # @!attribute [r] events + # @return [Array] A list of events generated + # while evaluating `@real_resource`. + attr_reader :events + + # A list of instance variables that should be serialized with this object + # when converted to YAML. YAML_ATTRIBUTES = %w{@resource @file @line @evaluation_time @change_count @out_of_sync_count @tags @time @events @out_of_sync @changed @resource_type @title @skipped @failed @containment_path}. map(&:to_sym) def self.from_data_hash(data) obj = self.allocate obj.initialize_from_hash(data) obj end def self.from_pson(data) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(data) end # Provide a boolean method for each of the states. STATES.each do |attr| define_method("#{attr}?") do !! send(attr) end end def <<(event) add_event(event) self end def add_event(event) @events << event if event.status == 'failure' self.failed = true elsif event.status == 'success' @change_count += 1 @changed = true end if event.status != 'audit' @out_of_sync_count += 1 @out_of_sync = true end end - def events - @events - end - def failed_because(detail) @real_resource.log_exception(detail, "Could not evaluate: #{detail}") failed = true # There's a contract (implicit unfortunately) that a status of failed # will always be accompanied by an event with some explanatory power. This # is useful for reporting/diagnostics/etc. So synthesize an event here # with the exception detail as the message. add_event(@real_resource.event(:name => :resource_error, :status => "failure", :message => detail.to_s)) end def initialize(resource) @real_resource = resource @source_description = resource.path @containment_path = resource.pathbuilder @resource = resource.to_s @change_count = 0 @out_of_sync_count = 0 @changed = false @out_of_sync = false @skipped = false @failed = false @file = resource.file @line = resource.line tag(*resource.tags) @time = Time.now @events = [] @resource_type = resource.type.to_s.capitalize @title = resource.title end def initialize_from_hash(data) @resource_type = data['resource_type'] @title = data['title'] @resource = data['resource'] @containment_path = data['containment_path'] @file = data['file'] @line = data['line'] @evaluation_time = data['evaluation_time'] @change_count = data['change_count'] @out_of_sync_count = data['out_of_sync_count'] @tags = Puppet::Util::TagSet.new(data['tags']) @time = data['time'] @time = Time.parse(@time) if @time.is_a? String @out_of_sync = data['out_of_sync'] @changed = data['changed'] @skipped = data['skipped'] @failed = data['failed'] @events = data['events'].map do |event| Puppet::Transaction::Event.from_data_hash(event) end end def to_data_hash { 'title' => @title, 'file' => @file, 'line' => @line, 'resource' => @resource, 'resource_type' => @resource_type, 'containment_path' => @containment_path, 'evaluation_time' => @evaluation_time, 'tags' => @tags, 'time' => @time.iso8601(9), 'failed' => @failed, 'changed' => @changed, 'out_of_sync' => @out_of_sync, 'skipped' => @skipped, 'change_count' => @change_count, 'out_of_sync_count' => @out_of_sync_count, 'events' => @events, } end def to_yaml_properties YAML_ATTRIBUTES & super end - - private - - def log_source - source_description - end end end end diff --git a/lib/puppet/resource/type.rb b/lib/puppet/resource/type.rb index 04d1f1f03..826ef6110 100644 --- a/lib/puppet/resource/type.rb +++ b/lib/puppet/resource/type.rb @@ -1,413 +1,406 @@ require 'puppet/parser' require 'puppet/util/warnings' require 'puppet/util/errors' require 'puppet/util/inline_docs' require 'puppet/parser/ast/leaf' require 'puppet/parser/ast/block_expression' -require 'puppet/dsl' # Puppet::Resource::Type represents nodes, classes and defined types. # # It has a standard format for external consumption, usable from the # resource_type indirection via rest and the resource_type face. See the # {file:api_docs/http_resource_type.md#Schema resource type schema # description}. # # @api public class Puppet::Resource::Type Puppet::ResourceType = self include Puppet::Util::InlineDocs include Puppet::Util::Warnings include Puppet::Util::Errors RESOURCE_KINDS = [:hostclass, :node, :definition] # Map the names used in our documentation to the names used internally RESOURCE_KINDS_TO_EXTERNAL_NAMES = { :hostclass => "class", :node => "node", :definition => "defined_type", } RESOURCE_EXTERNAL_NAMES_TO_KINDS = RESOURCE_KINDS_TO_EXTERNAL_NAMES.invert - attr_accessor :file, :line, :doc, :code, :ruby_code, :parent, :resource_type_collection + attr_accessor :file, :line, :doc, :code, :parent, :resource_type_collection attr_reader :namespace, :arguments, :behaves_like, :module_name # Map from argument (aka parameter) names to Puppet Type # @return [Hash :parser def self.from_data_hash(data) name = data.delete('name') or raise ArgumentError, "Resource Type names must be specified" kind = data.delete('kind') || "definition" unless type = RESOURCE_EXTERNAL_NAMES_TO_KINDS[kind] raise ArgumentError, "Unsupported resource kind '#{kind}'" end data = data.inject({}) { |result, ary| result[ary[0].intern] = ary[1]; result } # External documentation uses "parameters" but the internal name # is "arguments" data[:arguments] = data.delete(:parameters) new(type, name, data) end def self.from_pson(data) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(data) end def to_data_hash data = [:doc, :line, :file, :parent].inject({}) do |hash, param| next hash unless (value = self.send(param)) and (value != "") hash[param.to_s] = value hash end # External documentation uses "parameters" but the internal name # is "arguments" data['parameters'] = arguments.dup unless arguments.empty? data['name'] = name unless RESOURCE_KINDS_TO_EXTERNAL_NAMES.has_key?(type) raise ArgumentError, "Unsupported resource kind '#{type}'" end data['kind'] = RESOURCE_KINDS_TO_EXTERNAL_NAMES[type] data end # Are we a child of the passed class? Do a recursive search up our # parentage tree to figure it out. def child_of?(klass) return false unless parent return(klass == parent_type ? true : parent_type.child_of?(klass)) end # Now evaluate the code associated with this class or definition. def evaluate_code(resource) static_parent = evaluate_parent_type(resource) scope = static_parent || resource.scope scope = scope.newscope(:namespace => namespace, :source => self, :resource => resource) unless resource.title == :main scope.compiler.add_class(name) unless definition? set_resource_parameters(resource, scope) resource.add_edge_to_stage if code if @match # Only bother setting up the ephemeral scope if there are match variables to add into it begin elevel = scope.ephemeral_level scope.ephemeral_from(@match, file, line) code.safeevaluate(scope) ensure scope.unset_ephemeral_var(elevel) end else code.safeevaluate(scope) end end - - evaluate_ruby_code(resource, scope) if ruby_code end def initialize(type, name, options = {}) @type = type.to_s.downcase.to_sym raise ArgumentError, "Invalid resource supertype '#{type}'" unless RESOURCE_KINDS.include?(@type) name = convert_from_ast(name) if name.is_a?(Puppet::Parser::AST::HostName) set_name_and_namespace(name) [:code, :doc, :line, :file, :parent].each do |param| next unless value = options[param] send(param.to_s + "=", value) end set_arguments(options[:arguments]) set_argument_types(options[:argument_types]) @match = nil @module_name = options[:module_name] end # This is only used for node names, and really only when the node name # is a regexp. def match(string) return string.to_s.downcase == name unless name_is_regex? @match = @name.match(string) end # Add code from a new instance to our code. def merge(other) fail "#{name} is not a class; cannot add code to it" unless type == :hostclass fail "#{other.name} is not a class; cannot add code from it" unless other.type == :hostclass fail "Cannot have code outside of a class/node/define because 'freeze_main' is enabled" if name == "" and Puppet.settings[:freeze_main] if parent and other.parent and parent != other.parent fail "Cannot merge classes with different parent classes (#{name} => #{parent} vs. #{other.name} => #{other.parent})" end # We know they're either equal or only one is set, so keep whichever parent is specified. self.parent ||= other.parent if other.doc self.doc ||= "" self.doc += other.doc end # This might just be an empty, stub class. return unless other.code unless self.code self.code = other.code return end self.code = Puppet::Parser::ParserFactory.code_merger.concatenate([self, other]) # self.code = self.code.sequence_with(other.code) end # Make an instance of the resource type, and place it in the catalog # if it isn't in the catalog already. This is only possible for # classes and nodes. No parameters are be supplied--if this is a # parameterized class, then all parameters take on their default # values. def ensure_in_catalog(scope, parameters=nil) type == :definition and raise ArgumentError, "Cannot create resources for defined resource types" resource_type = type == :hostclass ? :class : :node # Do nothing if the resource already exists; this makes sure we don't # get multiple copies of the class resource, which helps provide the # singleton nature of classes. # we should not do this for classes with parameters # if parameters are passed, we should still try to create the resource # even if it exists so that we can fail # this prevents us from being able to combine param classes with include if resource = scope.catalog.resource(resource_type, name) and !parameters return resource end resource = Puppet::Parser::Resource.new(resource_type, name, :scope => scope, :source => self) assign_parameter_values(parameters, resource) instantiate_resource(scope, resource) scope.compiler.add_resource(scope, resource) resource end def instantiate_resource(scope, resource) # Make sure our parent class has been evaluated, if we have one. if parent && !scope.catalog.resource(resource.type, parent) parent_type(scope).ensure_in_catalog(scope) end if ['Class', 'Node'].include? resource.type scope.catalog.tag(*resource.tags) end end def name return @name unless @name.is_a?(Regexp) @name.source.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'') end def name_is_regex? @name.is_a?(Regexp) end def assign_parameter_values(parameters, resource) return unless parameters # It'd be nice to assign default parameter values here, # but we can't because they often rely on local variables # created during set_resource_parameters. parameters.each do |name, value| resource.set_parameter name, value end end # MQR TODO: # # The change(s) introduced by the fix for #4270 are mostly silly & should be # removed, though we didn't realize it at the time. If it can be established/ # ensured that nodes never call parent_type and that resource_types are always # (as they should be) members of exactly one resource_type_collection the # following method could / should be replaced with: # # def parent_type # @parent_type ||= parent && ( # resource_type_collection.find_or_load([name],parent,type.to_sym) || # fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{resource_type_collection.environment}" # ) # end # # ...and then the rest of the changes around passing in scope reverted. # def parent_type(scope = nil) return nil unless parent unless @parent_type raise "Must pass scope to parent_type when called first time" unless scope unless @parent_type = scope.environment.known_resource_types.send("find_#{type}", [name], parent) fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{scope.environment}" end end @parent_type end # Set any arguments passed by the resource as variables in the scope. def set_resource_parameters(resource, scope) set = {} resource.to_hash.each do |param, value| param = param.to_sym fail Puppet::ParseError, "#{resource.ref} does not accept attribute #{param}" unless valid_parameter?(param) exceptwrap { scope[param.to_s] = value } set[param] = true end if @type == :hostclass scope["title"] = resource.title.to_s.downcase unless set.include? :title scope["name"] = resource.name.to_s.downcase unless set.include? :name else scope["title"] = resource.title unless set.include? :title scope["name"] = resource.name unless set.include? :name end scope["module_name"] = module_name if module_name and ! set.include? :module_name if caller_name = scope.parent_module_name and ! set.include?(:caller_module_name) scope["caller_module_name"] = caller_name end scope.class_set(self.name,scope) if hostclass? or node? # Evaluate the default parameters, now that all other variables are set default_params = resource.set_default_parameters(scope) default_params.each { |param| scope[param] = resource[param] } # This has to come after the above parameters so that default values # can use their values resource.validate_complete end # Check whether a given argument is valid. def valid_parameter?(param) param = param.to_s return true if param == "name" return true if Puppet::Type.metaparam?(param) return false unless defined?(@arguments) return(arguments.include?(param) ? true : false) end def set_arguments(arguments) @arguments = {} return if arguments.nil? arguments.each do |arg, default| arg = arg.to_s warn_if_metaparam(arg, default) @arguments[arg] = default end end # Sets the argument name to Puppet Type hash used for type checking. # Names must correspond to available arguments (they must be defined first). # Arguments not mentioned will not be type-checked. Only supported when parser == "future" # def set_argument_types(name_to_type_hash) @argument_types = {} # Stop here if not running under future parser, the rest requires pops to be initialized # and that the type system is available return unless Puppet[:parser] == 'future' && name_to_type_hash name_to_type_hash.each do |name, t| # catch internal errors unless @arguments.include?(name) raise Puppet::DevError, "Parameter '#{name}' is given a type, but is not a valid parameter." end unless t.is_a? Puppet::Pops::Types::PAnyType raise Puppet::DevError, "Parameter '#{name}' is given a type that is not a Puppet Type, got #{t.class}" end @argument_types[name] = t end end private def convert_from_ast(name) value = name.value if value.is_a?(Puppet::Parser::AST::Regex) name = value.value else name = value end end def evaluate_parent_type(resource) return unless klass = parent_type(resource.scope) and parent_resource = resource.scope.compiler.catalog.resource(:class, klass.name) || resource.scope.compiler.catalog.resource(:node, klass.name) parent_resource.evaluate unless parent_resource.evaluated? parent_scope(resource.scope, klass) end - def evaluate_ruby_code(resource, scope) - Puppet::DSL::ResourceAPI.new(resource, scope, ruby_code).evaluate - end - # Split an fq name into a namespace and name def namesplit(fullname) ary = fullname.split("::") n = ary.pop || "" ns = ary.join("::") return ns, n end def parent_scope(scope, klass) scope.class_scope(klass) || raise(Puppet::DevError, "Could not find scope for #{klass.name}") end def set_name_and_namespace(name) if name.is_a?(Regexp) @name = name @namespace = "" else @name = name.to_s.downcase # Note we're doing something somewhat weird here -- we're setting # the class's namespace to its fully qualified name. This means # anything inside that class starts looking in that namespace first. @namespace, ignored_shortname = @type == :hostclass ? [@name, ''] : namesplit(@name) end end def warn_if_metaparam(param, default) return unless Puppet::Type.metaparamclass(param) if default warnonce "#{param} is a metaparam; this value will inherit to all contained resources in the #{self.name} definition" else raise Puppet::ParseError, "#{param} is a metaparameter; please choose another parameter name in the #{self.name} definition" end end end diff --git a/lib/puppet/transaction/event.rb b/lib/puppet/transaction/event.rb index fc635a708..0a594331b 100644 --- a/lib/puppet/transaction/event.rb +++ b/lib/puppet/transaction/event.rb @@ -1,107 +1,111 @@ require 'puppet/transaction' require 'puppet/util/tagging' require 'puppet/util/logging' require 'puppet/util/methodhelper' require 'puppet/network/format_support' # A simple struct for storing what happens on the system. class Puppet::Transaction::Event include Puppet::Util::MethodHelper include Puppet::Util::Tagging include Puppet::Util::Logging include Puppet::Network::FormatSupport ATTRIBUTES = [:name, :resource, :property, :previous_value, :desired_value, :historical_value, :status, :message, :file, :line, :source_description, :audited, :invalidate_refreshes] YAML_ATTRIBUTES = %w{@audited @property @previous_value @desired_value @historical_value @message @name @status @time}.map(&:to_sym) attr_accessor *ATTRIBUTES attr_accessor :time attr_reader :default_log_level EVENT_STATUSES = %w{noop success failure audit} def self.from_data_hash(data) obj = self.allocate obj.initialize_from_hash(data) obj end def self.from_pson(data) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(data) end def initialize(options = {}) @audited = false set_options(options) @time = Time.now end def initialize_from_hash(data) @audited = data['audited'] @property = data['property'] @previous_value = data['previous_value'] @desired_value = data['desired_value'] @historical_value = data['historical_value'] @message = data['message'] @name = data['name'].intern if data['name'] @status = data['status'] @time = data['time'] @time = Time.parse(@time) if @time.is_a? String end def to_data_hash { 'audited' => @audited, 'property' => @property, 'previous_value' => @previous_value, 'desired_value' => @desired_value, 'historical_value' => @historical_value, 'message' => @message, 'name' => @name, 'status' => @status, 'time' => @time.iso8601(9), } end def property=(prop) @property = prop.to_s end def resource=(res) if res.respond_to?(:[]) and level = res[:loglevel] @default_log_level = level end @resource = res.to_s end def send_log super(log_level, message) end def status=(value) raise ArgumentError, "Event status can only be #{EVENT_STATUSES.join(', ')}" unless EVENT_STATUSES.include?(value) @status = value end def to_s message end + def inspect + %Q(#<#{self.class.name} @name="#{@name.inspect}" @message="#{@message.inspect}">) + end + def to_yaml_properties YAML_ATTRIBUTES & super end private # If it's a failure, use 'err', else use either the resource's log level (if available) # or 'notice'. def log_level status == "failure" ? :err : (@default_log_level || :notice) end # Used by the Logging module def log_source source_description || property || resource end end diff --git a/lib/puppet/transaction/event_manager.rb b/lib/puppet/transaction/event_manager.rb index 187f27a56..c9338da8b 100644 --- a/lib/puppet/transaction/event_manager.rb +++ b/lib/puppet/transaction/event_manager.rb @@ -1,122 +1,147 @@ require 'puppet/transaction' +# This class stores, routes, and responds to events generated while evaluating +# a transaction. +# +# @api private class Puppet::Transaction::EventManager - attr_reader :transaction, :events + + # @!attribute [r] transaction + # @return [Puppet::Transaction] The transaction associated with this event manager. + attr_reader :transaction + + # @!attribute [r] events + # @todo Determine if this instance variable is used for anything aside from testing. + # @return [Array] A list of events that can be + # handled by the target resouce. Events that cannot be handled by the + # target resource will be discarded. + attr_reader :events def initialize(transaction) @transaction = transaction @event_queues = {} @events = [] end def relationship_graph transaction.relationship_graph end # Respond to any queued events for this resource. def process_events(resource) restarted = false queued_events(resource) do |callback, events| r = process_callback(resource, callback, events) restarted ||= r end if restarted queue_events(resource, [resource.event(:name => :restarted, :status => "success")]) transaction.resource_status(resource).restarted = true end end - # Queue events for other resources to respond to. All of these events have + # Queues events for other resources to respond to. All of these events have # to be from the same resource. + # + # @param resource [Puppet::Type] The resource generating the given events + # @param events [Array] All events generated by this resource + # @return [void] def queue_events(resource, events) #@events += events # Do some basic normalization so we're not doing so many # graph queries for large sets of events. events.inject({}) do |collection, event| collection[event.name] ||= [] collection[event.name] << event collection end.collect do |name, list| # It doesn't matter which event we use - they all have the same source # and name here. event = list[0] # Collect the targets of any subscriptions to those events. We pass # the parent resource in so it will override the source in the events, # since eval_generated children can't have direct relationships. received = (event.name != :restarted) relationship_graph.matching_edges(event, resource).each do |edge| received ||= true unless edge.target.is_a?(Puppet::Type.type(:whit)) next unless method = edge.callback next unless edge.target.respond_to?(method) queue_events_for_resource(resource, edge.target, method, list) end @events << event if received queue_events_for_resource(resource, resource, :refresh, [event]) if resource.self_refresh? and ! resource.deleting? end dequeue_events_for_resource(resource, :refresh) if events.detect { |e| e.invalidate_refreshes } end def dequeue_events_for_resource(target, callback) target.info "Unscheduling #{callback} on #{target}" @event_queues[target][callback] = {} if @event_queues[target] end def queue_events_for_resource(source, target, callback, events) whit = Puppet::Type.type(:whit) # The message that a resource is refreshing the completed-whit for its own class # is extremely counter-intuitive. Basically everything else is easy to understand, # if you suppress the whit-lookingness of the whit resources refreshing_c_whit = target.is_a?(whit) && target.name =~ /^completed_/ if refreshing_c_whit source.debug "The container #{target} will propagate my #{callback} event" else source.info "Scheduling #{callback} of #{target}" end @event_queues[target] ||= {} @event_queues[target][callback] ||= [] @event_queues[target][callback].concat(events) end def queued_events(resource) return unless callbacks = @event_queues[resource] callbacks.each do |callback, events| yield callback, events unless events.empty? end end private + # Processes callbacks for a given resource. + # + # @param resource [Puppet::Type] The resource receiving the callback. + # @param callback [Symbol] The name of the callback method that will be invoked. + # @param events [Array] A list of events + # associated with this callback and resource. + # @return [true, false] Whether the callback was successfully run. def process_callback(resource, callback, events) process_noop_events(resource, callback, events) and return false unless events.detect { |e| e.status != "noop" } resource.send(callback) if not resource.is_a?(Puppet::Type.type(:whit)) resource.notice "Triggered '#{callback}' from #{events.length} events" end return true rescue => detail resource.err "Failed to call #{callback}: #{detail}" transaction.resource_status(resource).failed_to_restart = true resource.log_exception(detail) return false end def process_noop_events(resource, callback, events) resource.notice "Would have triggered '#{callback}' from #{events.length} events" # And then add an event for it. queue_events(resource, [resource.event(:status => "noop", :name => :noop_restart)]) true # so the 'and if' works end end diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 208d860f0..90a84a430 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1,2452 +1,2452 @@ require '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 +# of types, other methods deal with various aspects while running; wiring up a resource (expressed in Puppet 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' diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index 851b97312..5850377b0 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,933 +1,931 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'enumerator' require 'pathname' require 'puppet/parameter/boolean' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/util/backups' require 'puppet/util/symbolic_file_mode' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups include Puppet::Util::SymbolicFileMode @doc = "Manages files, including their content, ownership, and permissions. The `file` type can manage normal files, directories, and symlinks; the type should be specified in the `ensure` attribute. File contents can be managed directly with the `content` attribute, or downloaded from a remote source using the `source` attribute; the latter can also be used to recursively serve directories (when the `recurse` attribute is set to `true` or `local`). On Windows, note that file contents are managed in binary mode; Puppet never automatically translates line endings. **Autorequires:** If Puppet is managing the user or group that owns a file, the file resource will autorequire them. If Puppet is managing any parent directories of a file, the file resource will autorequire them." feature :manages_symlinks, "The provider can manage symbolic links." def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path ] ] ] ] end newparam(:path) do desc <<-'EOT' The path to the file to manage. Must be fully qualified. On Windows, the path should include the drive letter and should use `/` as the separator character (rather than `\\`). EOT isnamevar validate do |value| unless Puppet::Util.absolute_path?(value) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end munge do |value| if value.start_with?('//') and ::File.basename(value) == "/" # This is a UNC path pointing to a share, so don't add a trailing slash ::File.expand_path(value) else ::File.join(::File.split(::File.expand_path(value))) end end end newparam(:backup) do desc <<-EOT Whether (and how) file content should be backed up before being replaced. This attribute works best as a resource default in the site manifest (`File { backup => main }`), so it can affect all file resources. * If set to `false`, file content won't be backed up. * If set to a string beginning with `.` (e.g., `.puppet-bak`), Puppet will use copy the file in the same directory with that value as the extension of the backup. (A value of `true` is a synonym for `.puppet-bak`.) * If set to any other string, Puppet will try to back up to a filebucket with that title. See the `filebucket` resource type for more details. (This is the preferred method for backup, since it can be centralized and queried.) Default value: `puppet`, which backs up to a filebucket of the same name. (Puppet automatically creates a **local** filebucket named `puppet` if one doesn't already exist.) Backing up to a local filebucket isn't particularly useful. If you want to make organized use of backups, you will generally want to use the puppet master server's filebucket service. This requires declaring a filebucket resource and a resource default for the `backup` attribute in site.pp: # /etc/puppet/manifests/site.pp filebucket { 'main': path => false, # This is required for remote filebuckets. server => 'puppet.example.com', # Optional; defaults to the configured puppet master. } File { backup => main, } If you are using multiple puppet master servers, you will want to centralize the contents of the filebucket. Either configure your load balancer to direct all filebucket traffic to a single master, or use something like an out-of-band rsync task to synchronize the content on all masters. EOT defaultto "puppet" munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value when false, "false", :false false when true, "true", ".puppet-bak", :true ".puppet-bak" when String value else self.fail "Invalid backup type #{value.inspect}" end end end newparam(:recurse) do desc "Whether to recursively manage the _contents_ of a directory. This attribute is only used when `ensure => directory` is set. The allowed values are: * `false` --- The default behavior. The contents of the directory will not be automatically managed. * `remote` --- If the `source` attribute is set, Puppet will automatically manage the contents of the source directory (or directories), ensuring that equivalent files and directories exist on the target system and that their contents match. Using `remote` will disable the `purge` attribute, but results in faster catalog application than `recurse => true`. The `source` attribute is mandatory when `recurse => remote`. * `true` --- If the `source` attribute is set, this behaves similarly to `recurse => remote`, automatically managing files from the source directory. This also enables the `purge` attribute, which can delete unmanaged files from a directory. See the description of `purge` for more details. The `source` attribute is not mandatory when using `recurse => true`, so you can enable purging in directories where all files are managed individually. - (Note: `inf` is a deprecated synonym for `true`.) - By default, setting recurse to `remote` or `true` will manage _all_ subdirectories. You can use the `recurselimit` attribute to limit the recursion depth. " - newvalues(:true, :false, :inf, :remote) + newvalues(:true, :false, :remote) validate { |arg| } munge do |value| newval = super(value) case newval - when :true, :inf; true + when :true; true when :false; false when :remote; :remote else self.fail "Invalid recurse value #{value.inspect}" end end end newparam(:recurselimit) do desc "How far Puppet should descend into subdirectories, when using `ensure => directory` and either `recurse => true` or `recurse => remote`. The recursion limit affects which files will be copied from the `source` directory, as well as which files can be purged when `purge => true`. Setting `recurselimit => 0` is the same as setting `recurse => false` --- Puppet will manage the directory, but all of its contents will be treated as unmanaged. Setting `recurselimit => 1` will manage files and directories that are directly inside the directory, but will not manage the contents of any subdirectories. Setting `recurselimit => 2` will manage the direct contents of the directory, as well as the contents of the _first_ level of subdirectories. And so on --- 3 will manage the contents of the second level of subdirectories, etc." newvalues(/^[0-9]+$/) munge do |value| newval = super(value) case newval when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else self.fail "Invalid recurselimit value #{value.inspect}" end end end newparam(:replace, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether to replace a file or symlink that already exists on the local system but whose content doesn't match what the `source` or `content` attribute specifies. Setting this to false allows file resources to initialize files without overwriting future changes. Note that this only affects content; Puppet will still manage ownership and permissions. Defaults to `true`." defaultto :true end newparam(:force, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Perform the file operation even if it will destroy one or more directories. You must use `force` in order to: * `purge` subdirectories * Replace directories with files or links * Remove a directory when `ensure => absent`" defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. `[a-z]*`. Matches that would descend into the directory structure are ignored, e.g., `*/*`." validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, `follow` will copy the target file instead of the link, `manage` will copy the link itself, and `ignore` will just pass it by. When not copying, `manage` and `ignore` behave equivalently (because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the link points." newvalues(:follow, :manage) defaultto :manage end newparam(:purge, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether unmanaged files should be purged. This option only makes sense when `ensure => directory` and `recurse => true`. * When recursively duplicating an entire directory with the `source` attribute, `purge => true` will automatically purge any files that are not in the source directory. * When managing files in a directory as individual resources, setting `purge => true` will purge any files that aren't being specifically managed. If you have a filebucket configured, the purged files will be uploaded, but if you do not, this will destroy data. Unless `force => true` is set, purging will **not** delete directories, although it will delete the files they contain. If `recurselimit` is set and you aren't using `force => true`, purging will obey the recursion limit; files in any subdirectories deeper than the limit will be treated as unmanaged and left alone." defaultto :false end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter only affects recursive directory copies; by default, the first valid source is the only one used, but if this parameter is set to `all`, then all valid sources will have all of their contents copied to the local system. If a given file exists in more than one source, the version from the earliest source in the list will be used." defaultto :first newvalues(:first, :all) end newparam(:show_diff, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether to display differences when the file changes, defaulting to true. This parameter is useful for files that may contain passwords or other secret data, which might otherwise be included in Puppet reports or other insecure outputs. If the global `show_diff` setting is false, then no diffs will be shown even if this parameter is true." defaultto :true end newparam(:validate_cmd) do desc "A command for validating the file's syntax before replacing it. If Puppet would need to rewrite a file due to new `source` or `content`, it will check the new content's validity first. If validation fails, the file resource will fail. This command must have a fully qualified path, and should contain a percent (`%`) token where it would expect an input file. It must exit `0` if the syntax is correct, and non-zero otherwise. The command will be run on the target system while applying the catalog, not on the puppet master. Example: file { '/etc/apache2/apache2.conf': content => 'example', validate_cmd => '/usr/sbin/apache2 -t -f %', } This would replace apache2.conf only if the test returned true. Note that if a validation command requires a `%` as part of its text, you can specify a different placeholder token with the `validate_replacement` attribute." end newparam(:validate_replacement) do desc "The replacement string in a `validate_cmd` that will be replaced with an input file name. Defaults to: `%`" defaultto '%' end # Autorequire the nearest ancestor directory found in the catalog. autorequire(:file) do req = [] path = Pathname.new(self[:path]) if !path.root? # Start at our parent, to avoid autorequiring ourself parents = path.parent.enum_for(:ascend) if found = parents.find { |p| catalog.resource(:file, p.to_s) } req << found.to_s end end # if the resource is a link, make sure the target is created first req << self[:target] if self[:target] req end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] SOURCE_ONLY_CHECKSUMS = [:none, :ctime, :mtime] validate do creator_count = 0 CREATORS.each do |param| creator_count += 1 if self.should(param) end creator_count += 1 if @parameters.include?(:source) self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if creator_count > 1 self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote self.fail "You cannot specify source when using checksum 'none'" if self[:checksum] == :none && !self[:source].nil? SOURCE_ONLY_CHECKSUMS.each do |checksum_type| self.fail "You cannot specify content when using checksum '#{checksum_type}'" if self[:checksum] == checksum_type && !self[:content].nil? end self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit] provider.validate if provider.respond_to?(:validate) end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end def self.instances return [] end # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(::File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' property to do things. asuser = self.should(:owner) if writeable end asuser end def bucket return @bucket if @bucket backup = self[:backup] return nil unless backup return nil if backup =~ /^\./ unless catalog or backup == "puppet" fail "Can not find filebucket for backups without a catalog" end unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet" fail "Could not find filebucket #{backup} specified in backup" end return default_bucket unless filebucket @bucket = filebucket.bucket @bucket end def default_bucket Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end # Does the file currently exist? Just checks for whether # we have a stat def exist? stat ? true : false end def present?(current_values) super && current_values[:ensure] != :false end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Look up our bucket, if there is one bucket super end # Create any children via recursion or whatever. def eval_generate return [] unless self.recurse? recurse end def ancestors ancestors = Pathname.new(self[:path]).enum_for(:ascend).map(&:to_s) ancestors.delete(self[:path]) ancestors end def flush # We want to make sure we retrieve metadata anew on each transaction. @parameters.each do |name, param| param.flush if param.respond_to?(:flush) end @stat = :needs_stat end def initialize(hash) # Used for caching clients @clients = {} super # If they've specified a source, we get our 'should' values # from it. unless self[:ensure] if self[:target] self[:ensure] = :link elsif self[:content] self[:ensure] = :file end end @stat = :needs_stat end # Configure discovered resources to be purged. def mark_children_for_purging(children) children.each do |name, child| next if child[:source] child[:ensure] = :absent end end # Create a new file or directory object as a child to the current # object. def newchild(path) full_path = ::File.join(self[:path], path) # Add some new values to our original arguments -- these are the ones # set at initialization. We specifically want to exclude any param # values set by the :source property or any default values. # LAK:NOTE This is kind of silly, because the whole point here is that # the values set at initialization should live as long as the resource # but values set by default or by :source should only live for the transaction # or so. Unfortunately, we don't have a straightforward way to manage # the different lifetimes of this data, so we kludge it like this. # The right-side hash wins in the merge. options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? } # These should never be passed to our children. [:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param| options.delete(param) if options.include?(param) end self.class.new(options) end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder # We specifically need to call the method here, so it looks # up our parent in the catalog graph. if parent = parent() # We only need to behave specially when our parent is also # a file if parent.is_a?(self.class) # Remove the parent file name list = parent.pathbuilder list.pop # remove the parent's path info return list << self.ref else return super end else return [self.ref] end end # Recursively generate a list of file resources, which will # be used to copy remote files, manage local files, and/or make links # to map to another directory. def recurse children = (self[:recurse] == :remote) ? {} : recurse_local if self[:target] recurse_link(children) elsif self[:source] recurse_remote(children) end # If we're purging resources, then delete any resource that isn't on the # remote system. mark_children_for_purging(children) if self.purge? # REVISIT: sort_by is more efficient? result = children.values.sort { |a, b| a[:path] <=> b[:path] } remove_less_specific_files(result) end # This is to fix bug #2296, where two files recurse over the same # set of files. It's a rare case, and when it does happen you're # not likely to have many actual conflicts, which is good, because # this is a pretty inefficient implementation. def remove_less_specific_files(files) # REVISIT: is this Windows safe? AltSeparator? mypath = self[:path].split(::File::Separator) other_paths = catalog.vertices. select { |r| r.is_a?(self.class) and r[:path] != self[:path] }. collect { |r| r[:path].split(::File::Separator) }. select { |p| p[0,mypath.length] == mypath } return files if other_paths.empty? files.reject { |file| path = file[:path].split(::File::Separator) other_paths.any? { |p| path[0,p.length] == p } } end # A simple method for determining whether we should be recursing. def recurse? self[:recurse] == true or self[:recurse] == :remote end # Recurse the target of the link. def recurse_link(children) perform_recursion(self[:target]).each do |meta| if meta.relative_path == "." self[:ensure] = :directory next end children[meta.relative_path] ||= newchild(meta.relative_path) if meta.ftype == "directory" children[meta.relative_path][:ensure] = :directory else children[meta.relative_path][:ensure] = :link children[meta.relative_path][:target] = meta.full_path end end children end # Recurse the file itself, returning a Metadata instance for every found file. def recurse_local result = perform_recursion(self[:path]) return {} unless result result.inject({}) do |hash, meta| next hash if meta.relative_path == "." hash[meta.relative_path] = newchild(meta.relative_path) hash end end # Recurse against our remote file. def recurse_remote(children) sourceselect = self[:sourceselect] total = self[:source].collect do |source| next unless result = 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| if meta.relative_path == "." parameter(:source).metadata = meta next end children[meta.relative_path] ||= newchild(meta.relative_path) children[meta.relative_path][:source] = meta.source if meta.ftype == "file" children[meta.relative_path][:checksum] = Puppet[:digest_algorithm].to_sym end children[meta.relative_path].parameter(:source).metadata = meta end children end def perform_recursion(path) Puppet::FileServing::Metadata.indirection.search( path, :links => self[:links], :recurse => (self[:recurse] == :remote ? true : self[:recurse]), :recurselimit => self[:recurselimit], :ignore => self[:ignore], :checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none, :environment => catalog.environment ) end # Back up and remove the file or directory at `self[:path]`. # # @param [Symbol] should The file type replacing the current content. # @return [Boolean] True if the file was removed, else False # @raises [fail???] If the current file isn't one of %w{file link directory} and can't be removed. def remove_existing(should) wanted_type = should.to_s current_type = read_current_type if current_type.nil? return false end if can_backup?(current_type) backup_existing end if wanted_type != "link" and current_type == wanted_type return false end case current_type when "directory" return remove_directory(wanted_type) when "link", "file" return remove_file(current_type, wanted_type) else self.fail "Could not back up files of type #{current_type}" end end def retrieve if source = parameter(:source) source.copy_source_values end super end # Set the checksum, from another property. There are multiple # properties that modify the contents of a file, and they need the # ability to make sure that the checksum value is in sync. def setchecksum(sum = nil) if @parameters.include? :checksum if sum @parameters[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. currentvalue = @parameters[:checksum].retrieve @parameters[:checksum].checksum = currentvalue end end end # Should this thing be a normal file? This is a relatively complex # way of determining whether we're trying to create a normal file, # and it's here so that the logic isn't visible in the content property. def should_be_file? return true if self[:ensure] == :file # I.e., it's set to something like "directory" return false if e = self[:ensure] and e != :present # The user doesn't really care, apparently if self[:ensure] == :present return true unless s = stat return(s.ftype == "file" ? true : false) end # If we've gotten here, then :ensure isn't set return true if self[:content] return true if stat and stat.ftype == "file" false end # Stat our file. Depending on the value of the 'links' attribute, we # use either 'stat' or 'lstat', and we expect the properties to use the # resulting stat object accordingly (mostly by testing the 'ftype' # value). # # We use the initial value :needs_stat to ensure we only stat the file once, # but can also keep track of a failed stat (@stat == nil). This also allows # us to re-stat on demand by setting @stat = :needs_stat. def stat return @stat unless @stat == :needs_stat method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end @stat = begin Puppet::FileSystem.send(method, self[:path]) rescue Errno::ENOENT => error nil rescue Errno::ENOTDIR => error nil rescue Errno::EACCES => error warning "Could not stat; permission denied" nil end end def to_resource resource = super resource.delete(:target) if resource[:target] == :notlink resource end # Write out the file. Requires the property name for logging. # Write will be done by the content property, along with checksum computation def write(property) remove_existing(:file) mode = self.should(:mode) # might be nil mode_int = mode ? symbolic_mode_to_int(mode, Puppet::Util::DEFAULT_POSIX_MODE) : nil if write_temporary_file? Puppet::Util.replace_file(self[:path], mode_int) do |file| file.binmode content_checksum = write_content(file) file.flush fail_if_checksum_is_wrong(file.path, content_checksum) if validate_checksum? if self[:validate_cmd] output = Puppet::Util::Execution.execute(self[:validate_cmd].gsub(self[:validate_replacement], file.path), :failonfail => true, :combine => true) output.split(/\n/).each { |line| self.debug(line) } end end else umask = mode ? 000 : 022 Puppet::Util.withumask(umask) { ::File.open(self[:path], 'wb', mode_int ) { |f| write_content(f) } } end # make sure all of the modes are actually correct property_fix end private # @return [String] The type of the current file, cast to a string. def read_current_type stat_info = stat if stat_info stat_info.ftype.to_s else nil end end # @return [Boolean] If the current file can be backed up and needs to be backed up. def can_backup?(type) if type == "directory" and not force? # (#18110) Directories cannot be removed without :force, so it doesn't # make sense to back them up. false else true end end # @return [Boolean] True if the directory was removed # @api private def remove_directory(wanted_type) if force? debug "Removing existing directory for replacement with #{wanted_type}" FileUtils.rmtree(self[:path]) stat_needed true else notice "Not removing directory; use 'force' to override" false end end # @return [Boolean] if the file was removed (which is always true currently) # @api private def remove_file(current_type, wanted_type) debug "Removing existing #{current_type} for replacement with #{wanted_type}" Puppet::FileSystem.unlink(self[:path]) stat_needed true end def stat_needed @stat = :needs_stat end # Back up the existing file at a given prior to it being removed # @api private # @raise [Puppet::Error] if the file backup failed # @return [void] def backup_existing unless perform_backup raise Puppet::Error, "Could not back up; will not replace" end end # Should we validate the checksum of the file we're writing? def validate_checksum? self[:checksum] !~ /time/ end # Make sure the file we wrote out is what we think it is. def fail_if_checksum_is_wrong(path, content_checksum) newsum = parameter(:checksum).sum_file(path) return if [:absent, nil, content_checksum].include?(newsum) self.fail "File written to disk did not match checksum; discarding changes (#{content_checksum} vs #{newsum})" end # write the current content. Note that if there is no content property # simply opening the file with 'w' as done in write is enough to truncate # or write an empty length file. def write_content(file) (content = property(:content)) && content.write(file) end def write_temporary_file? # unfortunately we don't know the source file size before fetching it # so let's assume the file won't be empty (c = property(:content) and c.length) || @parameters[:source] end # There are some cases where all of the work does not get done on # file creation/modification, so we have to do some extra checking. def property_fix properties.each do |thing| next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name) # Make sure we get a new stat objct @stat = :needs_stat currentvalue = thing.retrieve thing.sync unless thing.safe_insync?(currentvalue) end end end # We put all of the properties in separate files, because there are so many # of them. The order these are loaded is important, because it determines # the order they are in the property lit. require 'puppet/type/file/checksum' require 'puppet/type/file/content' # can create the file require 'puppet/type/file/source' # can create the file require 'puppet/type/file/target' # creates a different type of file require 'puppet/type/file/ensure' # can create the file require 'puppet/type/file/owner' require 'puppet/type/file/group' require 'puppet/type/file/mode' require 'puppet/type/file/type' require 'puppet/type/file/selcontext' # SELinux file context require 'puppet/type/file/ctime' require 'puppet/type/file/mtime' diff --git a/lib/puppet/type/tidy.rb b/lib/puppet/type/tidy.rb index aecb1e609..c17707f7e 100644 --- a/lib/puppet/type/tidy.rb +++ b/lib/puppet/type/tidy.rb @@ -1,324 +1,324 @@ require 'puppet/parameter/boolean' Puppet::Type.newtype(:tidy) do require 'puppet/file_serving/fileset' require 'puppet/file_bucket/dipper' @doc = "Remove unwanted files based on specific criteria. Multiple criteria are OR'd together, so a file that is too large but is not old enough will still get tidied. If you don't specify either `age` or `size`, then all files will be removed. This resource type works by generating a file resource for every file that should be deleted and then letting that resource perform the actual deletion. " newparam(:path) do desc "The path to the file or directory to manage. Must be fully qualified." isnamevar end newparam(:recurse) do desc "If target is a directory, recursively descend into the directory looking for files to tidy." newvalues(:true, :false, :inf, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else raise ArgumentError, "Invalid recurse value #{value.inspect}" end end end newparam(:matches) do desc <<-'EOT' One or more (shell type) file glob patterns, which restrict the list of files to be tidied to those whose basenames match at least one of the patterns specified. Multiple patterns can be specified using an array. Example: tidy { "/tmp": age => "1w", recurse => 1, matches => [ "[0-9]pub*.tmp", "*.temp", "tmpfile?" ] } This removes files from `/tmp` if they are one week old or older, are not in a subdirectory and match one of the shell globs given. Note that the patterns are matched against the basename of each file -- that is, your glob patterns should not have any '/' characters in them, since you are only specifying against the last bit of the file. Finally, note that you must now specify a non-zero/non-false value for recurse if matches is used, as matches only apply to files found by recursion (there's no reason to use static patterns match against a statically determined path). Requiering explicit recursion clears up a common source of confusion. EOT # Make sure we convert to an array. munge do |value| fail "Tidy can't use matches with recurse 0, false, or undef" if "#{@resource[:recurse]}" =~ /^(0|false|)$/ [value].flatten end # Does a given path match our glob patterns, if any? Return true # if no patterns have been provided. def tidy?(path, stat) basename = File.basename(path) flags = File::FNM_DOTMATCH | File::FNM_PATHNAME return(value.find {|pattern| File.fnmatch(pattern, basename, flags) } ? true : false) end end newparam(:backup) do desc "Whether tidied files should be backed up. Any values are passed directly to the file resources used for actual file deletion, so consult the `file` type's backup documentation to determine valid values." end newparam(:age) do desc "Tidy files whose age is equal to or greater than the specified time. You can choose seconds, minutes, hours, days, or weeks by specifying the first letter of any of those words (e.g., '1w'). Specifying 0 will remove all files." AgeConvertors = { :s => 1, :m => 60, :h => 60 * 60, :d => 60 * 60 * 24, :w => 60 * 60 * 24 * 7, } def convert(unit, multi) if num = AgeConvertors[unit] return num * multi else self.fail "Invalid age unit '#{unit}'" end end def tidy?(path, stat) # If the file's older than we allow, we should get rid of it. (Time.now.to_i - stat.send(resource[:type]).to_i) > value end munge do |age| unit = multi = nil case age when /^([0-9]+)(\w)\w*$/ multi = Integer($1) unit = $2.downcase.intern when /^([0-9]+)$/ multi = Integer($1) unit = :d else self.fail "Invalid tidy age #{age}" end convert(unit, multi) end end newparam(:size) do desc "Tidy files whose size is equal to or greater than the specified size. Unqualified values are in kilobytes, but *b*, *k*, *m*, *g*, and *t* can be appended to specify *bytes*, *kilobytes*, *megabytes*, *gigabytes*, and *terabytes*, respectively. Only the first character is significant, so the full word can also be used." def convert(unit, multi) if num = { :b => 0, :k => 1, :m => 2, :g => 3, :t => 4 }[unit] result = multi num.times do result *= 1024 end return result else self.fail "Invalid size unit '#{unit}'" end end def tidy?(path, stat) stat.size >= value end munge do |size| case size when /^([0-9]+)(\w)\w*$/ multi = Integer($1) unit = $2.downcase.intern when /^([0-9]+)$/ multi = Integer($1) unit = :k else self.fail "Invalid tidy size #{age}" end convert(unit, multi) end end newparam(:type) do desc "Set the mechanism for determining age. Default: atime." newvalues(:atime, :mtime, :ctime) defaultto :atime end newparam(:rmdirs, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Tidy directories in addition to files; that is, remove directories whose age is older than the specified criteria. This will only remove empty directories, so all contained files must also be tidied before a directory gets removed." end # Erase PFile's validate method validate do end def self.instances [] end def depthfirst? true end def initialize(hash) super # only allow backing up into filebuckets self[:backup] = false unless self[:backup].is_a? Puppet::FileBucket::Dipper end # Make a file resource to remove a given file. def mkfile(path) # Force deletion, so directories actually get deleted. Puppet::Type.type(:file).new :path => path, :backup => self[:backup], :ensure => :absent, :force => true end def retrieve # Our ensure property knows how to retrieve everything for us. if obj = @parameters[:ensure] return obj.retrieve else return {} end end # Hack things a bit so we only ever check the ensure property. def properties [] end def generate return [] unless stat(self[:path]) case self[:recurse] when Integer, Fixnum, Bignum, /^\d+$/ parameter = { :recurse => true, :recurselimit => self[:recurse] } when true, :true, :inf parameter = { :recurse => true } end if parameter files = Puppet::FileServing::Fileset.new(self[:path], parameter).files.collect do |f| f == "." ? self[:path] : ::File.join(self[:path], f) end else files = [self[:path]] end result = files.find_all { |path| tidy?(path) }.collect { |path| mkfile(path) }.each { |file| notice "Tidying #{file.ref}" }.sort { |a,b| b[:path] <=> a[:path] } # No need to worry about relationships if we don't have rmdirs; there won't be # any directories. return result unless rmdirs? # Now make sure that all directories require the files they contain, if all are available, # so that a directory is emptied before we try to remove it. files_by_name = result.inject({}) { |hash, file| hash[file[:path]] = file; hash } - files_by_name.keys.sort { |a,b| b <=> b }.each do |path| + files_by_name.keys.sort { |a,b| b <=> a }.each do |path| dir = ::File.dirname(path) next unless resource = files_by_name[dir] if resource[:require] resource[:require] << Puppet::Resource.new(:file, path) else resource[:require] = [Puppet::Resource.new(:file, path)] end end result end # Does a given path match our glob patterns, if any? Return true # if no patterns have been provided. def matches?(path) return true unless self[:matches] basename = File.basename(path) flags = File::FNM_DOTMATCH | File::FNM_PATHNAME if self[:matches].find {|pattern| File.fnmatch(pattern, basename, flags) } return true else debug "No specified patterns match #{path}, not tidying" return false end end # Should we remove the specified file? def tidy?(path) return false unless stat = self.stat(path) return false if stat.ftype == "directory" and ! rmdirs? # The 'matches' parameter isn't OR'ed with the other tests -- # it's just used to reduce the list of files we can match. return false if param = parameter(:matches) and ! param.tidy?(path, stat) tested = false [:age, :size].each do |name| next unless param = parameter(name) tested = true return true if param.tidy?(path, stat) end # If they don't specify either, then the file should always be removed. return true unless tested false end def stat(path) begin Puppet::FileSystem.lstat(path) rescue Errno::ENOENT => error info "File does not exist" return nil rescue Errno::EACCES => error warning "Could not stat; permission denied" return nil end end end diff --git a/lib/puppet/util/command_line.rb b/lib/puppet/util/command_line.rb index 22fd3532a..f10ecada5 100644 --- a/lib/puppet/util/command_line.rb +++ b/lib/puppet/util/command_line.rb @@ -1,199 +1,199 @@ # Bundler and rubygems maintain a set of directories from which to # load gems. If Bundler is loaded, let it determine what can be # loaded. If it's not loaded, then use rubygems. But do this before # loading any puppet code, so that our gem loading system is sane. if not defined? ::Bundler begin require 'rubygems' rescue LoadError end end require 'puppet' require 'puppet/util' require "puppet/util/plugins" require "puppet/util/rubygems" require "puppet/util/limits" require 'puppet/util/colors' module Puppet module Util # This is the main entry point for all puppet applications / faces; it # is basically where the bootstrapping process / lifecycle of an app # begins. class CommandLine include Puppet::Util::Limits - OPTION_OR_MANIFEST_FILE = /^-|\.pp$|\.rb$/ + OPTION_OR_MANIFEST_FILE = /^-|\.pp$/ # @param zero [String] the name of the executable # @param argv [Array] the arguments passed on the command line # @param stdin [IO] (unused) def initialize(zero = $0, argv = ARGV, stdin = STDIN) @command = File.basename(zero, '.rb') @argv = argv Puppet::Plugins.on_commandline_initialization(:command_line_object => self) end # @return [String] name of the subcommand is being executed # @api public def subcommand_name return @command if @command != 'puppet' if @argv.first =~ OPTION_OR_MANIFEST_FILE nil else @argv.first end end # @return [Array] the command line arguments being passed to the subcommand # @api public def args return @argv if @command != 'puppet' if subcommand_name.nil? @argv else @argv[1..-1] end end # @api private # @deprecated def self.available_subcommands Puppet.deprecation_warning('Puppet::Util::CommandLine.available_subcommands is deprecated; please use Puppet::Application.available_application_names instead.') Puppet::Application.available_application_names end # available_subcommands was previously an instance method, not a class # method, and we have an unknown number of user-implemented applications # that depend on that behaviour. Forwarding allows us to preserve a # backward compatible API. --daniel 2011-04-11 # @api private # @deprecated def available_subcommands Puppet.deprecation_warning('Puppet::Util::CommandLine#available_subcommands is deprecated; please use Puppet::Application.available_application_names instead.') Puppet::Application.available_application_names end # Run the puppet subcommand. If the subcommand is determined to be an # external executable, this method will never return and the current # process will be replaced via {Kernel#exec}. # # @return [void] def execute Puppet::Util.exit_on_fail("initialize global default settings") do Puppet.initialize_settings(args) end setpriority(Puppet[:priority]) find_subcommand.run end # @api private def external_subcommand Puppet::Util.which("puppet-#{subcommand_name}") end private def find_subcommand if subcommand_name.nil? NilSubcommand.new(self) elsif Puppet::Application.available_application_names.include?(subcommand_name) ApplicationSubcommand.new(subcommand_name, self) elsif path_to_subcommand = external_subcommand ExternalSubcommand.new(path_to_subcommand, self) else UnknownSubcommand.new(subcommand_name, self) end end # @api private class ApplicationSubcommand def initialize(subcommand_name, command_line) @subcommand_name = subcommand_name @command_line = command_line end def run # For most applications, we want to be able to load code from the modulepath, # such as apply, describe, resource, and faces. # For agent, we only want to load pluginsync'ed code from libdir. # For master, we shouldn't ever be loading per-enviroment code into the master's # ruby process, but that requires fixing (#17210, #12173, #8750). So for now # we try to restrict to only code that can be autoloaded from the node's # environment. # PUP-2114 - at this point in the bootstrapping process we do not # have an appropriate application-wide current_environment set. # If we cannot find the configured environment, which may not exist, # we do not attempt to add plugin directories to the load path. # if @subcommand_name != 'master' and @subcommand_name != 'agent' if configured_environment = Puppet.lookup(:environments).get(Puppet[:environment]) configured_environment.each_plugin_directory do |dir| $LOAD_PATH << dir unless $LOAD_PATH.include?(dir) end end end app = Puppet::Application.find(@subcommand_name).new(@command_line) Puppet::Plugins.on_application_initialization(:application_object => @command_line) app.run end end # @api private class ExternalSubcommand def initialize(path_to_subcommand, command_line) @path_to_subcommand = path_to_subcommand @command_line = command_line end def run Kernel.exec(@path_to_subcommand, *@command_line.args) end end # @api private class NilSubcommand include Puppet::Util::Colors def initialize(command_line) @command_line = command_line end def run args = @command_line.args if args.include? "--version" or args.include? "-V" puts Puppet.version elsif @command_line.subcommand_name.nil? && args.count > 0 # If the subcommand is truly nil and there is an arg, it's an option; print out the invalid option message puts colorize(:hred, "Error: Could not parse application options: invalid option: #{args[0]}") exit 1 else puts "See 'puppet help' for help on available puppet subcommands" end end end # @api private class UnknownSubcommand < NilSubcommand def initialize(subcommand_name, command_line) @subcommand_name = subcommand_name super(command_line) end def run puts colorize(:hred, "Error: Unknown Puppet subcommand '#{@subcommand_name}'") super exit 1 end end end end end diff --git a/spec/fixtures/unit/pops/binder/config/binder_config/nolayer/binder_config.yaml b/spec/fixtures/unit/pops/binder/config/binder_config/nolayer/binder_config.yaml new file mode 100644 index 000000000..08913a746 --- /dev/null +++ b/spec/fixtures/unit/pops/binder/config/binder_config/nolayer/binder_config.yaml @@ -0,0 +1,6 @@ +--- +version: 1 +categories: + - ['node', '$fqn'] + - ['environment', '$environment'] + - ['common', 'true'] diff --git a/spec/integration/parser/ruby_manifest_spec.rb b/spec/integration/parser/ruby_manifest_spec.rb deleted file mode 100755 index 29e9c6379..000000000 --- a/spec/integration/parser/ruby_manifest_spec.rb +++ /dev/null @@ -1,119 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' - -require 'tempfile' -require 'puppet_spec/files' - -describe "Pure ruby manifests" do - include PuppetSpec::Files - - before do - @test_dir = tmpdir('ruby_manifest_test') - end - - def write_file(name, contents) - path = File.join(@test_dir, name) - File.open(path, "w") { |f| f.write(contents) } - path - end - - def compile(contents) - Puppet[:code] = contents - Dir.chdir(@test_dir) do - Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) - end - end - - it "should allow classes" do - write_file('foo.rb', ["hostclass 'one' do notify('one_notify') end", - "hostclass 'two' do notify('two_notify') end"].join("\n")) - catalog = compile("import 'foo'\ninclude one") - catalog.resource("Notify[one_notify]").should_not be_nil - catalog.resource("Notify[two_notify]").should be_nil - end - - it "should allow defines" do - write_file('foo.rb', 'define "bar", :arg do notify("bar_#{@name}_#{@arg}") end') - catalog = compile("import 'foo'\nbar { instance: arg => 'xyz' }") - catalog.resource("Notify[bar_instance_xyz]").should_not be_nil - catalog.resource("Bar[instance]").should_not be_nil - end - - it "should allow node declarations" do - write_file('foo.rb', "node 'mynode' do notify('mynode') end") - catalog = compile("import 'foo'") - node_declaration = catalog.resource("Notify[mynode]") - node_declaration.should_not be_nil - node_declaration.title.should == 'mynode' - end - - it "should allow access to the environment" do - write_file('foo.rb', ["hostclass 'bar' do", - " if environment.is_a? Puppet::Node::Environment", - " notify('success')", - " end", - "end"].join("\n")) - compile("import 'foo'\ninclude bar").resource("Notify[success]").should_not be_nil - end - - it "should allow creation of resources of built-in types" do - write_file('foo.rb', "hostclass 'bar' do file 'test_file', :owner => 'root', :mode => '644' end") - catalog = compile("import 'foo'\ninclude bar") - file = catalog.resource("File[test_file]") - file.should be_a(Puppet::Resource) - file.type.should == 'File' - file.title.should == 'test_file' - file.exported.should_not be - file.virtual.should_not be - file[:owner].should == 'root' - file[:mode].should == '644' - file[:stage].should be_nil # TODO: is this correct behavior? - end - - it "should allow calling user-defined functions" do - write_file('foo.rb', "hostclass 'bar' do user_func 'name', :arg => 'xyz' end") - catalog = compile(['define user_func($arg) { notify {"n_$arg": } }', - 'import "foo"', - 'include bar'].join("\n")) - catalog.resource("Notify[n_xyz]").should_not be_nil - catalog.resource("User_func[name]").should_not be_nil - end - - it "should be properly cached for multiple compiles" do - # Note: we can't test this by calling compile() twice, because - # that sets Puppet[:code], which clears out all cached - # environments. - Puppet[:filetimeout] = 1000 - write_file('foo.rb', "hostclass 'bar' do notify('success') end") - Puppet[:code] = "import 'foo'\ninclude bar" - - # Compile the catalog and check it - catalog = Dir.chdir(@test_dir) do - Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) - end - catalog.resource("Notify[success]").should_not be_nil - - # Secretly change the file to make it invalid. This change - # shouldn't be noticed because the we've set a high - # Puppet[:filetimeout]. - write_file('foo.rb', "raise 'should not be executed'") - - # Compile the catalog a second time and make sure it's still ok. - catalog = Dir.chdir(@test_dir) do - Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) - end - catalog.resource("Notify[success]").should_not be_nil - end - - it "should be properly reloaded when stale" do - Puppet[:filetimeout] = -1 # force stale check to happen all the time - write_file('foo.rb', "hostclass 'bar' do notify('version1') end") - catalog = compile("import 'foo'\ninclude bar") - catalog.resource("Notify[version1]").should_not be_nil - sleep 1 # so that timestamp will change forcing file reload - write_file('foo.rb', "hostclass 'bar' do notify('version2') end") - catalog = compile("import 'foo'\ninclude bar") - catalog.resource("Notify[version1]").should be_nil - catalog.resource("Notify[version2]").should_not be_nil - end -end diff --git a/spec/unit/application/agent_spec.rb b/spec/unit/application/agent_spec.rb index c6fbba48f..17dc72d9b 100755 --- a/spec/unit/application/agent_spec.rb +++ b/spec/unit/application/agent_spec.rb @@ -1,656 +1,655 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/agent' require 'puppet/application/agent' require 'puppet/network/server' require 'puppet/daemon' describe Puppet::Application::Agent do include PuppetSpec::Files before :each do @puppetd = Puppet::Application[:agent] @daemon = Puppet::Daemon.new(nil) @daemon.stubs(:daemonize) @daemon.stubs(:start) @daemon.stubs(:stop) Puppet::Daemon.stubs(:new).returns(@daemon) Puppet[:daemonize] = false @agent = stub_everything 'agent' Puppet::Agent.stubs(:new).returns(@agent) @puppetd.preinit Puppet::Util::Log.stubs(:newdestination) @ssl_host = stub_everything 'ssl host' Puppet::SSL::Host.stubs(:new).returns(@ssl_host) Puppet::Node.indirection.stubs(:terminus_class=) Puppet::Node.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:terminus_class=) $stderr.expects(:puts).never Puppet.settings.stubs(:use) end it "should operate in agent run_mode" do @puppetd.class.run_mode.name.should == :agent end it "should declare a main command" do @puppetd.should respond_to(:main) end it "should declare a onetime command" do @puppetd.should respond_to(:onetime) end it "should declare a fingerprint command" do @puppetd.should respond_to(:fingerprint) end it "should declare a preinit block" do @puppetd.should respond_to(:preinit) end describe "in preinit" do it "should catch INT" do Signal.expects(:trap).with { |arg,block| arg == :INT } @puppetd.preinit end it "should init client to true" do @puppetd.preinit @puppetd.options[:client].should be_true end it "should init fqdn to nil" do @puppetd.preinit @puppetd.options[:fqdn].should be_nil end it "should init serve to []" do @puppetd.preinit @puppetd.options[:serve].should == [] end it "should use SHA256 as default digest algorithm" do @puppetd.preinit @puppetd.options[:digest].should == 'SHA256' end it "should not fingerprint by default" do @puppetd.preinit @puppetd.options[:fingerprint].should be_false end it "should init waitforcert to nil" do @puppetd.preinit @puppetd.options[:waitforcert].should be_nil end end describe "when handling options" do before do @puppetd.command_line.stubs(:args).returns([]) end [:enable, :debug, :fqdn, :test, :verbose, :digest].each do |option| it "should declare handle_#{option} method" do @puppetd.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @puppetd.send("handle_#{option}".to_sym, 'arg') @puppetd.options[option].should == 'arg' end end describe "when handling --disable" do it "should set disable to true" do @puppetd.handle_disable('') @puppetd.options[:disable].should == true end it "should store disable message" do @puppetd.handle_disable('message') @puppetd.options[:disable_message].should == 'message' end end it "should set client to false with --no-client" do @puppetd.handle_no_client(nil) @puppetd.options[:client].should be_false end it "should set waitforcert to 0 with --onetime and if --waitforcert wasn't given" do @agent.stubs(:run).returns(2) Puppet[:onetime] = true @ssl_host.expects(:wait_for_cert).with(0) expect { execute_agent }.to exit_with 0 end it "should use supplied waitforcert when --onetime is specified" do @agent.stubs(:run).returns(2) Puppet[:onetime] = true @puppetd.handle_waitforcert(60) @ssl_host.expects(:wait_for_cert).with(60) expect { execute_agent }.to exit_with 0 end it "should use a default value for waitforcert when --onetime and --waitforcert are not specified" do @ssl_host.expects(:wait_for_cert).with(120) execute_agent end it "should use the waitforcert setting when checking for a signed certificate" do Puppet[:waitforcert] = 10 @ssl_host.expects(:wait_for_cert).with(10) execute_agent end it "should set the log destination with --logdest" do Puppet::Log.expects(:newdestination).with("console") @puppetd.handle_logdest("console") end it "should put the setdest options to true" do @puppetd.handle_logdest("console") @puppetd.options[:setdest].should == true end it "should parse the log destination from the command line" do @puppetd.command_line.stubs(:args).returns(%w{--logdest /my/file}) Puppet::Util::Log.expects(:newdestination).with("/my/file") @puppetd.parse_options end it "should store the waitforcert options with --waitforcert" do @puppetd.handle_waitforcert("42") @puppetd.options[:waitforcert].should == 42 end end describe "during setup" do before :each do Puppet.stubs(:info) Puppet[:libdir] = "/dev/null/lib" Puppet::Transaction::Report.indirection.stubs(:terminus_class=) Puppet::Transaction::Report.indirection.stubs(:cache_class=) Puppet::Resource::Catalog.indirection.stubs(:terminus_class=) Puppet::Resource::Catalog.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:terminus_class=) Puppet.stubs(:settraps) end describe "with --test" do it "should call setup_test" do @puppetd.options[:test] = true @puppetd.expects(:setup_test) @puppetd.setup end it "should set options[:verbose] to true" do @puppetd.setup_test @puppetd.options[:verbose].should == true end it "should set options[:onetime] to true" do Puppet[:onetime] = false @puppetd.setup_test Puppet[:onetime].should == true end it "should set options[:detailed_exitcodes] to true" do @puppetd.setup_test @puppetd.options[:detailed_exitcodes].should == true end end it "should call setup_logs" do @puppetd.expects(:setup_logs) @puppetd.setup end describe "when setting up logs" do before :each do Puppet::Util::Log.stubs(:newdestination) end it "should set log level to debug if --debug was passed" do @puppetd.options[:debug] = true @puppetd.setup_logs Puppet::Util::Log.level.should == :debug end it "should set log level to info if --verbose was passed" do @puppetd.options[:verbose] = true @puppetd.setup_logs Puppet::Util::Log.level.should == :info end [:verbose, :debug].each do |level| it "should set console as the log destination with level #{level}" do @puppetd.options[level] = true Puppet::Util::Log.expects(:newdestination).at_least_once Puppet::Util::Log.expects(:newdestination).with(:console).once @puppetd.setup_logs end end it "should set a default log destination if no --logdest" do @puppetd.options[:setdest] = false Puppet::Util::Log.expects(:setup_default) @puppetd.setup_logs end end it "should print puppet config if asked to in Puppet config" do Puppet[:configprint] = "pluginsync" Puppet.settings.expects(:print_configs).returns true expect { execute_agent }.to exit_with 0 end it "should exit after printing puppet config if asked to in Puppet config" do path = make_absolute('/my/path') Puppet[:modulepath] = path Puppet[:configprint] = "modulepath" Puppet::Settings.any_instance.expects(:puts).with(path) expect { execute_agent }.to exit_with 0 end it "should use :main, :puppetd, and :ssl" do Puppet.settings.unstub(:use) Puppet.settings.expects(:use).with(:main, :agent, :ssl) @puppetd.setup end it "should install a remote ca location" do Puppet::SSL::Host.expects(:ca_location=).with(:remote) @puppetd.setup end it "should install a none ca location in fingerprint mode" do @puppetd.options[:fingerprint] = true Puppet::SSL::Host.expects(:ca_location=).with(:none) @puppetd.setup end it "should tell the report handler to use REST" do Puppet::Transaction::Report.indirection.expects(:terminus_class=).with(:rest) @puppetd.setup end it "should tell the report handler to cache locally as yaml" do Puppet::Transaction::Report.indirection.expects(:cache_class=).with(:yaml) @puppetd.setup end it "should default catalog_terminus setting to 'rest'" do @puppetd.initialize_app_defaults Puppet[:catalog_terminus].should == :rest end it "should default node_terminus setting to 'rest'" do @puppetd.initialize_app_defaults Puppet[:node_terminus].should == :rest end it "has an application default :catalog_cache_terminus setting of 'json'" do Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:json) @puppetd.initialize_app_defaults @puppetd.setup end it "should tell the catalog cache class based on the :catalog_cache_terminus setting" do Puppet[:catalog_cache_terminus] = "yaml" Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:yaml) @puppetd.initialize_app_defaults @puppetd.setup end it "should not set catalog cache class if :catalog_cache_terminus is explicitly nil" do Puppet[:catalog_cache_terminus] = nil Puppet::Resource::Catalog.indirection.expects(:cache_class=).never @puppetd.initialize_app_defaults @puppetd.setup end it "should default facts_terminus setting to 'facter'" do @puppetd.initialize_app_defaults Puppet[:facts_terminus].should == :facter end it "should create an agent" do Puppet::Agent.stubs(:new).with(Puppet::Configurer) @puppetd.setup end [:enable, :disable].each do |action| it "should delegate to enable_disable_client if we #{action} the agent" do @puppetd.options[action] = true @puppetd.expects(:enable_disable_client).with(@agent) @puppetd.setup end end describe "when enabling or disabling agent" do [:enable, :disable].each do |action| it "should call client.#{action}" do @puppetd.options[action] = true @agent.expects(action) expect { execute_agent }.to exit_with 0 end end it "should pass the disable message when disabling" do @puppetd.options[:disable] = true @puppetd.options[:disable_message] = "message" @agent.expects(:disable).with("message") expect { execute_agent }.to exit_with 0 end it "should pass the default disable message when disabling without a message" do @puppetd.options[:disable] = true @puppetd.options[:disable_message] = nil @agent.expects(:disable).with("reason not specified") expect { execute_agent }.to exit_with 0 end end it "should inform the daemon about our agent if :client is set to 'true'" do @puppetd.options[:client] = true execute_agent @daemon.agent.should == @agent end it "should not inform the daemon about our agent if :client is set to 'false'" do @puppetd.options[:client] = false execute_agent @daemon.agent.should be_nil end it "should daemonize if needed" do Puppet.features.stubs(:microsoft_windows?).returns false Puppet[:daemonize] = true @daemon.expects(:daemonize) execute_agent end it "should wait for a certificate" do @puppetd.options[:waitforcert] = 123 @ssl_host.expects(:wait_for_cert).with(123) execute_agent end it "should not wait for a certificate in fingerprint mode" do @puppetd.options[:fingerprint] = true @puppetd.options[:waitforcert] = 123 @puppetd.options[:digest] = 'MD5' certificate = mock 'certificate' certificate.stubs(:digest).with('MD5').returns('ABCDE') @ssl_host.stubs(:certificate).returns(certificate) @ssl_host.expects(:wait_for_cert).never @puppetd.expects(:puts).with('ABCDE') execute_agent end describe "when setting up listen" do before :each do Puppet::FileSystem.stubs(:exist?).with(Puppet[:rest_authconfig]).returns(true) @puppetd.options[:serve] = [] @server = stub_everything 'server' Puppet::Network::Server.stubs(:new).returns(@server) end it "should exit if no authorization file" do Puppet[:listen] = true Puppet.stubs(:err) Puppet::FileSystem.stubs(:exist?).with(Puppet[:rest_authconfig]).returns(false) expect do execute_agent end.to exit_with 14 end it "should use puppet default port" do Puppet[:puppetport] = 32768 Puppet[:listen] = true Puppet::Network::Server.expects(:new).with(anything, 32768) execute_agent end it "should issue a warning that listen is deprecated" do Puppet[:listen] = true Puppet.expects(:warning).with() { |msg| msg =~ /kick is deprecated/ } execute_agent end end describe "when setting up for fingerprint" do before(:each) do @puppetd.options[:fingerprint] = true end it "should not setup as an agent" do @puppetd.expects(:setup_agent).never @puppetd.setup end it "should not create an agent" do Puppet::Agent.stubs(:new).with(Puppet::Configurer).never @puppetd.setup end it "should not daemonize" do @daemon.expects(:daemonize).never @puppetd.setup end end describe "when configuring agent for catalog run" do it "should set should_fork as true when running normally" do Puppet::Agent.expects(:new).with(anything, true) @puppetd.setup end it "should not set should_fork as false for --onetime" do Puppet[:onetime] = true Puppet::Agent.expects(:new).with(anything, false) @puppetd.setup end end end describe "when running" do before :each do @puppetd.options[:fingerprint] = false end it "should dispatch to fingerprint if --fingerprint is used" do @puppetd.options[:fingerprint] = true @puppetd.stubs(:fingerprint) execute_agent end it "should dispatch to onetime if --onetime is used" do @puppetd.options[:onetime] = true @puppetd.stubs(:onetime) execute_agent end it "should dispatch to main if --onetime and --fingerprint are not used" do @puppetd.options[:onetime] = false @puppetd.stubs(:main) execute_agent end describe "with --onetime" do before :each do @agent.stubs(:run).returns(:report) Puppet[:onetime] = true @puppetd.options[:client] = :client @puppetd.options[:detailed_exitcodes] = false - Puppet.stubs(:newservice) end it "should exit if no defined --client" do @puppetd.options[:client] = nil Puppet.expects(:err).with('onetime is specified but there is no client') expect { execute_agent }.to exit_with 43 end it "should setup traps" do @daemon.expects(:set_signal_traps) expect { execute_agent }.to exit_with 0 end it "should let the agent run" do @agent.expects(:run).returns(:report) expect { execute_agent }.to exit_with 0 end it "should stop the daemon" do @daemon.expects(:stop).with(:exit => false) expect { execute_agent }.to exit_with 0 end describe "and --detailed-exitcodes" do before :each do @puppetd.options[:detailed_exitcodes] = true end it "should exit with agent computed exit status" do Puppet[:noop] = false @agent.stubs(:run).returns(666) expect { execute_agent }.to exit_with 666 end it "should exit with the agent's exit status, even if --noop is set." do Puppet[:noop] = true @agent.stubs(:run).returns(666) expect { execute_agent }.to exit_with 666 end end end describe "with --fingerprint" do before :each do @cert = mock 'cert' @puppetd.options[:fingerprint] = true @puppetd.options[:digest] = :MD5 end it "should fingerprint the certificate if it exists" do @ssl_host.stubs(:certificate).returns(@cert) @cert.stubs(:digest).with('MD5').returns "fingerprint" @puppetd.expects(:puts).with "fingerprint" @puppetd.fingerprint end it "should fingerprint the certificate request if no certificate have been signed" do @ssl_host.stubs(:certificate).returns(nil) @ssl_host.stubs(:certificate_request).returns(@cert) @cert.stubs(:digest).with('MD5').returns "fingerprint" @puppetd.expects(:puts).with "fingerprint" @puppetd.fingerprint end end describe "without --onetime and --fingerprint" do before :each do Puppet.stubs(:notice) @puppetd.options[:client] = nil end it "should start our daemon" do @daemon.expects(:start) execute_agent end end end def execute_agent @puppetd.setup @puppetd.run_command end end diff --git a/spec/unit/dsl/resource_api_spec.rb b/spec/unit/dsl/resource_api_spec.rb deleted file mode 100755 index d5be2561a..000000000 --- a/spec/unit/dsl/resource_api_spec.rb +++ /dev/null @@ -1,180 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' - -require 'puppet/dsl/resource_api' - -describe Puppet::DSL::ResourceAPI do - before do - @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("foo")) - @scope = Puppet::Parser::Scope.new(@compiler, :source => "foo") - @resource = Puppet::Parser::Resource.new(:mytype, "myresource", :scope => @scope) - @api = Puppet::DSL::ResourceAPI.new(@resource, @scope, proc { }) - end - - it "should include the resource type collection helper" do - Puppet::DSL::ResourceAPI.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper) - end - - it "should use the scope's environment as its environment" do - @scope.expects(:environment).returns "myenv" - @api.environment.should == "myenv" - end - - it "should be able to set all of its parameters as instance variables" do - @resource["foo"] = "myval" - @api.set_instance_variables - @api.instance_variable_get("@foo").should == "myval" - end - - describe "when calling a function" do - it "should return false if the function does not exist" do - Puppet::Parser::Functions.expects(:function).with("myfunc").returns nil - @api.call_function("myfunc", "foo").should be_false - end - - it "should use the scope the call the provided function with the provided arguments and return the results" do - scope = stub 'scope' - @api.stubs(:scope).returns scope - Puppet::Parser::Functions.expects(:function).with("myfunc").returns "myfunc_method" - - scope.expects(:myfunc_method).with("one", "two") - @api.call_function("myfunc", ["one", "two"]) - end - - it "should call 'include' when asked to call 'acquire'" do - scope = stub 'scope' - @api.stubs(:scope).returns scope - @api.stubs(:valid_type?).returns false - - scope.expects(:function_include).with("one", "two") - @api.acquire("one", "two") - end - end - - describe "when determining if a provided name is a valid type" do - it "should be valid if it's :class" do - @api.should be_valid_type(:class) - end - - it "should be valid if it's :node" do - @api.should be_valid_type(:node) - end - - it "should be valid if it's a builtin type" do - Puppet::Type.expects(:type).with(:mytype).returns "whatever" - @api.should be_valid_type(:mytype) - end - - it "should be valid if it's a defined resource type in the environment's known resource types" do - collection = stub 'collection' - @api.stubs(:known_resource_types).returns collection - collection.expects(:definition).with(:mytype).returns "whatever" - @api.should be_valid_type(:mytype) - end - - it "should not be valid unless it's a node, class, builtin type, or defined resource" do - collection = stub 'collection' - @api.stubs(:known_resource_types).returns collection - collection.expects(:definition).returns nil - Puppet::Type.expects(:type).returns nil - @api.should_not be_valid_type(:mytype) - end - end - - describe "when creating a resource" do - before do - @api.scope.stubs(:source).returns stub("source") - @api.scope.compiler.stubs(:add_resource) - @created_resource = Puppet::Parser::Resource.new("yay", "eh", :scope => @api.scope) - end - - it "should create and return a resource of the type specified" do - Puppet::Parser::Resource.expects(:new).with { |type, title, args| type == "mytype" }.returns @created_resource - @api.create_resource("mytype", "myname", {:foo => "bar"}).should == [@created_resource] - end - - it "should use the name from the first element of the provided argument array" do - Puppet::Parser::Resource.expects(:new).with { |type, title, args| title == "myname" }.returns @created_resource - @api.create_resource("mytype", "myname", {:foo => "bar"}) - end - - it "should create multiple resources if the first element of the argument array is an array" do - second_resource = Puppet::Parser::Resource.new('yay', "eh", :scope => @api.scope) - Puppet::Parser::Resource.expects(:new).with { |type, title, args| title == "first" }.returns @created_resource - Puppet::Parser::Resource.expects(:new).with { |type, title, args| title == "second" }.returns @created_resource - @api.create_resource("mytype", ["first", "second"], {:foo => "bar"}) - end - - it "should provide its scope as the scope" do - Puppet::Parser::Resource.expects(:new).with { |type, title, args| args[:scope] == @api.scope }.returns @created_resource - @api.create_resource("mytype", "myname", {:foo => "bar"}) - end - - it "should set each provided argument as a parameter on the created resource" do - result = @api.create_resource("mytype", "myname", {"foo" => "bar", "biz" => "baz"}).shift - result["foo"].should == "bar" - result["biz"].should == "baz" - end - - it "should add the resource to the scope's copmiler" do - Puppet::Parser::Resource.expects(:new).returns @created_resource - @api.scope.compiler.expects(:add_resource).with(@api.scope, @created_resource) - @api.create_resource("mytype", "myname", {:foo => "bar"}) - end - - it "should fail if the resource parameters are not a hash" do - lambda { @api.create_resource("mytype", "myname", %w{foo bar}) }.should raise_error(ArgumentError) - end - end - - describe "when an unknown method is called" do - it "should create a resource if the method name is a valid type" do - @api.expects(:valid_type?).with(:mytype).returns true - @api.expects(:create_resource).with(:mytype, "myname", {:foo => "bar"}).returns true - - @api.mytype("myname", :foo => "bar") - end - - it "should call any function whose name matches the undefined method if the name is not a valid type" do - @api.expects(:valid_type?).with(:myfunc).returns false - @api.expects(:create_resource).never - - Puppet::Parser::Functions.expects(:function).with(:myfunc).returns true - - @api.expects(:call_function).with(:myfunc, %w{foo bar}) - - @api.myfunc("foo", "bar") - end - - it "should raise a method missing error if the method is neither a type nor a function" do - @api.expects(:valid_type?).with(:myfunc).returns false - @api.expects(:create_resource).never - - Puppet::Parser::Functions.expects(:function).with(:myfunc).returns false - - @api.expects(:call_function).never - - lambda { @api.myfunc("foo", "bar") }.should raise_error(NoMethodError) - end - end - - it "should mark the specified resource as exported when creating a single exported resource" do - resources = @api.export @api.file("/my/file", :ensure => :present) - resources[0].should be_exported - end - - it "should mark all created resources as exported when creating exported resources using a block" do - @compiler.expects(:add_resource).with { |s, res| res.exported == true } - @api.export { file "/my/file", :ensure => :present } - end - - it "should mark the specified resource as virtual when creating a single virtual resource" do - resources = @api.virtual @api.file("/my/file", :ensure => :present) - resources[0].should be_virtual - end - - it "should mark all created resources as virtual when creating virtual resources using a block" do - @compiler.expects(:add_resource).with { |s, res| res.virtual == true } - @api.virtual { file "/my/file", :ensure => :present } - end -end diff --git a/spec/unit/dsl/resource_type_api_spec.rb b/spec/unit/dsl/resource_type_api_spec.rb deleted file mode 100755 index c8c4e3ef3..000000000 --- a/spec/unit/dsl/resource_type_api_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' - -require 'puppet/dsl/resource_type_api' - -describe Puppet::DSL::ResourceTypeAPI do - # Verify that the block creates a single AST node through the API, - # instantiate that AST node into a types, and return that type. - def test_api_call(&block) - main_object = Puppet::DSL::ResourceTypeAPI.new - main_object.instance_eval(&block) - created_ast_objects = main_object.instance_eval { @__created_ast_objects__ } - created_ast_objects.length.should == 1 - new_types = created_ast_objects[0].instantiate('') - new_types.length.should == 1 - new_types[0] - ensure - Thread.current[:ruby_file_parse_result] = nil - end - - [:definition, :node, :hostclass].each do |type| - method = type == :definition ? "define" : type - it "should be able to create a #{type}" do - newtype = test_api_call { send(method, "myname").should == nil } - newtype.should be_a(Puppet::Resource::Type) - newtype.type.should == type - end - - it "should use the provided name when creating a #{type}" do - newtype = test_api_call { send(method, "myname") } - newtype.name.should == "myname" - end - - unless type == :definition - it "should pass in any provided options when creating a #{type}" do - newtype = test_api_call { send(method, "myname", :line => 200) } - newtype.line.should == 200 - end - end - - it "should set any provided block as the type's ruby code" do - newtype = test_api_call { send(method, "myname") { 'method_result' } } - newtype.ruby_code.call.should == 'method_result' - end - end - - describe "when creating a definition" do - it "should use the provided options to define valid arguments for the resource type" do - newtype = test_api_call { define("myname", :arg1, :arg2) } - newtype.arguments.should == { 'arg1' => nil, 'arg2' => nil } - end - end -end diff --git a/spec/unit/parser/functions/search_spec.rb b/spec/unit/parser/functions/search_spec.rb deleted file mode 100755 index 54054bd6a..000000000 --- a/spec/unit/parser/functions/search_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' - -describe "the 'search' function" do - before :all do - Puppet::Parser::Functions.autoloader.loadall - end - - let :node do Puppet::Node.new('localhost') end - let :compiler do Puppet::Parser::Compiler.new(node) end - let :scope do Puppet::Parser::Scope.new(compiler) end - - it "should exist" do - Puppet::Parser::Functions.function("search").should == "function_search" - end - - it "should invoke #add_namespace on the scope for all inputs" do - scope.expects(:add_namespace).with("where") - scope.expects(:add_namespace).with("what") - scope.expects(:add_namespace).with("who") - scope.function_search(["where", "what", "who"]) - end - - it "is deprecated" do - Puppet.expects(:deprecation_warning).with("The 'search' function is deprecated. See http://links.puppetlabs.com/search-function-deprecation") - scope.function_search(['wat']) - end -end diff --git a/spec/unit/parser/parser_spec.rb b/spec/unit/parser/parser_spec.rb index eb9370292..71e682563 100755 --- a/spec/unit/parser/parser_spec.rb +++ b/spec/unit/parser/parser_spec.rb @@ -1,535 +1,521 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Parser do Puppet::Parser::AST before :each do @known_resource_types = Puppet::Resource::TypeCollection.new("development") @parser = Puppet::Parser::Parser.new "development" @parser.stubs(:known_resource_types).returns @known_resource_types @true_ast = Puppet::Parser::AST::Boolean.new :value => true end it "should require an environment at initialization" do expect { Puppet::Parser::Parser.new }.to raise_error(ArgumentError, /wrong number of arguments/) end it "should set the environment" do env = Puppet::Node::Environment.create(:testing, []) Puppet::Parser::Parser.new(env).environment.should == env end it "should be able to look up the environment-specific resource type collection" do env = Puppet::Node::Environment.create(:development, []) rtc = env.known_resource_types parser = Puppet::Parser::Parser.new env parser.known_resource_types.should equal(rtc) end context "when importing" do it "uses the directory of the currently parsed file" do @parser.lexer.stubs(:file).returns "/tmp/current_file" @parser.known_resource_types.loader.expects(:import).with("newfile", "/tmp") @parser.import("newfile") end it "uses the current working directory, when there is no file being parsed" do @parser.known_resource_types.loader.expects(:import).with('one', Dir.pwd) @parser.known_resource_types.loader.expects(:import).with('two', Dir.pwd) @parser.parse("import 'one', 'two'") end end - describe "when parsing files" do - before do - Puppet::FileSystem.stubs(:exist?).returns true - Puppet::FileSystem.stubs(:read).returns "" - @parser.stubs(:watch_file) - end - - it "should treat files ending in 'rb' as ruby files" do - @parser.expects(:parse_ruby_file) - @parser.file = "/my/file.rb" - @parser.parse - end - end - describe "when parsing append operator" do it "should not raise syntax errors" do expect { @parser.parse("$var += something") }.to_not raise_error end it "should raise syntax error on incomplete syntax " do expect { @parser.parse("$var += ") }.to raise_error(Puppet::ParseError, /Syntax error at end of file/) end it "should create ast::VarDef with append=true" do vardef = @parser.parse("$var += 2").code[0] vardef.should be_a(Puppet::Parser::AST::VarDef) vardef.append.should == true end it "should work with arrays too" do vardef = @parser.parse("$var += ['test']").code[0] vardef.should be_a(Puppet::Parser::AST::VarDef) vardef.append.should == true end end describe "when parsing selector" do it "should support hash access on the left hand side" do expect { @parser.parse("$h = { 'a' => 'b' } $a = $h['a'] ? { 'b' => 'd', default => undef }") }.to_not raise_error end end describe "parsing 'unless'" do it "should create the correct ast objects" do Puppet::Parser::AST::Not.expects(:new).with { |h| h[:value].is_a?(Puppet::Parser::AST::Boolean) } @parser.parse("unless false { $var = 1 }") end it "should not raise an error with empty statements" do expect { @parser.parse("unless false { }") }.to_not raise_error end #test for bug #13296 it "should not override 'unless' as a parameter inside resources" do lambda { @parser.parse("exec {'/bin/echo foo': unless => '/usr/bin/false',}") }.should_not raise_error end end describe "when parsing parameter names" do Puppet::Parser::Lexer::KEYWORDS.sort_tokens.each do |keyword| it "should allow #{keyword} as a keyword" do lambda { @parser.parse("exec {'/bin/echo foo': #{keyword} => '/usr/bin/false',}") }.should_not raise_error end end end describe "when parsing 'if'" do it "not, it should create the correct ast objects" do Puppet::Parser::AST::Not.expects(:new).with { |h| h[:value].is_a?(Puppet::Parser::AST::Boolean) } @parser.parse("if ! true { $var = 1 }") end it "boolean operation, it should create the correct ast objects" do Puppet::Parser::AST::BooleanOperator.expects(:new).with { |h| h[:rval].is_a?(Puppet::Parser::AST::Boolean) and h[:lval].is_a?(Puppet::Parser::AST::Boolean) and h[:operator]=="or" } @parser.parse("if true or true { $var = 1 }") end it "comparison operation, it should create the correct ast objects" do Puppet::Parser::AST::ComparisonOperator.expects(:new).with { |h| h[:lval].is_a?(Puppet::Parser::AST::Name) and h[:rval].is_a?(Puppet::Parser::AST::Name) and h[:operator]=="<" } @parser.parse("if 1 < 2 { $var = 1 }") end end describe "when parsing if complex expressions" do it "should create a correct ast tree" do aststub = stub_everything 'ast' Puppet::Parser::AST::ComparisonOperator.expects(:new).with { |h| h[:rval].is_a?(Puppet::Parser::AST::Name) and h[:lval].is_a?(Puppet::Parser::AST::Name) and h[:operator]==">" }.returns(aststub) Puppet::Parser::AST::ComparisonOperator.expects(:new).with { |h| h[:rval].is_a?(Puppet::Parser::AST::Name) and h[:lval].is_a?(Puppet::Parser::AST::Name) and h[:operator]=="==" }.returns(aststub) Puppet::Parser::AST::BooleanOperator.expects(:new).with { |h| h[:rval]==aststub and h[:lval]==aststub and h[:operator]=="and" } @parser.parse("if (1 > 2) and (1 == 2) { $var = 1 }") end it "should raise an error on incorrect expression" do expect { @parser.parse("if (1 > 2 > ) or (1 == 2) { $var = 1 }") }.to raise_error(Puppet::ParseError, /Syntax error at '\)'/) end end describe "when parsing resource references" do it "should not raise syntax errors" do expect { @parser.parse('exec { test: param => File["a"] }') }.to_not raise_error end it "should not raise syntax errors with multiple references" do expect { @parser.parse('exec { test: param => File["a","b"] }') }.to_not raise_error end it "should create an ast::ResourceReference" do Puppet::Parser::AST::ResourceReference.expects(:new).with { |arg| arg[:line]==1 and arg[:type]=="File" and arg[:title].is_a?(Puppet::Parser::AST::ASTArray) } @parser.parse('exec { test: command => File["a","b"] }') end end describe "when parsing resource overrides" do it "should not raise syntax errors" do expect { @parser.parse('Resource["title"] { param => value }') }.to_not raise_error end it "should not raise syntax errors with multiple overrides" do expect { @parser.parse('Resource["title1","title2"] { param => value }') }.to_not raise_error end it "should create an ast::ResourceOverride" do #Puppet::Parser::AST::ResourceOverride.expects(:new).with { |arg| # arg[:line]==1 and arg[:object].is_a?(Puppet::Parser::AST::ResourceReference) and arg[:parameters].is_a?(Puppet::Parser::AST::ResourceParam) #} ro = @parser.parse('Resource["title1","title2"] { param => value }').code[0] ro.should be_a(Puppet::Parser::AST::ResourceOverride) ro.line.should == 1 ro.object.should be_a(Puppet::Parser::AST::ResourceReference) ro.parameters[0].should be_a(Puppet::Parser::AST::ResourceParam) end end describe "when parsing if statements" do it "should not raise errors with empty if" do expect { @parser.parse("if true { }") }.to_not raise_error end it "should not raise errors with empty else" do expect { @parser.parse("if false { notice('if') } else { }") }.to_not raise_error end it "should not raise errors with empty if and else" do expect { @parser.parse("if false { } else { }") }.to_not raise_error end it "should create a nop node for empty branch" do Puppet::Parser::AST::Nop.expects(:new) @parser.parse("if true { }") end it "should create a nop node for empty else branch" do Puppet::Parser::AST::Nop.expects(:new) @parser.parse("if true { notice('test') } else { }") end it "should build a chain of 'ifs' if there's an 'elsif'" do expect { @parser.parse(<<-PP) }.to_not raise_error if true { notice('test') } elsif true {} else { } PP end end describe "when parsing function calls" do it "should not raise errors with no arguments" do expect { @parser.parse("tag()") }.to_not raise_error end it "should not raise errors with rvalue function with no args" do expect { @parser.parse("$a = template()") }.to_not raise_error end it "should not raise errors with arguments" do expect { @parser.parse("notice(1)") }.to_not raise_error end it "should not raise errors with multiple arguments" do expect { @parser.parse("notice(1,2)") }.to_not raise_error end it "should not raise errors with multiple arguments and a trailing comma" do expect { @parser.parse("notice(1,2,)") }.to_not raise_error end end describe "when parsing arrays" do it "should parse an array" do expect { @parser.parse("$a = [1,2]") }.to_not raise_error end it "should not raise errors with a trailing comma" do expect { @parser.parse("$a = [1,2,]") }.to_not raise_error end it "should accept an empty array" do expect { @parser.parse("$var = []\n") }.to_not raise_error end end describe "when providing AST context" do before do @lexer = stub 'lexer', :line => 50, :file => "/foo/bar", :getcomment => "whev" @parser.stubs(:lexer).returns @lexer end it "should include the lexer's line" do @parser.ast_context[:line].should == 50 end it "should include the lexer's file" do @parser.ast_context[:file].should == "/foo/bar" end it "should include the docs if directed to do so" do @parser.ast_context(true)[:doc].should == "whev" end it "should not include the docs when told not to" do @parser.ast_context(false)[:doc].should be_nil end it "should not include the docs by default" do @parser.ast_context[:doc].should be_nil end end describe "when building ast nodes" do before do @lexer = stub 'lexer', :line => 50, :file => "/foo/bar", :getcomment => "whev" @parser.stubs(:lexer).returns @lexer @class = Puppet::Resource::Type.new(:hostclass, "myclass", :use_docs => false) end it "should return a new instance of the provided class created with the provided options" do @class.expects(:new).with { |opts| opts[:foo] == "bar" } @parser.ast(@class, :foo => "bar") end it "should merge the ast context into the provided options" do @class.expects(:new).with { |opts| opts[:file] == "/foo" } @parser.expects(:ast_context).returns :file => "/foo" @parser.ast(@class, :foo => "bar") end it "should prefer provided options over AST context" do @class.expects(:new).with { |opts| opts[:file] == "/bar" } @lexer.expects(:file).returns "/foo" @parser.ast(@class, :file => "/bar") end it "should include docs when the AST class uses them" do @class.expects(:use_docs).returns true @class.stubs(:new) @parser.expects(:ast_context).with{ |docs, line| docs == true }.returns({}) @parser.ast(@class, :file => "/bar") end it "should get docs from lexer using the correct AST line number" do @class.expects(:use_docs).returns true @class.stubs(:new).with{ |a| a[:doc] == "doc" } @lexer.expects(:getcomment).with(12).returns "doc" @parser.ast(@class, :file => "/bar", :line => 12) end end describe "when retrieving a specific node" do it "should delegate to the known_resource_types node" do @known_resource_types.expects(:node).with("node") @parser.node("node") end end describe "when retrieving a specific class" do it "should delegate to the loaded code" do @known_resource_types.expects(:hostclass).with("class") @parser.hostclass("class") end end describe "when retrieving a specific definitions" do it "should delegate to the loaded code" do @known_resource_types.expects(:definition).with("define") @parser.definition("define") end end describe "when determining the configuration version" do it "should determine it from the resource type collection" do @parser.known_resource_types.expects(:version).returns "foo" @parser.version.should == "foo" end end describe "when looking up definitions" do it "should use the known resource types to check for them by name" do @parser.known_resource_types.stubs(:find_or_load).with("namespace","name",:definition).returns(:this_value) @parser.find_definition("namespace","name").should == :this_value end end describe "when looking up hostclasses" do it "should use the known resource types to check for them by name" do @parser.known_resource_types.stubs(:find_or_load).with("namespace","name",:hostclass,{}).returns(:this_value) @parser.find_hostclass("namespace","name").should == :this_value end end describe "when parsing classes" do before :each do @krt = Puppet::Resource::TypeCollection.new("development") @parser = Puppet::Parser::Parser.new "development" @parser.stubs(:known_resource_types).returns @krt end it "should not create new classes" do @parser.parse("class foobar {}").code[0].should be_a(Puppet::Parser::AST::Hostclass) @krt.hostclass("foobar").should be_nil end it "should correctly set the parent class when one is provided" do @parser.parse("class foobar inherits yayness {}").code[0].instantiate('')[0].parent.should == "yayness" end it "should correctly set the parent class for multiple classes at a time" do statements = @parser.parse("class foobar inherits yayness {}\nclass boo inherits bar {}").code statements[0].instantiate('')[0].parent.should == "yayness" statements[1].instantiate('')[0].parent.should == "bar" end it "should define the code when some is provided" do @parser.parse("class foobar { $var = val }").code[0].code.should_not be_nil end it "should accept parameters with trailing comma" do @parser.parse("file { '/example': ensure => file, }").should be end it "should accept parametrized classes with trailing comma" do @parser.parse("class foobar ($var1 = 0,) { $var = val }").code[0].code.should_not be_nil end it "should define parameters when provided" do foobar = @parser.parse("class foobar($biz,$baz) {}").code[0].instantiate('')[0] foobar.arguments.should == {"biz" => nil, "baz" => nil} end end describe "when parsing resources" do before :each do @krt = Puppet::Resource::TypeCollection.new("development") @parser = Puppet::Parser::Parser.new "development" @parser.stubs(:known_resource_types).returns @krt end it "should be able to parse class resources" do @krt.add(Puppet::Resource::Type.new(:hostclass, "foobar", :arguments => {"biz" => nil})) expect { @parser.parse("class { foobar: biz => stuff }") }.to_not raise_error end it "should correctly mark exported resources as exported" do @parser.parse("@@file { '/file': }").code[0].exported.should be_true end it "should correctly mark virtual resources as virtual" do @parser.parse("@file { '/file': }").code[0].virtual.should be_true end end describe "when parsing nodes" do it "should be able to parse a node with a single name" do node = @parser.parse("node foo { }").code[0] node.should be_a Puppet::Parser::AST::Node node.names.length.should == 1 node.names[0].value.should == "foo" end it "should be able to parse a node with two names" do node = @parser.parse("node foo, bar { }").code[0] node.should be_a Puppet::Parser::AST::Node node.names.length.should == 2 node.names[0].value.should == "foo" node.names[1].value.should == "bar" end it "should be able to parse a node with three names" do node = @parser.parse("node foo, bar, baz { }").code[0] node.should be_a Puppet::Parser::AST::Node node.names.length.should == 3 node.names[0].value.should == "foo" node.names[1].value.should == "bar" node.names[2].value.should == "baz" end end it "should fail if trying to collect defaults" do expect { @parser.parse("@Port { protocols => tcp }") }.to raise_error(Puppet::ParseError, /Defaults are not virtualizable/) end context "when parsing collections" do it "should parse basic collections" do @parser.parse("Port <| |>").code. should be_all {|x| x.is_a? Puppet::Parser::AST::Collection } end it "should parse fully qualified collections" do @parser.parse("Port::Range <| |>").code. should be_all {|x| x.is_a? Puppet::Parser::AST::Collection } end end it "should not assign to a fully qualified variable" do expect { @parser.parse("$one::two = yay") }.to raise_error(Puppet::ParseError, /Cannot assign to variables in other namespaces/) end it "should parse assignment of undef" do tree = @parser.parse("$var = undef") tree.code.children[0].should be_an_instance_of Puppet::Parser::AST::VarDef tree.code.children[0].value.should be_an_instance_of Puppet::Parser::AST::Undef end context "#namesplit" do { "base::sub" => %w{base sub}, "main" => ["", "main"], "one::two::three::four" => ["one::two::three", "four"], }.each do |input, output| it "should split #{input.inspect} to #{output.inspect}" do @parser.namesplit(input).should == output end end end it "should treat classes as case insensitive" do @parser.known_resource_types.import_ast(@parser.parse("class yayness {}"), '') @parser.known_resource_types.hostclass('yayness'). should == @parser.find_hostclass("", "YayNess") end it "should treat defines as case insensitive" do @parser.known_resource_types.import_ast(@parser.parse("define funtest {}"), '') @parser.known_resource_types.hostclass('funtest'). should == @parser.find_hostclass("", "fUntEst") end context "deprecations" do it "should flag use of import as deprecated" do Puppet.expects(:deprecation_warning).once @parser.known_resource_types.loader.expects(:import).with('foo', Dir.pwd) @parser.parse("import 'foo'") end end end diff --git a/spec/unit/parser/type_loader_spec.rb b/spec/unit/parser/type_loader_spec.rb index 5454528a7..759ade0f8 100755 --- a/spec/unit/parser/type_loader_spec.rb +++ b/spec/unit/parser/type_loader_spec.rb @@ -1,224 +1,202 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parser/type_loader' require 'puppet/parser/parser_factory' require 'puppet_spec/modules' require 'puppet_spec/files' describe Puppet::Parser::TypeLoader do include PuppetSpec::Modules include PuppetSpec::Files let(:empty_hostclass) { Puppet::Parser::AST::Hostclass.new('') } let(:loader) { Puppet::Parser::TypeLoader.new(:myenv) } it "should support an environment" do loader = Puppet::Parser::TypeLoader.new(:myenv) loader.environment.name.should == :myenv end it "should delegate its known resource types to its environment" do loader.known_resource_types.should be_instance_of(Puppet::Resource::TypeCollection) end describe "when loading names from namespaces" do it "should do nothing if the name to import is an empty string" do loader.try_load_fqname(:hostclass, "").should be_nil end it "should attempt to import each generated name" do loader.expects(:import_from_modules).with("foo/bar").returns([]) loader.expects(:import_from_modules).with("foo").returns([]) loader.try_load_fqname(:hostclass, "foo::bar") end it "should attempt to load each possible name going from most to least specific" do path_order = sequence('path') ['foo/bar/baz', 'foo/bar', 'foo'].each do |path| Puppet::Parser::Files.expects(:find_manifests_in_modules).with(path, anything).returns([nil, []]).in_sequence(path_order) end loader.try_load_fqname(:hostclass, 'foo::bar::baz') end end describe "when importing" do let(:stub_parser) { stub 'Parser', :file= => nil, :parse => empty_hostclass } before(:each) do Puppet::Parser::ParserFactory.stubs(:parser).with(anything).returns(stub_parser) end it "should return immediately when imports are being ignored" do Puppet::Parser::Files.expects(:find_manifests_in_modules).never Puppet[:ignoreimport] = true loader.import("foo", "/path").should be_nil end it "should find all manifests matching the file or pattern" do Puppet::Parser::Files.expects(:find_manifests_in_modules).with("myfile", anything).returns ["modname", %w{one}] loader.import("myfile", "/path") end it "should pass the environment when looking for files" do Puppet::Parser::Files.expects(:find_manifests_in_modules).with(anything, loader.environment).returns ["modname", %w{one}] loader.import("myfile", "/path") end it "should fail if no files are found" do Puppet::Parser::Files.expects(:find_manifests_in_modules).returns [nil, []] lambda { loader.import("myfile", "/path") }.should raise_error(Puppet::ImportError) end it "should parse each found file" do Puppet::Parser::Files.expects(:find_manifests_in_modules).returns ["modname", [make_absolute("/one")]] loader.expects(:parse_file).with(make_absolute("/one")).returns(Puppet::Parser::AST::Hostclass.new('')) loader.import("myfile", "/path") end it "should not attempt to import files that have already been imported" do loader = Puppet::Parser::TypeLoader.new(:myenv) Puppet::Parser::Files.expects(:find_manifests_in_modules).twice.returns ["modname", %w{/one}] loader.import("myfile", "/path").should_not be_empty loader.import("myfile", "/path").should be_empty end end describe "when importing all" do before do @base = tmpdir("base") # Create two module path directories @modulebase1 = File.join(@base, "first") FileUtils.mkdir_p(@modulebase1) @modulebase2 = File.join(@base, "second") FileUtils.mkdir_p(@modulebase2) Puppet[:modulepath] = "#{@modulebase1}#{File::PATH_SEPARATOR}#{@modulebase2}" end def mk_module(basedir, name) PuppetSpec::Modules.create(name, basedir) end # We have to pass the base path so that we can # write to modules that are in the second search path - def mk_manifests(base, mod, type, files) - exts = {"ruby" => ".rb", "puppet" => ".pp"} + def mk_manifests(base, mod, files) files.collect do |file| name = mod.name + "::" + file.gsub("/", "::") - path = File.join(base, mod.name, "manifests", file + exts[type]) + path = File.join(base, mod.name, "manifests", file + ".pp") FileUtils.mkdir_p(File.split(path)[0]) # write out the class - if type == "ruby" - File.open(path, "w") { |f| f.print "hostclass '#{name}' do\nend" } - else - File.open(path, "w") { |f| f.print "class #{name} {}" } - end + File.open(path, "w") { |f| f.print "class #{name} {}" } name end end it "should load all puppet manifests from all modules in the specified environment" do @module1 = mk_module(@modulebase1, "one") @module2 = mk_module(@modulebase2, "two") - mk_manifests(@modulebase1, @module1, "puppet", %w{a b}) - mk_manifests(@modulebase2, @module2, "puppet", %w{c d}) - - loader.import_all - - loader.environment.known_resource_types.hostclass("one::a").should be_instance_of(Puppet::Resource::Type) - loader.environment.known_resource_types.hostclass("one::b").should be_instance_of(Puppet::Resource::Type) - loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type) - loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type) - end - - it "should load all ruby manifests from all modules in the specified environment" do - Puppet.expects(:deprecation_warning).at_least(1) - - @module1 = mk_module(@modulebase1, "one") - @module2 = mk_module(@modulebase2, "two") - - mk_manifests(@modulebase1, @module1, "ruby", %w{a b}) - mk_manifests(@modulebase2, @module2, "ruby", %w{c d}) + mk_manifests(@modulebase1, @module1, %w{a b}) + mk_manifests(@modulebase2, @module2, %w{c d}) loader.import_all loader.environment.known_resource_types.hostclass("one::a").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("one::b").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type) end it "should not load manifests from duplicate modules later in the module path" do @module1 = mk_module(@modulebase1, "one") # duplicate @module2 = mk_module(@modulebase2, "one") - mk_manifests(@modulebase1, @module1, "puppet", %w{a}) - mk_manifests(@modulebase2, @module2, "puppet", %w{c}) + mk_manifests(@modulebase1, @module1, %w{a}) + mk_manifests(@modulebase2, @module2, %w{c}) loader.import_all loader.environment.known_resource_types.hostclass("one::c").should be_nil end it "should load manifests from subdirectories" do @module1 = mk_module(@modulebase1, "one") - mk_manifests(@modulebase1, @module1, "puppet", %w{a a/b a/b/c}) + mk_manifests(@modulebase1, @module1, %w{a a/b a/b/c}) loader.import_all loader.environment.known_resource_types.hostclass("one::a::b").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("one::a::b::c").should be_instance_of(Puppet::Resource::Type) end it "should skip modules that don't have manifests" do @module1 = mk_module(@modulebase1, "one") @module2 = mk_module(@modulebase2, "two") - mk_manifests(@modulebase2, @module2, "ruby", %w{c d}) + mk_manifests(@modulebase2, @module2, %w{c d}) loader.import_all loader.environment.known_resource_types.hostclass("one::a").should be_nil loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type) end end describe "when parsing a file" do it "should create a new parser instance for each file using the current environment" do parser = stub 'Parser', :file= => nil, :parse => empty_hostclass Puppet::Parser::ParserFactory.expects(:parser).twice.with(loader.environment).returns(parser) loader.parse_file("/my/file") loader.parse_file("/my/other_file") end it "should assign the parser its file and parse" do parser = mock 'parser' Puppet::Parser::ParserFactory.expects(:parser).with(loader.environment).returns(parser) parser.expects(:file=).with("/my/file") parser.expects(:parse).returns(empty_hostclass) loader.parse_file("/my/file") end end it "should be able to add classes to the current resource type collection" do file = tmpfile("simple_file.pp") File.open(file, "w") { |f| f.puts "class foo {}" } loader.import(File.basename(file), File.dirname(file)) loader.known_resource_types.hostclass("foo").should be_instance_of(Puppet::Resource::Type) end end diff --git a/spec/unit/pops/binder/config/binder_config_spec.rb b/spec/unit/pops/binder/config/binder_config_spec.rb index 67fe0bb6e..48df78e94 100644 --- a/spec/unit/pops/binder/config/binder_config_spec.rb +++ b/spec/unit/pops/binder/config/binder_config_spec.rb @@ -1,35 +1,42 @@ require 'spec_helper' require 'puppet/pops' require 'puppet_spec/pops' describe 'BinderConfig' do include PuppetSpec::Pops let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() } let(:diag) { Puppet::Pops::Binder::Config::DiagnosticProducer.new(acceptor) } let(:issues) { Puppet::Pops::Binder::Config::Issues } it 'should load default config if no config file exists' do diagnostics = diag config = Puppet::Pops::Binder::Config::BinderConfig.new(diagnostics) expect(acceptor.errors?()).to be == false expect(config.layering_config[0]['name']).to be == 'site' expect(config.layering_config[0]['include']).to be == ['confdir:/default?optional'] expect(config.layering_config[1]['name']).to be == 'modules' expect(config.layering_config[1]['include']).to be == ['module:/*::default'] end it 'should load binder_config.yaml if it exists in confdir)' do Puppet::Pops::Binder::Config::BinderConfig.any_instance.stubs(:confdir).returns(my_fixture("/ok/")) config = Puppet::Pops::Binder::Config::BinderConfig.new(diag) expect(acceptor.errors?()).to be == false expect(config.layering_config[0]['name']).to be == 'site' expect(config.layering_config[0]['include']).to be == 'confdir:/' expect(config.layering_config[1]['name']).to be == 'modules' expect(config.layering_config[1]['include']).to be == 'module:/*::test/' expect(config.layering_config[1]['exclude']).to be == 'module:/bad::test/' end + it 'should correctly set values to default if not defined in bunder_config.yml)' do + Puppet::Pops::Binder::Config::BinderConfig.any_instance.stubs(:confdir).returns(my_fixture("/nolayer/")) + config = Puppet::Pops::Binder::Config::BinderConfig.new(diag) + expect(acceptor.errors?()).to be == false + expect(config.layering_config[0]['name']).to be == 'site' + end + # TODO: test error conditions (see BinderConfigChecker for what to test) -end \ No newline at end of file +end diff --git a/spec/unit/property_spec.rb b/spec/unit/property_spec.rb index 2c9a54e68..df855ac5b 100755 --- a/spec/unit/property_spec.rb +++ b/spec/unit/property_spec.rb @@ -1,541 +1,511 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/property' describe Puppet::Property do let :resource do Puppet::Type.type(:host).new :name => "foo" end let :subclass do # We need a completely fresh subclass every time, because we modify both # class and instance level things inside the tests. subclass = Class.new(Puppet::Property) do class << self attr_accessor :name end @name = :foo end subclass.initvars subclass end let :property do subclass.new :resource => resource end it "should be able to look up the modified name for a given value" do subclass.newvalue(:foo) subclass.value_name("foo").should == :foo end it "should be able to look up the modified name for a given value matching a regex" do subclass.newvalue(%r{.}) subclass.value_name("foo").should == %r{.} end it "should be able to look up a given value option" do subclass.newvalue(:foo, :event => :whatever) subclass.value_option(:foo, :event).should == :whatever end it "should be able to specify required features" do subclass.should respond_to(:required_features=) end {"one" => [:one],:one => [:one],%w{a} => [:a],[:b] => [:b],%w{one two} => [:one,:two],[:a,:b] => [:a,:b]}.each { |in_value,out_value| it "should always convert required features into an array of symbols (e.g. #{in_value.inspect} --> #{out_value.inspect})" do subclass.required_features = in_value subclass.required_features.should == out_value end } it "should return its name as a string when converted to a string" do property.to_s.should == property.name.to_s end - it "should be able to shadow metaparameters" do - property.must respond_to(:shadow) - end - describe "when returning the default event name" do it "should use the current 'should' value to pick the event name" do property.expects(:should).returns "myvalue" subclass.expects(:value_option).with('myvalue', :event).returns :event_name property.event_name end it "should return any event defined with the specified value" do property.expects(:should).returns :myval subclass.expects(:value_option).with(:myval, :event).returns :event_name property.event_name.should == :event_name end describe "and the property is 'ensure'" do before :each do property.stubs(:name).returns :ensure resource.expects(:type).returns :mytype end it "should use _created if the 'should' value is 'present'" do property.expects(:should).returns :present property.event_name.should == :mytype_created end it "should use _removed if the 'should' value is 'absent'" do property.expects(:should).returns :absent property.event_name.should == :mytype_removed end it "should use _changed if the 'should' value is not 'absent' or 'present'" do property.expects(:should).returns :foo property.event_name.should == :mytype_changed end it "should use _changed if the 'should value is nil" do property.expects(:should).returns nil property.event_name.should == :mytype_changed end end it "should use _changed if the property is not 'ensure'" do property.stubs(:name).returns :myparam property.expects(:should).returns :foo property.event_name.should == :myparam_changed end it "should use _changed if no 'should' value is set" do property.stubs(:name).returns :myparam property.expects(:should).returns nil property.event_name.should == :myparam_changed end end describe "when creating an event" do before :each do property.stubs(:should).returns "myval" end it "should use an event from the resource as the base event" do event = Puppet::Transaction::Event.new resource.expects(:event).returns event property.event.should equal(event) end it "should have the default event name" do property.expects(:event_name).returns :my_event property.event.name.should == :my_event end it "should have the property's name" do property.event.property.should == property.name.to_s end it "should have the 'should' value set" do property.stubs(:should).returns "foo" property.event.desired_value.should == "foo" end it "should provide its path as the source description" do property.stubs(:path).returns "/my/param" property.event.source_description.should == "/my/param" end it "should have the 'invalidate_refreshes' value set if set on a value" do property.stubs(:event_name).returns :my_event property.stubs(:should).returns "foo" foo = mock() foo.expects(:invalidate_refreshes).returns(true) collection = mock() collection.expects(:match?).with("foo").returns(foo) property.class.stubs(:value_collection).returns(collection) property.event.invalidate_refreshes.should be_true end end - describe "when shadowing metaparameters" do - let :shadow_class do - shadow_class = Class.new(Puppet::Property) do - @name = :alias - end - shadow_class.initvars - shadow_class - end - - it "should create an instance of the metaparameter at initialization" do - Puppet::Type.metaparamclass(:alias).expects(:new).with(:resource => resource) - - shadow_class.new :resource => resource - end - - it "should munge values using the shadow's munge method" do - shadow = mock 'shadow' - Puppet::Type.metaparamclass(:alias).expects(:new).returns shadow - - shadow.expects(:munge).with "foo" - - property = shadow_class.new :resource => resource - property.munge("foo") - end - end - describe "when defining new values" do it "should define a method for each value created with a block that's not a regex" do subclass.newvalue(:foo) { } property.must respond_to(:set_foo) end end describe "when assigning the value" do it "should just set the 'should' value" do property.value = "foo" property.should.must == "foo" end it "should validate each value separately" do property.expects(:validate).with("one") property.expects(:validate).with("two") property.value = %w{one two} end it "should munge each value separately and use any result as the actual value" do property.expects(:munge).with("one").returns :one property.expects(:munge).with("two").returns :two # Do this so we get the whole array back. subclass.array_matching = :all property.value = %w{one two} property.should.must == [:one, :two] end it "should return any set value" do (property.value = :one).should == :one end end describe "when returning the value" do it "should return nil if no value is set" do property.should.must be_nil end it "should return the first set 'should' value if :array_matching is set to :first" do subclass.array_matching = :first property.should = %w{one two} property.should.must == "one" end it "should return all set 'should' values as an array if :array_matching is set to :all" do subclass.array_matching = :all property.should = %w{one two} property.should.must == %w{one two} end it "should default to :first array_matching" do subclass.array_matching.should == :first end it "should unmunge the returned value if :array_matching is set to :first" do property.class.unmunge do |v| v.to_sym end subclass.array_matching = :first property.should = %w{one two} property.should.must == :one end it "should unmunge all the returned values if :array_matching is set to :all" do property.class.unmunge do |v| v.to_sym end subclass.array_matching = :all property.should = %w{one two} property.should.must == [:one, :two] end end describe "when validating values" do it "should do nothing if no values or regexes have been defined" do lambda { property.should = "foo" }.should_not raise_error end it "should fail if the value is not a defined value or alias and does not match a regex" do subclass.newvalue(:foo) lambda { property.should = "bar" }.should raise_error end it "should succeeed if the value is one of the defined values" do subclass.newvalue(:foo) lambda { property.should = :foo }.should_not raise_error end it "should succeeed if the value is one of the defined values even if the definition uses a symbol and the validation uses a string" do subclass.newvalue(:foo) lambda { property.should = "foo" }.should_not raise_error end it "should succeeed if the value is one of the defined values even if the definition uses a string and the validation uses a symbol" do subclass.newvalue("foo") lambda { property.should = :foo }.should_not raise_error end it "should succeed if the value is one of the defined aliases" do subclass.newvalue("foo") subclass.aliasvalue("bar", "foo") lambda { property.should = :bar }.should_not raise_error end it "should succeed if the value matches one of the regexes" do subclass.newvalue(/./) lambda { property.should = "bar" }.should_not raise_error end it "should validate that all required features are present" do subclass.newvalue(:foo, :required_features => [:a, :b]) resource.provider.expects(:satisfies?).with([:a, :b]).returns true property.should = :foo end it "should fail if required features are missing" do subclass.newvalue(:foo, :required_features => [:a, :b]) resource.provider.expects(:satisfies?).with([:a, :b]).returns false lambda { property.should = :foo }.should raise_error(Puppet::Error) end it "should internally raise an ArgumentError if required features are missing" do subclass.newvalue(:foo, :required_features => [:a, :b]) resource.provider.expects(:satisfies?).with([:a, :b]).returns false lambda { property.validate_features_per_value :foo }.should raise_error(ArgumentError) end it "should validate that all required features are present for regexes" do value = subclass.newvalue(/./, :required_features => [:a, :b]) resource.provider.expects(:satisfies?).with([:a, :b]).returns true property.should = "foo" end it "should support specifying an individual required feature" do value = subclass.newvalue(/./, :required_features => :a) resource.provider.expects(:satisfies?).returns true property.should = "foo" end end describe "when munging values" do it "should do nothing if no values or regexes have been defined" do property.munge("foo").should == "foo" end it "should return return any matching defined values" do subclass.newvalue(:foo) property.munge("foo").should == :foo end it "should return any matching aliases" do subclass.newvalue(:foo) subclass.aliasvalue(:bar, :foo) property.munge("bar").should == :foo end it "should return the value if it matches a regex" do subclass.newvalue(/./) property.munge("bar").should == "bar" end it "should return the value if no other option is matched" do subclass.newvalue(:foo) property.munge("bar").should == "bar" end end describe "when syncing the 'should' value" do it "should set the value" do subclass.newvalue(:foo) property.should = :foo property.expects(:set).with(:foo) property.sync end end describe "when setting a value" do it "should catch exceptions and raise Puppet::Error" do subclass.newvalue(:foo) { raise "eh" } lambda { property.set(:foo) }.should raise_error(Puppet::Error) end it "fails when the provider does not handle the attribute" do subclass.name = "unknown" lambda { property.set(:a_value) }.should raise_error(Puppet::Error) end it "propogates the errors about missing methods from the provider" do provider = resource.provider def provider.bad_method=(value) value.this_method_does_not_exist end subclass.name = :bad_method lambda { property.set(:a_value) }.should raise_error(NoMethodError, /this_method_does_not_exist/) end describe "that was defined without a block" do it "should call the settor on the provider" do subclass.newvalue(:bar) resource.provider.expects(:foo=).with :bar property.set(:bar) end end describe "that was defined with a block" do it "should call the method created for the value if the value is not a regex" do subclass.newvalue(:bar) {} property.expects(:set_bar) property.set(:bar) end it "should call the provided block if the value is a regex" do subclass.newvalue(/./) { self.test } property.expects(:test) property.set("foo") end end end describe "when producing a change log" do it "should say 'defined' when the current value is 'absent'" do property.change_to_s(:absent, "foo").should =~ /^defined/ end it "should say 'undefined' when the new value is 'absent'" do property.change_to_s("foo", :absent).should =~ /^undefined/ end it "should say 'changed' when neither value is 'absent'" do property.change_to_s("foo", "bar").should =~ /changed/ end end shared_examples_for "#insync?" do # We share a lot of behaviour between the all and first matching, so we # use a shared behaviour set to emulate that. The outside world makes # sure the class, etc, point to the right content. [[], [12], [12, 13]].each do |input| it "should return true if should is empty with is => #{input.inspect}" do property.should = [] property.must be_insync(input) end end end describe "#insync?" do context "array_matching :all" do # `@should` is an array of scalar values, and `is` is an array of scalar values. before :each do property.class.array_matching = :all end it_should_behave_like "#insync?" context "if the should value is an array" do before :each do property.should = [1, 2] end it "should match if is exactly matches" do property.must be_insync [1, 2] end it "should match if it matches, but all stringified" do property.must be_insync ["1", "2"] end it "should not match if some-but-not-all values are stringified" do property.must_not be_insync ["1", 2] property.must_not be_insync [1, "2"] end it "should not match if order is different but content the same" do property.must_not be_insync [2, 1] end it "should not match if there are more items in should than is" do property.must_not be_insync [1] end it "should not match if there are less items in should than is" do property.must_not be_insync [1, 2, 3] end it "should not match if `is` is empty but `should` isn't" do property.must_not be_insync [] end end end context "array_matching :first" do # `@should` is an array of scalar values, and `is` is a scalar value. before :each do property.class.array_matching = :first end it_should_behave_like "#insync?" [[1], # only the value [1, 2], # matching value first [2, 1], # matching value last [0, 1, 2], # matching value in the middle ].each do |input| it "should by true if one unmodified should value of #{input.inspect} matches what is" do property.should = input property.must be_insync 1 end it "should be true if one stringified should value of #{input.inspect} matches what is" do property.should = input property.must be_insync "1" end end it "should not match if we expect a string but get the non-stringified value" do property.should = ["1"] property.must_not be_insync 1 end [[0], [0, 2]].each do |input| it "should not match if no should values match what is" do property.should = input property.must_not be_insync 1 property.must_not be_insync "1" # shouldn't match either. end end end end describe "#property_matches?" do [1, "1", [1], :one].each do |input| it "should treat two equal objects as equal (#{input.inspect})" do property.property_matches?(input, input).should be_true end end it "should treat two objects as equal if the first argument is the stringified version of the second" do property.property_matches?("1", 1).should be_true end it "should NOT treat two objects as equal if the first argument is not a string, and the second argument is a string, even if it stringifies to the first" do property.property_matches?(1, "1").should be_false end end end diff --git a/spec/unit/provider/cron/parsed_spec.rb b/spec/unit/provider/cron/parsed_spec.rb index 92ece1bff..1a038eae5 100644 --- a/spec/unit/provider/cron/parsed_spec.rb +++ b/spec/unit/provider/cron/parsed_spec.rb @@ -1,336 +1,357 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:cron).provider(:crontab) do let :provider do described_class.new(:command => '/bin/true') end let :resource do Puppet::Type.type(:cron).new( :minute => %w{0 15 30 45}, :hour => %w{8-18 20-22}, :monthday => %w{31}, :month => %w{12}, :weekday => %w{7}, :name => 'basic', :command => '/bin/true', :target => 'root', :provider => provider ) end let :resource_special do Puppet::Type.type(:cron).new( :special => 'reboot', :name => 'special', :command => '/bin/true', :target => 'nobody' ) end let :resource_sparse do Puppet::Type.type(:cron).new( :minute => %w{42}, :target => 'root', :name => 'sparse' ) end let :record_special do { :record_type => :crontab, :special => 'reboot', :command => '/bin/true', :on_disk => true, :target => 'nobody' } end let :record do { :record_type => :crontab, :minute => %w{0 15 30 45}, :hour => %w{8-18 20-22}, :monthday => %w{31}, :month => %w{12}, :weekday => %w{7}, :special => :absent, :command => '/bin/true', :on_disk => true, :target => 'root' } end describe "when determining the correct filetype" do it "should use the suntab filetype on Solaris" do Facter.stubs(:value).with(:osfamily).returns 'Solaris' described_class.filetype.should == Puppet::Util::FileType::FileTypeSuntab end it "should use the aixtab filetype on AIX" do Facter.stubs(:value).with(:osfamily).returns 'AIX' described_class.filetype.should == Puppet::Util::FileType::FileTypeAixtab end it "should use the crontab filetype on other platforms" do Facter.stubs(:value).with(:osfamily).returns 'Not a real operating system family' described_class.filetype.should == Puppet::Util::FileType::FileTypeCrontab end end # I'd use ENV.expects(:[]).with('USER') but this does not work because # ENV["USER"] is evaluated at load time. describe "when determining the default target" do it "should use the current user #{ENV['USER']}", :if => ENV['USER'] do described_class.default_target.should == ENV['USER'] end it "should fallback to root", :unless => ENV['USER'] do described_class.default_target.should == "root" end end + describe ".targets" do + let(:tabs) { [ described_class.default_target ] + %w{foo bar} } + before do + File.expects(:readable?).returns true + File.stubs(:file?).returns true + File.stubs(:writable?).returns true + end + after do + File.unstub :readable?, :file?, :writable? + Dir.unstub :foreach + end + it "should add all crontabs as targets" do + Dir.expects(:foreach).multiple_yields(*tabs) + described_class.targets.should == tabs + end + end + describe "when parsing a record" do it "should parse a comment" do described_class.parse_line("# This is a test").should == { :record_type => :comment, :line => "# This is a test", } end it "should get the resource name of a PUPPET NAME comment" do described_class.parse_line('# Puppet Name: My Fancy Cronjob').should == { :record_type => :comment, :name => 'My Fancy Cronjob', :line => '# Puppet Name: My Fancy Cronjob', } end it "should ignore blank lines" do described_class.parse_line('').should == {:record_type => :blank, :line => ''} described_class.parse_line(' ').should == {:record_type => :blank, :line => ' '} described_class.parse_line("\t").should == {:record_type => :blank, :line => "\t"} described_class.parse_line(" \t ").should == {:record_type => :blank, :line => " \t "} end it "should extract environment assignments" do # man 5 crontab: MAILTO="" with no value can be used to surpress sending # mails at all described_class.parse_line('MAILTO=""').should == {:record_type => :environment, :line => 'MAILTO=""'} described_class.parse_line('FOO=BAR').should == {:record_type => :environment, :line => 'FOO=BAR'} described_class.parse_line('FOO_BAR=BAR').should == {:record_type => :environment, :line => 'FOO_BAR=BAR'} end it "should extract a cron entry" do described_class.parse_line('* * * * * /bin/true').should == { :record_type => :crontab, :hour => :absent, :minute => :absent, :month => :absent, :weekday => :absent, :monthday => :absent, :special => :absent, :command => '/bin/true' } described_class.parse_line('0,15,30,45 8-18,20-22 31 12 7 /bin/true').should == { :record_type => :crontab, :minute => %w{0 15 30 45}, :hour => %w{8-18 20-22}, :monthday => %w{31}, :month => %w{12}, :weekday => %w{7}, :special => :absent, :command => '/bin/true' } # A percent sign will cause the rest of the string to be passed as # standard input and will also act as a newline character. Not sure # if puppet should convert % to a \n as the command property so the # test covers the current behaviour: Do not do any conversions described_class.parse_line('0 22 * * 1-5 mail -s "It\'s 10pm" joe%Joe,%%Where are your kids?%').should == { :record_type => :crontab, :minute => %w{0}, :hour => %w{22}, :monthday => :absent, :month => :absent, :weekday => %w{1-5}, :special => :absent, :command => 'mail -s "It\'s 10pm" joe%Joe,%%Where are your kids?%' } end describe "it should support special strings" do ['reboot','yearly','anually','monthly', 'weekly', 'daily', 'midnight', 'hourly'].each do |special| it "should support @#{special}" do described_class.parse_line("@#{special} /bin/true").should == { :record_type => :crontab, :hour => :absent, :minute => :absent, :month => :absent, :weekday => :absent, :monthday => :absent, :special => special, :command => '/bin/true' } end end end end describe ".instances" do before :each do described_class.stubs(:default_target).returns 'foobar' end describe "on linux" do before do Facter.stubs(:value).with(:osfamily).returns 'Linux' Facter.stubs(:value).with(:operatingsystem) end - it "should be empty if user has no crontab" do + it "should contain no resources for a user who has no crontab" do # `crontab...` does only capture stdout here. On vixie-cron-4.1 # STDERR shows "no crontab for foobar" but stderr is ignored as # well as the exitcode. described_class.target_object('foobar').expects(:`).with('crontab -u foobar -l 2>/dev/null').returns "" - described_class.instances.should be_empty + described_class.instances.select { |resource| + resource.get('target') == 'foobar' + }.should be_empty end - it "should be empty if user is not present" do + it "should contain no resources for a user who is absent" do # `crontab...` does only capture stdout. On vixie-cron-4.1 # STDERR shows "crontab: user `foobar' unknown" but stderr is # ignored as well as the exitcode described_class.target_object('foobar').expects(:`).with('crontab -u foobar -l 2>/dev/null').returns "" - described_class.instances.should be_empty + described_class.instances.select { |resource| + resource.get('target') == 'foobar' + }.should be_empty end it "should be able to create records from not-managed records" do - described_class.expects(:target_object).returns File.new(my_fixture('simple')) + described_class.stubs(:target_object).returns File.new(my_fixture('simple')) parameters = described_class.instances.map do |p| h = {:name => p.get(:name)} Puppet::Type.type(:cron).validproperties.each do |property| h[property] = p.get(property) end h end expect(parameters[0][:name]).to match(%r{unmanaged:\$HOME/bin/daily.job_>>_\$HOME/tmp/out_2>&1-\d+}) expect(parameters[0][:minute]).to eq(['5']) expect(parameters[0][:hour]).to eq(['0']) expect(parameters[0][:weekday]).to eq(:absent) expect(parameters[0][:month]).to eq(:absent) expect(parameters[0][:monthday]).to eq(:absent) expect(parameters[0][:special]).to eq(:absent) expect(parameters[0][:command]).to match(%r{\$HOME/bin/daily.job >> \$HOME/tmp/out 2>&1}) expect(parameters[0][:ensure]).to eq(:present) expect(parameters[0][:environment]).to eq(:absent) expect(parameters[0][:user]).to eq(:absent) expect(parameters[1][:name]).to match(%r{unmanaged:\$HOME/bin/monthly-\d+}) expect(parameters[1][:minute]).to eq(['15']) expect(parameters[1][:hour]).to eq(['14']) expect(parameters[1][:weekday]).to eq(:absent) expect(parameters[1][:month]).to eq(:absent) expect(parameters[1][:monthday]).to eq(['1']) expect(parameters[1][:special]).to eq(:absent) expect(parameters[1][:command]).to match(%r{\$HOME/bin/monthly}) expect(parameters[1][:ensure]).to eq(:present) expect(parameters[1][:environment]).to eq(:absent) expect(parameters[1][:user]).to eq(:absent) expect(parameters[1][:target]).to eq('foobar') end - it "should be able to parse puppet manged cronjobs" do - described_class.expects(:target_object).returns File.new(my_fixture('managed')) + it "should be able to parse puppet managed cronjobs" do + described_class.stubs(:target_object).returns File.new(my_fixture('managed')) described_class.instances.map do |p| h = {:name => p.get(:name)} Puppet::Type.type(:cron).validproperties.each do |property| h[property] = p.get(property) end h end.should == [ { :name => 'real_job', :minute => :absent, :hour => :absent, :weekday => :absent, :month => :absent, :monthday => :absent, :special => :absent, :command => '/bin/true', :ensure => :present, :environment => :absent, :user => :absent, :target => 'foobar' }, { :name => 'complex_job', :minute => :absent, :hour => :absent, :weekday => :absent, :month => :absent, :monthday => :absent, :special => 'reboot', :command => '/bin/true >> /dev/null 2>&1', :ensure => :present, :environment => [ 'MAILTO=foo@example.com', 'SHELL=/bin/sh' ], :user => :absent, :target => 'foobar' } ] end end end describe ".match" do describe "normal records" do it "should match when all fields are the same" do described_class.match(record,{resource[:name] => resource}).must == resource end { :minute => %w{0 15 31 45}, :hour => %w{8-18}, :monthday => %w{30 31}, :month => %w{12 23}, :weekday => %w{4}, :command => '/bin/false', :target => 'nobody' }.each_pair do |field, new_value| it "should not match a record when #{field} does not match" do record[field] = new_value described_class.match(record,{resource[:name] => resource}).must be_false end end end describe "special records" do it "should match when all fields are the same" do described_class.match(record_special,{resource_special[:name] => resource_special}).must == resource_special end { :special => 'monthly', :command => '/bin/false', :target => 'root' }.each_pair do |field, new_value| it "should not match a record when #{field} does not match" do record_special[field] = new_value described_class.match(record_special,{resource_special[:name] => resource_special}).must be_false end end end describe "with a resource without a command" do it "should not raise an error" do expect { described_class.match(record,{resource_sparse[:name] => resource_sparse}) }.to_not raise_error end end end end diff --git a/spec/unit/resource/status_spec.rb b/spec/unit/resource/status_spec.rb index 271554325..59616b9d6 100755 --- a/spec/unit/resource/status_spec.rb +++ b/spec/unit/resource/status_spec.rb @@ -1,220 +1,194 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/resource/status' describe Puppet::Resource::Status do include PuppetSpec::Files before do @resource = Puppet::Type.type(:file).new :path => make_absolute("/my/file") @containment_path = ["foo", "bar", "baz"] @resource.stubs(:pathbuilder).returns @containment_path @status = Puppet::Resource::Status.new(@resource) end it "should compute type and title correctly" do @status.resource_type.should == "File" @status.title.should == make_absolute("/my/file") end - [:node, :file, :line, :current_values, :status, :evaluation_time].each do |attr| + [:file, :line, :evaluation_time].each do |attr| it "should support #{attr}" do @status.send(attr.to_s + "=", "foo") @status.send(attr).should == "foo" end end [:skipped, :failed, :restarted, :failed_to_restart, :changed, :out_of_sync, :scheduled].each do |attr| it "should support #{attr}" do @status.send(attr.to_s + "=", "foo") @status.send(attr).should == "foo" end it "should have a boolean method for determining whehter it was #{attr}" do @status.send(attr.to_s + "=", "foo") @status.should send("be_#{attr}") end end it "should accept a resource at initialization" do Puppet::Resource::Status.new(@resource).resource.should_not be_nil end it "should set its source description to the resource's path" do @resource.expects(:path).returns "/my/path" Puppet::Resource::Status.new(@resource).source_description.should == "/my/path" end it "should set its containment path" do Puppet::Resource::Status.new(@resource).containment_path.should == @containment_path end [:file, :line].each do |attr| it "should copy the resource's #{attr}" do @resource.expects(attr).returns "foo" Puppet::Resource::Status.new(@resource).send(attr).should == "foo" end end it "should copy the resource's tags" do @resource.expects(:tags).returns %w{foo bar} status = Puppet::Resource::Status.new(@resource) status.should be_tagged("foo") status.should be_tagged("bar") end it "should always convert the resource to a string" do @resource.expects(:to_s).returns "foo" Puppet::Resource::Status.new(@resource).resource.should == "foo" end it "should support tags" do Puppet::Resource::Status.ancestors.should include(Puppet::Util::Tagging) end it "should create a timestamp at its creation time" do @status.time.should be_instance_of(Time) end - describe "when sending logs" do - before do - Puppet::Util::Log.stubs(:new) - end - - it "should set the tags to the event tags" do - Puppet::Util::Log.expects(:new).with { |args| args[:tags] == %w{one two} } - @status.stubs(:tags).returns %w{one two} - @status.send_log :notice, "my message" - end - - [:file, :line].each do |attr| - it "should pass the #{attr}" do - Puppet::Util::Log.expects(:new).with { |args| args[attr] == "my val" } - @status.send(attr.to_s + "=", "my val") - @status.send_log :notice, "my message" - end - end - - it "should use the source description as the source" do - Puppet::Util::Log.expects(:new).with { |args| args[:source] == "my source" } - @status.stubs(:source_description).returns "my source" - @status.send_log :notice, "my message" - end - end - it "should support adding events" do event = Puppet::Transaction::Event.new(:name => :foobar) @status.add_event(event) @status.events.should == [event] end it "should use '<<' to add events" do event = Puppet::Transaction::Event.new(:name => :foobar) (@status << event).should equal(@status) @status.events.should == [event] end it "records an event for a failure caused by an error" do @status.failed_because(StandardError.new("the message")) expect(@status.events[0].message).to eq("the message") expect(@status.events[0].status).to eq("failure") expect(@status.events[0].name).to eq(:resource_error) end it "should count the number of successful events and set changed" do 3.times{ @status << Puppet::Transaction::Event.new(:status => 'success') } @status.change_count.should == 3 @status.changed.should == true @status.out_of_sync.should == true end it "should not start with any changes" do @status.change_count.should == 0 @status.changed.should == false @status.out_of_sync.should == false end it "should not treat failure, audit, or noop events as changed" do ['failure', 'audit', 'noop'].each do |s| @status << Puppet::Transaction::Event.new(:status => s) end @status.change_count.should == 0 @status.changed.should == false end it "should not treat audit events as out of sync" do @status << Puppet::Transaction::Event.new(:status => 'audit') @status.out_of_sync_count.should == 0 @status.out_of_sync.should == false end ['failure', 'noop', 'success'].each do |event_status| it "should treat #{event_status} events as out of sync" do 3.times do @status << Puppet::Transaction::Event.new(:status => event_status) end @status.out_of_sync_count.should == 3 @status.out_of_sync.should == true end end describe "When converting to YAML" do it "should include only documented attributes" do @status.file = "/foo.rb" @status.line = 27 @status.evaluation_time = 2.7 @status.tags = %w{one two} @status.to_yaml_properties.should =~ Puppet::Resource::Status::YAML_ATTRIBUTES end end it "should round trip through pson" do @status.file = "/foo.rb" @status.line = 27 @status.evaluation_time = 2.7 @status.tags = %w{one two} @status << Puppet::Transaction::Event.new(:name => :mode_changed, :status => 'audit') @status.failed = false @status.changed = true @status.out_of_sync = true @status.skipped = false @status.containment_path.should == @containment_path tripped = Puppet::Resource::Status.from_data_hash(PSON.parse(@status.to_pson)) tripped.title.should == @status.title tripped.containment_path.should == @status.containment_path tripped.file.should == @status.file tripped.line.should == @status.line tripped.resource.should == @status.resource tripped.resource_type.should == @status.resource_type tripped.evaluation_time.should == @status.evaluation_time tripped.tags.should == @status.tags tripped.time.should == @status.time tripped.failed.should == @status.failed tripped.changed.should == @status.changed tripped.out_of_sync.should == @status.out_of_sync tripped.skipped.should == @status.skipped tripped.change_count.should == @status.change_count tripped.out_of_sync_count.should == @status.out_of_sync_count events_as_hashes(tripped).should == events_as_hashes(@status) end def events_as_hashes(report) report.events.collect do |e| { :audited => e.audited, :property => e.property, :previous_value => e.previous_value, :desired_value => e.desired_value, :historical_value => e.historical_value, :message => e.message, :name => e.name, :status => e.status, :time => e.time, } end end end diff --git a/spec/unit/resource/type_spec.rb b/spec/unit/resource/type_spec.rb index 497f0e711..dade96a4b 100755 --- a/spec/unit/resource/type_spec.rb +++ b/spec/unit/resource/type_spec.rb @@ -1,777 +1,766 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/resource/type' require 'matchers/json' describe Puppet::Resource::Type do include JSONMatchers it "should have a 'name' attribute" do Puppet::Resource::Type.new(:hostclass, "foo").name.should == "foo" end - [:code, :doc, :line, :file, :resource_type_collection, :ruby_code].each do |attr| + [:code, :doc, :line, :file, :resource_type_collection].each do |attr| it "should have a '#{attr}' attribute" do type = Puppet::Resource::Type.new(:hostclass, "foo") type.send(attr.to_s + "=", "yay") type.send(attr).should == "yay" end end [:hostclass, :node, :definition].each do |type| it "should know when it is a #{type}" do Puppet::Resource::Type.new(type, "foo").send("#{type}?").should be_true end end it "should indirect 'resource_type'" do Puppet::Resource::Type.indirection.name.should == :resource_type end it "should default to 'parser' for its terminus class" do Puppet::Resource::Type.indirection.terminus_class.should == :parser end describe "when converting to json" do before do @type = Puppet::Resource::Type.new(:hostclass, "foo") end def from_json(json) Puppet::Resource::Type.from_data_hash(json) end def double_convert Puppet::Resource::Type.from_data_hash(PSON.parse(@type.to_pson)) end it "should include the name and type" do double_convert.name.should == @type.name double_convert.type.should == @type.type end it "should validate with only name and kind" do expect(@type.to_pson).to validate_against('api/schemas/resource_type.json') end it "should validate with all fields set" do @type.set_arguments("one" => nil, "two" => "foo") @type.line = 100 @type.doc = "A weird type" @type.file = "/etc/manifests/thing.pp" @type.parent = "one::two" expect(@type.to_pson).to validate_against('api/schemas/resource_type.json') end it "should include any arguments" do @type.set_arguments("one" => nil, "two" => "foo") double_convert.arguments.should == {"one" => nil, "two" => "foo"} end it "should not include arguments if none are present" do @type.to_pson["arguments"].should be_nil end [:line, :doc, :file, :parent].each do |attr| it "should include #{attr} when set" do @type.send(attr.to_s + "=", "value") double_convert.send(attr).should == "value" end it "should not include #{attr} when not set" do @type.to_pson[attr.to_s].should be_nil end end it "should not include docs if they are empty" do @type.doc = "" @type.to_pson["doc"].should be_nil end end describe "when a node" do it "should allow a regex as its name" do lambda { Puppet::Resource::Type.new(:node, /foo/) }.should_not raise_error end it "should allow an AST::HostName instance as its name" do regex = Puppet::Parser::AST::Regex.new(:value => /foo/) name = Puppet::Parser::AST::HostName.new(:value => regex) lambda { Puppet::Resource::Type.new(:node, name) }.should_not raise_error end it "should match against the regexp in the AST::HostName when a HostName instance is provided" do regex = Puppet::Parser::AST::Regex.new(:value => /\w/) name = Puppet::Parser::AST::HostName.new(:value => regex) node = Puppet::Resource::Type.new(:node, name) node.match("foo").should be_true end it "should return the value of the hostname if provided a string-form AST::HostName instance as the name" do name = Puppet::Parser::AST::HostName.new(:value => "foo") node = Puppet::Resource::Type.new(:node, name) node.name.should == "foo" end describe "and the name is a regex" do it "should have a method that indicates that this is the case" do Puppet::Resource::Type.new(:node, /w/).should be_name_is_regex end it "should set its namespace to ''" do Puppet::Resource::Type.new(:node, /w/).namespace.should == "" end it "should return the regex converted to a string when asked for its name" do Puppet::Resource::Type.new(:node, /ww/).name.should == "ww" end it "should downcase the regex when returning the name as a string" do Puppet::Resource::Type.new(:node, /W/).name.should == "w" end it "should remove non-alpha characters when returning the name as a string" do Puppet::Resource::Type.new(:node, /w*w/).name.should_not include("*") end it "should remove leading dots when returning the name as a string" do Puppet::Resource::Type.new(:node, /.ww/).name.should_not =~ /^\./ end it "should have a method for matching its regex name against a provided name" do Puppet::Resource::Type.new(:node, /.ww/).should respond_to(:match) end it "should return true when its regex matches the provided name" do Puppet::Resource::Type.new(:node, /\w/).match("foo").should be_true end it "should return true when its regex matches the provided name" do Puppet::Resource::Type.new(:node, /\w/).match("foo").should be_true end it "should return false when its regex does not match the provided name" do (!!Puppet::Resource::Type.new(:node, /\d/).match("foo")).should be_false end it "should return true when its name, as a string, is matched against an equal string" do Puppet::Resource::Type.new(:node, "foo").match("foo").should be_true end it "should return false when its name is matched against an unequal string" do Puppet::Resource::Type.new(:node, "foo").match("bar").should be_false end it "should match names insensitive to case" do Puppet::Resource::Type.new(:node, "fOo").match("foO").should be_true end end end describe "when initializing" do it "should require a resource super type" do Puppet::Resource::Type.new(:hostclass, "foo").type.should == :hostclass end it "should fail if provided an invalid resource super type" do lambda { Puppet::Resource::Type.new(:nope, "foo") }.should raise_error(ArgumentError) end it "should set its name to the downcased, stringified provided name" do Puppet::Resource::Type.new(:hostclass, "Foo::Bar".intern).name.should == "foo::bar" end it "should set its namespace to the downcased, stringified qualified name for classes" do Puppet::Resource::Type.new(:hostclass, "Foo::Bar::Baz".intern).namespace.should == "foo::bar::baz" end [:definition, :node].each do |type| it "should set its namespace to the downcased, stringified qualified portion of the name for #{type}s" do Puppet::Resource::Type.new(type, "Foo::Bar::Baz".intern).namespace.should == "foo::bar" end end %w{code line file doc}.each do |arg| it "should set #{arg} if provided" do type = Puppet::Resource::Type.new(:hostclass, "foo", arg.to_sym => "something") type.send(arg).should == "something" end end it "should set any provided arguments with the keys as symbols" do type = Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {:foo => "bar", :baz => "biz"}) type.should be_valid_parameter("foo") type.should be_valid_parameter("baz") end it "should set any provided arguments with they keys as strings" do type = Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {"foo" => "bar", "baz" => "biz"}) type.should be_valid_parameter(:foo) type.should be_valid_parameter(:baz) end it "should function if provided no arguments" do type = Puppet::Resource::Type.new(:hostclass, "foo") type.should_not be_valid_parameter(:foo) end end describe "when testing the validity of an attribute" do it "should return true if the parameter was typed at initialization" do Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {"foo" => "bar"}).should be_valid_parameter("foo") end it "should return true if it is a metaparam" do Puppet::Resource::Type.new(:hostclass, "foo").should be_valid_parameter("require") end it "should return true if the parameter is named 'name'" do Puppet::Resource::Type.new(:hostclass, "foo").should be_valid_parameter("name") end it "should return false if it is not a metaparam and was not provided at initialization" do Puppet::Resource::Type.new(:hostclass, "foo").should_not be_valid_parameter("yayness") end end describe "when setting its parameters in the scope" do before do @scope = Puppet::Parser::Scope.new(Puppet::Parser::Compiler.new(Puppet::Node.new("foo")), :source => stub("source")) @resource = Puppet::Parser::Resource.new(:foo, "bar", :scope => @scope) @type = Puppet::Resource::Type.new(:definition, "foo") @resource.environment.known_resource_types.add @type end ['module_name', 'name', 'title'].each do |variable| it "should allow #{variable} to be evaluated as param default" do @type.instance_eval { @module_name = "bar" } var = Puppet::Parser::AST::Variable.new({'value' => variable}) @type.set_arguments :foo => var @type.set_resource_parameters(@resource, @scope) @scope['foo'].should == 'bar' end end # this test is to clarify a crazy edge case # if you specify these special names as params, the resource # will override the special variables it "should allow the resource to override defaults" do @type.set_arguments :name => nil @resource[:name] = 'foobar' var = Puppet::Parser::AST::Variable.new({'value' => 'name'}) @type.set_arguments :foo => var @type.set_resource_parameters(@resource, @scope) @scope['foo'].should == 'foobar' end it "should set each of the resource's parameters as variables in the scope" do @type.set_arguments :foo => nil, :boo => nil @resource[:foo] = "bar" @resource[:boo] = "baz" @type.set_resource_parameters(@resource, @scope) @scope['foo'].should == "bar" @scope['boo'].should == "baz" end it "should set the variables as strings" do @type.set_arguments :foo => nil @resource[:foo] = "bar" @type.set_resource_parameters(@resource, @scope) @scope['foo'].should == "bar" end it "should fail if any of the resource's parameters are not valid attributes" do @type.set_arguments :foo => nil @resource[:boo] = "baz" lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError) end it "should evaluate and set its default values as variables for parameters not provided by the resource" do @type.set_arguments :foo => Puppet::Parser::AST::String.new(:value => "something") @type.set_resource_parameters(@resource, @scope) @scope['foo'].should == "something" end it "should set all default values as parameters in the resource" do @type.set_arguments :foo => Puppet::Parser::AST::String.new(:value => "something") @type.set_resource_parameters(@resource, @scope) @resource[:foo].should == "something" end it "should fail if the resource does not provide a value for a required argument" do @type.set_arguments :foo => nil lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError) end it "should set the resource's title as a variable if not otherwise provided" do @type.set_resource_parameters(@resource, @scope) @scope['title'].should == "bar" end it "should set the resource's name as a variable if not otherwise provided" do @type.set_resource_parameters(@resource, @scope) @scope['name'].should == "bar" end it "should set its module name in the scope if available" do @type.instance_eval { @module_name = "mymod" } @type.set_resource_parameters(@resource, @scope) @scope["module_name"].should == "mymod" end it "should set its caller module name in the scope if available" do @scope.expects(:parent_module_name).returns "mycaller" @type.set_resource_parameters(@resource, @scope) @scope["caller_module_name"].should == "mycaller" end end describe "when describing and managing parent classes" do before do environment = Puppet::Node::Environment.create(:testing, []) @krt = environment.known_resource_types @parent = Puppet::Resource::Type.new(:hostclass, "bar") @krt.add @parent @child = Puppet::Resource::Type.new(:hostclass, "foo", :parent => "bar") @krt.add @child @scope = Puppet::Parser::Scope.new(Puppet::Parser::Compiler.new(Puppet::Node.new("foo", :environment => environment))) end it "should be able to define a parent" do Puppet::Resource::Type.new(:hostclass, "foo", :parent => "bar") end it "should use the code collection to find the parent resource type" do @child.parent_type(@scope).should equal(@parent) end it "should be able to find parent nodes" do parent = Puppet::Resource::Type.new(:node, "bar") @krt.add parent child = Puppet::Resource::Type.new(:node, "foo", :parent => "bar") @krt.add child child.parent_type(@scope).should equal(parent) end it "should cache a reference to the parent type" do @krt.stubs(:hostclass).with("foo::bar").returns nil @krt.expects(:hostclass).with("bar").once.returns @parent @child.parent_type(@scope) @child.parent_type end it "should correctly state when it is another type's child" do @child.parent_type(@scope) @child.should be_child_of(@parent) end it "should be considered the child of a parent's parent" do @grandchild = Puppet::Resource::Type.new(:hostclass, "baz", :parent => "foo") @krt.add @grandchild @child.parent_type(@scope) @grandchild.parent_type(@scope) @grandchild.should be_child_of(@parent) end it "should correctly state when it is not another type's child" do @notchild = Puppet::Resource::Type.new(:hostclass, "baz") @krt.add @notchild @notchild.should_not be_child_of(@parent) end end describe "when evaluating its code" do before do @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) @scope = Puppet::Parser::Scope.new @compiler @resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope) # This is so the internal resource lookup works, yo. @compiler.catalog.add_resource @resource @type = Puppet::Resource::Type.new(:hostclass, "foo") @resource.environment.known_resource_types.add @type end it "should add node regex captures to its scope" do @type = Puppet::Resource::Type.new(:node, /f(\w)o(.*)$/) match = @type.match('foo') code = stub 'code' @type.stubs(:code).returns code subscope = stub 'subscope', :compiler => @compiler @scope.expects(:newscope).with(:source => @type, :namespace => '', :resource => @resource).returns subscope elevel = 876 subscope.expects(:ephemeral_level).returns elevel subscope.expects(:ephemeral_from).with(match, nil, nil).returns subscope code.expects(:safeevaluate).with(subscope) subscope.expects(:unset_ephemeral_var).with(elevel) # Just to keep the stub quiet about intermediate calls @type.expects(:set_resource_parameters).with(@resource, subscope) @type.evaluate_code(@resource) end it "should add hostclass names to the classes list" do @type.evaluate_code(@resource) @compiler.catalog.classes.should be_include("foo") end it "should not add defined resource names to the classes list" do @type = Puppet::Resource::Type.new(:definition, "foo") @type.evaluate_code(@resource) @compiler.catalog.classes.should_not be_include("foo") end it "should set all of its parameters in a subscope" do subscope = stub 'subscope', :compiler => @compiler @scope.expects(:newscope).with(:source => @type, :namespace => 'foo', :resource => @resource).returns subscope @type.expects(:set_resource_parameters).with(@resource, subscope) @type.evaluate_code(@resource) end it "should not create a subscope for the :main class" do @resource.stubs(:title).returns(:main) @type.expects(:subscope).never @type.expects(:set_resource_parameters).with(@resource, @scope) @type.evaluate_code(@resource) end it "should store the class scope" do @type.evaluate_code(@resource) @scope.class_scope(@type).should be_instance_of(@scope.class) end it "should still create a scope but not store it if the type is a definition" do @type = Puppet::Resource::Type.new(:definition, "foo") @type.evaluate_code(@resource) @scope.class_scope(@type).should be_nil end it "should evaluate the AST code if any is provided" do code = stub 'code' @type.stubs(:code).returns code subscope = stub_everything("subscope", :compiler => @compiler) @scope.stubs(:newscope).returns subscope code.expects(:safeevaluate).with subscope @type.evaluate_code(@resource) end - describe "and ruby code is provided" do - it "should create a DSL Resource API and evaluate it" do - @type.stubs(:ruby_code).returns(proc { "foo" }) - @api = stub 'api' - Puppet::DSL::ResourceAPI.expects(:new).with { |res, scope, code| code == @type.ruby_code }.returns @api - @api.expects(:evaluate) - - @type.evaluate_code(@resource) - end - end - it "should noop if there is no code" do @type.expects(:code).returns nil @type.evaluate_code(@resource) end describe "and it has a parent class" do before do @parent_type = Puppet::Resource::Type.new(:hostclass, "parent") @type.parent = "parent" @parent_resource = Puppet::Parser::Resource.new(:class, "parent", :scope => @scope) @compiler.add_resource @scope, @parent_resource @type.resource_type_collection = @scope.known_resource_types @type.resource_type_collection.add @parent_type end it "should evaluate the parent's resource" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@parent_type).should_not be_nil end it "should not evaluate the parent's resource if it has already been evaluated" do @parent_resource.evaluate @type.parent_type(@scope) @parent_resource.expects(:evaluate).never @type.evaluate_code(@resource) end it "should use the parent's scope as its base scope" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@type).parent.object_id.should == @scope.class_scope(@parent_type).object_id end end describe "and it has a parent node" do before do @type = Puppet::Resource::Type.new(:node, "foo") @parent_type = Puppet::Resource::Type.new(:node, "parent") @type.parent = "parent" @parent_resource = Puppet::Parser::Resource.new(:node, "parent", :scope => @scope) @compiler.add_resource @scope, @parent_resource @type.resource_type_collection = @scope.known_resource_types @type.resource_type_collection.add(@parent_type) end it "should evaluate the parent's resource" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@parent_type).should_not be_nil end it "should not evaluate the parent's resource if it has already been evaluated" do @parent_resource.evaluate @type.parent_type(@scope) @parent_resource.expects(:evaluate).never @type.evaluate_code(@resource) end it "should use the parent's scope as its base scope" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@type).parent.object_id.should == @scope.class_scope(@parent_type).object_id end end end describe "when creating a resource" do before do @node = Puppet::Node.new("foo", :environment => 'env') @compiler = Puppet::Parser::Compiler.new(@node) @scope = Puppet::Parser::Scope.new(@compiler) @top = Puppet::Resource::Type.new :hostclass, "top" @middle = Puppet::Resource::Type.new :hostclass, "middle", :parent => "top" @code = Puppet::Resource::TypeCollection.new("env") @code.add @top @code.add @middle @node.environment.stubs(:known_resource_types).returns(@code) end it "should create a resource instance" do @top.ensure_in_catalog(@scope).should be_instance_of(Puppet::Parser::Resource) end it "should set its resource type to 'class' when it is a hostclass" do Puppet::Resource::Type.new(:hostclass, "top").ensure_in_catalog(@scope).type.should == "Class" end it "should set its resource type to 'node' when it is a node" do Puppet::Resource::Type.new(:node, "top").ensure_in_catalog(@scope).type.should == "Node" end it "should fail when it is a definition" do lambda { Puppet::Resource::Type.new(:definition, "top").ensure_in_catalog(@scope) }.should raise_error(ArgumentError) end it "should add the created resource to the scope's catalog" do @top.ensure_in_catalog(@scope) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should add specified parameters to the resource" do @top.ensure_in_catalog(@scope, {'one'=>'1', 'two'=>'2'}) @compiler.catalog.resource(:class, "top")['one'].should == '1' @compiler.catalog.resource(:class, "top")['two'].should == '2' end it "should not require params for a param class" do @top.ensure_in_catalog(@scope, {}) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should evaluate the parent class if one exists" do @middle.ensure_in_catalog(@scope) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should evaluate the parent class if one exists" do @middle.ensure_in_catalog(@scope, {}) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should fail if you try to create duplicate class resources" do othertop = Puppet::Parser::Resource.new(:class, 'top',:source => @source, :scope => @scope ) # add the same class resource to the catalog @compiler.catalog.add_resource(othertop) lambda { @top.ensure_in_catalog(@scope, {}) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError) end it "should fail to evaluate if a parent class is defined but cannot be found" do othertop = Puppet::Resource::Type.new :hostclass, "something", :parent => "yay" @code.add othertop lambda { othertop.ensure_in_catalog(@scope) }.should raise_error(Puppet::ParseError) end it "should not create a new resource if one already exists" do @compiler.catalog.expects(:resource).with(:class, "top").returns("something") @compiler.catalog.expects(:add_resource).never @top.ensure_in_catalog(@scope) end it "should return the existing resource when not creating a new one" do @compiler.catalog.expects(:resource).with(:class, "top").returns("something") @compiler.catalog.expects(:add_resource).never @top.ensure_in_catalog(@scope).should == "something" end it "should not create a new parent resource if one already exists and it has a parent class" do @top.ensure_in_catalog(@scope) top_resource = @compiler.catalog.resource(:class, "top") @middle.ensure_in_catalog(@scope) @compiler.catalog.resource(:class, "top").should equal(top_resource) end # #795 - tag before evaluation. it "should tag the catalog with the resource tags when it is evaluated" do @middle.ensure_in_catalog(@scope) @compiler.catalog.should be_tagged("middle") end it "should tag the catalog with the parent class tags when it is evaluated" do @middle.ensure_in_catalog(@scope) @compiler.catalog.should be_tagged("top") end end describe "when merging code from another instance" do def code(str) Puppet::Parser::AST::Leaf.new :value => str end it "should fail unless it is a class" do lambda { Puppet::Resource::Type.new(:node, "bar").merge("foo") }.should raise_error(Puppet::Error) end it "should fail unless the source instance is a class" do dest = Puppet::Resource::Type.new(:hostclass, "bar") source = Puppet::Resource::Type.new(:node, "foo") lambda { dest.merge(source) }.should raise_error(Puppet::Error) end it "should fail if both classes have different parent classes" do code = Puppet::Resource::TypeCollection.new("env") {"a" => "b", "c" => "d"}.each do |parent, child| code.add Puppet::Resource::Type.new(:hostclass, parent) code.add Puppet::Resource::Type.new(:hostclass, child, :parent => parent) end lambda { code.hostclass("b").merge(code.hostclass("d")) }.should raise_error(Puppet::Error) end it "should fail if it's named 'main' and 'freeze_main' is enabled" do Puppet.settings[:freeze_main] = true code = Puppet::Resource::TypeCollection.new("env") code.add Puppet::Resource::Type.new(:hostclass, "") other = Puppet::Resource::Type.new(:hostclass, "") lambda { code.hostclass("").merge(other) }.should raise_error(Puppet::Error) end it "should copy the other class's parent if it has not parent" do dest = Puppet::Resource::Type.new(:hostclass, "bar") parent = Puppet::Resource::Type.new(:hostclass, "parent") source = Puppet::Resource::Type.new(:hostclass, "foo", :parent => "parent") dest.merge(source) dest.parent.should == "parent" end it "should copy the other class's documentation as its docs if it has no docs" do dest = Puppet::Resource::Type.new(:hostclass, "bar") source = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "yayness") dest.merge(source) dest.doc.should == "yayness" end it "should append the other class's docs to its docs if it has any" do dest = Puppet::Resource::Type.new(:hostclass, "bar", :doc => "fooness") source = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "yayness") dest.merge(source) dest.doc.should == "foonessyayness" end it "should set the other class's code as its code if it has none" do dest = Puppet::Resource::Type.new(:hostclass, "bar") source = Puppet::Resource::Type.new(:hostclass, "foo", :code => code("bar")) dest.merge(source) dest.code.value.should == "bar" end it "should append the other class's code to its code if it has any" do dcode = Puppet::Parser::AST::BlockExpression.new(:children => [code("dest")]) dest = Puppet::Resource::Type.new(:hostclass, "bar", :code => dcode) scode = Puppet::Parser::AST::BlockExpression.new(:children => [code("source")]) source = Puppet::Resource::Type.new(:hostclass, "foo", :code => scode) dest.merge(source) dest.code.children.collect { |l| l.value }.should == %w{dest source} end end end diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb index 7a4d02553..9609f7d8e 100755 --- a/spec/unit/type/file_spec.rb +++ b/spec/unit/type/file_spec.rb @@ -1,1504 +1,1504 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:file) do include PuppetSpec::Files let(:path) { tmpfile('file_testing') } let(:file) { described_class.new(:path => path, :catalog => catalog) } let(:provider) { file.provider } let(:catalog) { Puppet::Resource::Catalog.new } before do Puppet.features.stubs("posix?").returns(true) end describe "the path parameter" do describe "on POSIX systems", :if => Puppet.features.posix? do it "should remove trailing slashes" do file[:path] = "/foo/bar/baz/" file[:path].should == "/foo/bar/baz" end it "should remove double slashes" do file[:path] = "/foo/bar//baz" file[:path].should == "/foo/bar/baz" end it "should remove triple slashes" do file[:path] = "/foo/bar///baz" file[:path].should == "/foo/bar/baz" end it "should remove trailing double slashes" do file[:path] = "/foo/bar/baz//" file[:path].should == "/foo/bar/baz" end it "should leave a single slash alone" do file[:path] = "/" file[:path].should == "/" end it "should accept and collapse a double-slash at the start of the path" do file[:path] = "//tmp/xxx" file[:path].should == '/tmp/xxx' end it "should accept and collapse a triple-slash at the start of the path" do file[:path] = "///tmp/xxx" file[:path].should == '/tmp/xxx' end end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do it "should remove trailing slashes" do file[:path] = "X:/foo/bar/baz/" file[:path].should == "X:/foo/bar/baz" end it "should remove double slashes" do file[:path] = "X:/foo/bar//baz" file[:path].should == "X:/foo/bar/baz" end it "should remove trailing double slashes" do file[:path] = "X:/foo/bar/baz//" file[:path].should == "X:/foo/bar/baz" end it "should leave a drive letter with a slash alone" do file[:path] = "X:/" file[:path].should == "X:/" end it "should not accept a drive letter without a slash" do expect { file[:path] = "X:" }.to raise_error(/File paths must be fully qualified/) end describe "when using UNC filenames", :if => Puppet.features.microsoft_windows? do it "should remove trailing slashes" do file[:path] = "//localhost/foo/bar/baz/" file[:path].should == "//localhost/foo/bar/baz" end it "should remove double slashes" do file[:path] = "//localhost/foo/bar//baz" file[:path].should == "//localhost/foo/bar/baz" end it "should remove trailing double slashes" do file[:path] = "//localhost/foo/bar/baz//" file[:path].should == "//localhost/foo/bar/baz" end it "should remove a trailing slash from a sharename" do file[:path] = "//localhost/foo/" file[:path].should == "//localhost/foo" end it "should not modify a sharename" do file[:path] = "//localhost/foo" file[:path].should == "//localhost/foo" end end end end describe "the backup parameter" do [false, 'false', :false].each do |value| it "should disable backup if the value is #{value.inspect}" do file[:backup] = value file[:backup].should == false end end [true, 'true', '.puppet-bak'].each do |value| it "should use .puppet-bak if the value is #{value.inspect}" do file[:backup] = value file[:backup].should == '.puppet-bak' end end it "should use the provided value if it's any other string" do file[:backup] = "over there" file[:backup].should == "over there" end it "should fail if backup is set to anything else" do expect do file[:backup] = 97 end.to raise_error(Puppet::Error, /Invalid backup type 97/) end end describe "the recurse parameter" do it "should default to recursion being disabled" do file[:recurse].should be_false end - [true, "true", "inf", "remote"].each do |value| + [true, "true", "remote"].each do |value| it "should consider #{value} to enable recursion" do file[:recurse] = value file[:recurse].should be_true end end it "should not allow numbers" do expect { file[:recurse] = 10 }.to raise_error( Puppet::Error, /Parameter recurse failed on File\[[^\]]+\]: Invalid recurse value 10/) end [false, "false"].each do |value| it "should consider #{value} to disable recursion" do file[:recurse] = value file[:recurse].should be_false end end end describe "the recurselimit parameter" do it "should accept integers" do file[:recurselimit] = 12 file[:recurselimit].should == 12 end it "should munge string numbers to number numbers" do file[:recurselimit] = '12' file[:recurselimit].should == 12 end it "should fail if given a non-number" do expect do file[:recurselimit] = 'twelve' end.to raise_error(Puppet::Error, /Invalid value "twelve"/) end end describe "the replace parameter" do [true, :true, :yes].each do |value| it "should consider #{value} to be true" do file[:replace] = value file[:replace].should be_true end end [false, :false, :no].each do |value| it "should consider #{value} to be false" do file[:replace] = value file[:replace].should be_false end end end describe ".instances" do it "should return an empty array" do described_class.instances.should == [] end end describe "#bucket" do it "should return nil if backup is off" do file[:backup] = false file.bucket.should == nil end it "should not return a bucket if using a file extension for backup" do file[:backup] = '.backup' file.bucket.should == nil end it "should return the default filebucket if using the 'puppet' filebucket" do file[:backup] = 'puppet' bucket = stub('bucket') file.stubs(:default_bucket).returns bucket file.bucket.should == bucket end it "should fail if using a remote filebucket and no catalog exists" do file.catalog = nil file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Can not find filebucket for backups without a catalog") end it "should fail if the specified filebucket isn't in the catalog" do file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Could not find filebucket my_bucket specified in backup") end it "should use the specified filebucket if it is in the catalog" do file[:backup] = 'my_bucket' filebucket = Puppet::Type.type(:filebucket).new(:name => 'my_bucket') catalog.add_resource(filebucket) file.bucket.should == filebucket.bucket end end describe "#asuser" do before :each do # Mocha won't let me just stub SUIDManager.asuser to yield and return, # but it will do exactly that if we're not root. Puppet.features.stubs(:root?).returns false end it "should return the desired owner if they can write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns true file.asuser.should == 1001 end it "should return nil if the desired owner can't write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns false file.asuser.should == nil end it "should return nil if not managing owner" do file.asuser.should == nil end end describe "#exist?" do it "should be considered existent if it can be stat'ed" do file.expects(:stat).returns mock('stat') file.must be_exist end it "should be considered nonexistent if it can not be stat'ed" do file.expects(:stat).returns nil file.must_not be_exist end end describe "#eval_generate" do before do @graph = stub 'graph', :add_edge => nil catalog.stubs(:relationship_graph).returns @graph end it "should recurse if recursion is enabled" do resource = stub('resource', :[] => 'resource') file.expects(:recurse).returns [resource] file[:recurse] = true file.eval_generate.should == [resource] end it "should not recurse if recursion is disabled" do file.expects(:recurse).never file[:recurse] = false file.eval_generate.should == [] end end describe "#ancestors" do it "should return the ancestors of the file, in ascending order" do file = described_class.new(:path => make_absolute("/tmp/foo/bar/baz/qux")) pieces = %W[#{make_absolute('/')} tmp foo bar baz] ancestors = file.ancestors ancestors.should_not be_empty ancestors.reverse.each_with_index do |path,i| path.should == File.join(*pieces[0..i]) end end end describe "#flush" do it "should flush all properties that respond to :flush" do file[:source] = File.expand_path(__FILE__) file.parameter(:source).expects(:flush) file.flush end it "should reset its stat reference" do FileUtils.touch(path) stat1 = file.stat file.stat.should equal(stat1) file.flush file.stat.should_not equal(stat1) end end describe "#initialize" do it "should remove a trailing slash from the title to create the path" do title = File.expand_path("/abc/\n\tdef/") file = described_class.new(:title => title) file[:path].should == title end it "should set a desired 'ensure' value if none is set and 'content' is set" do file = described_class.new(:path => path, :content => "/foo/bar") file[:ensure].should == :file end it "should set a desired 'ensure' value if none is set and 'target' is set", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file = described_class.new(:path => path, :target => File.expand_path(__FILE__)) file[:ensure].should == :link end end describe "#mark_children_for_purging" do it "should set each child's ensure to absent" do paths = %w[foo bar baz] children = paths.inject({}) do |children,child| children.merge child => described_class.new(:path => File.join(path, child), :ensure => :present) end file.mark_children_for_purging(children) children.length.should == 3 children.values.each do |child| child[:ensure].should == :absent end end it "should skip children which have a source" do child = described_class.new(:path => path, :ensure => :present, :source => File.expand_path(__FILE__)) file.mark_children_for_purging('foo' => child) child[:ensure].should == :present end end describe "#newchild" do it "should create a new resource relative to the parent" do child = file.newchild('bar') child.must be_a(described_class) child[:path].should == File.join(file[:path], 'bar') end { :ensure => :present, :recurse => true, :recurselimit => 5, :target => "some_target", :source => File.expand_path("some_source"), }.each do |param, value| it "should omit the #{param} parameter", :if => described_class.defaultprovider.feature?(:manages_symlinks) do # Make a new file, because we have to set the param at initialization # or it wouldn't be copied regardless. file = described_class.new(:path => path, param => value) child = file.newchild('bar') child[param].should_not == value end end it "should copy all of the parent resource's 'should' values that were set at initialization" do parent = described_class.new(:path => path, :owner => 'root', :group => 'wheel') child = parent.newchild("my/path") child[:owner].should == 'root' child[:group].should == 'wheel' end it "should not copy default values to the new child" do child = file.newchild("my/path") child.original_parameters.should_not include(:backup) end it "should not copy values to the child which were set by the source" do source = File.expand_path(__FILE__) file[:source] = source metadata = stub 'metadata', :owner => "root", :group => "root", :mode => '0755', :ftype => "file", :checksum => "{md5}whatever", :source => source file.parameter(:source).stubs(:metadata).returns metadata file.parameter(:source).copy_source_values file.class.expects(:new).with { |params| params[:group].nil? } file.newchild("my/path") end end describe "#purge?" do it "should return false if purge is not set" do file.must_not be_purge end it "should return true if purge is set to true" do file[:purge] = true file.must be_purge end it "should return false if purge is set to false" do file[:purge] = false file.must_not be_purge end end describe "#recurse" do before do file[:recurse] = true @metadata = Puppet::FileServing::Metadata end describe "and a source is set" do it "should pass the already-discovered resources to recurse_remote" do file[:source] = File.expand_path(__FILE__) file.stubs(:recurse_local).returns(:foo => "bar") file.expects(:recurse_remote).with(:foo => "bar").returns [] file.recurse end end describe "and a target is set" do it "should use recurse_link" do file[:target] = File.expand_path(__FILE__) file.stubs(:recurse_local).returns(:foo => "bar") file.expects(:recurse_link).with(:foo => "bar").returns [] file.recurse end end it "should use recurse_local if recurse is not remote" do file.expects(:recurse_local).returns({}) file.recurse end it "should not use recurse_local if recurse is remote" do file[:recurse] = :remote file.expects(:recurse_local).never file.recurse end it "should return the generated resources as an array sorted by file path" do one = stub 'one', :[] => "/one" two = stub 'two', :[] => "/one/two" three = stub 'three', :[] => "/three" file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) file.recurse.should == [one, two, three] end describe "and purging is enabled" do before do file[:purge] = true end it "should mark each file for removal" do local = described_class.new(:path => path, :ensure => :present) file.expects(:recurse_local).returns("local" => local) file.recurse local[:ensure].should == :absent end it "should not remove files that exist in the remote repository" do file[:source] = File.expand_path(__FILE__) file.expects(:recurse_local).returns({}) remote = described_class.new(:path => path, :source => File.expand_path(__FILE__), :ensure => :present) file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } file.recurse remote[:ensure].should_not == :absent end end end describe "#remove_less_specific_files" do it "should remove any nested files that are already in the catalog" do foo = described_class.new :path => File.join(file[:path], 'foo') bar = described_class.new :path => File.join(file[:path], 'bar') baz = described_class.new :path => File.join(file[:path], 'baz') catalog.add_resource(foo) catalog.add_resource(bar) file.remove_less_specific_files([foo, bar, baz]).should == [baz] end end describe "#remove_less_specific_files" do it "should remove any nested files that are already in the catalog" do foo = described_class.new :path => File.join(file[:path], 'foo') bar = described_class.new :path => File.join(file[:path], 'bar') baz = described_class.new :path => File.join(file[:path], 'baz') catalog.add_resource(foo) catalog.add_resource(bar) file.remove_less_specific_files([foo, bar, baz]).should == [baz] end end describe "#recurse?" do it "should be true if recurse is true" do file[:recurse] = true file.must be_recurse end it "should be true if recurse is remote" do file[:recurse] = :remote file.must be_recurse end it "should be false if recurse is false" do file[:recurse] = false file.must_not be_recurse end end describe "#recurse_link" do before do @first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory" @second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file" @resource = stub 'file', :[]= => nil end it "should pass its target to the :perform_recursion method" do file[:target] = "mylinks" file.expects(:perform_recursion).with("mylinks").returns [@first] file.stubs(:newchild).returns @resource file.recurse_link({}) end it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do @first.stubs(:relative_path).returns "." file[:target] = "mylinks" file.expects(:perform_recursion).with("mylinks").returns [@first] file.stubs(:newchild).never file.expects(:[]=).with(:ensure, :directory) file.recurse_link({}) end it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do file.expects(:perform_recursion).returns [@first, @second] file.expects(:newchild).with(@first.relative_path).returns @resource file.recurse_link("second" => @resource) end it "should not create a new child resource for paths that already exist in the children hash" do file.expects(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_link("first" => @resource) end it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file.stubs(:perform_recursion).returns [@first, @second] file.recurse_link("first" => @resource, "second" => file) file[:ensure].should == :link file[:target].should == "/my/second" end it "should :ensure to :directory if the file is a directory" do file.stubs(:perform_recursion).returns [@first, @second] file.recurse_link("first" => file, "second" => @resource) file[:ensure].should == :directory end it "should return a hash with both created and existing resources with the relative paths as the hash keys" do file.expects(:perform_recursion).returns [@first, @second] file.stubs(:newchild).returns file file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file} end end describe "#recurse_local" do before do @metadata = stub 'metadata', :relative_path => "my/file" end it "should pass its path to the :perform_recursion method" do file.expects(:perform_recursion).with(file[:path]).returns [@metadata] file.stubs(:newchild) file.recurse_local end it "should return an empty hash if the recursion returns nothing" do file.expects(:perform_recursion).returns nil file.recurse_local.should == {} end it "should create a new child resource with each generated metadata instance's relative path" do file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" file.recurse_local end it "should not create a new child resource for the '.' directory" do @metadata.stubs(:relative_path).returns "." file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).never file.recurse_local end it "should return a hash of the created resources with the relative paths as the hash keys" do file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).with("my/file").returns "fiebar" file.recurse_local.should == {"my/file" => "fiebar"} end it "should set checksum_type to none if this file checksum is none" do file[:checksum] = :none Puppet::FileServing::Metadata.indirection.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] file.expects(:newchild).with("my/file").returns "fiebar" file.recurse_local end end describe "#recurse_remote", :uses_checksums => true do let(:my) { File.expand_path('/my') } before do file[:source] = "puppet://foo/bar" @first = Puppet::FileServing::Metadata.new(my, :relative_path => "first") @second = Puppet::FileServing::Metadata.new(my, :relative_path => "second") @first.stubs(:ftype).returns "directory" @second.stubs(:ftype).returns "directory" @parameter = stub 'property', :metadata= => nil @resource = stub 'file', :[]= => nil, :parameter => @parameter end it "should pass its source to the :perform_recursion method" do data = Puppet::FileServing::Metadata.new(File.expand_path("/whatever"), :relative_path => "foobar") file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] file.stubs(:newchild).returns @resource file.recurse_remote({}) end it "should not recurse when the remote file is not a directory" do data = Puppet::FileServing::Metadata.new(File.expand_path("/whatever"), :relative_path => ".") data.stubs(:ftype).returns "file" file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] file.expects(:newchild).never file.recurse_remote({}) end it "should set the source of each returned file to the searched-for URI plus the found relative path" do @first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path) file.expects(:perform_recursion).returns [@first] file.stubs(:newchild).returns @resource file.recurse_remote({}) end it "should create a new resource for any relative file paths that do not already have a resource" do file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).with("first").returns @resource file.recurse_remote({}).should == {"first" => @resource} end it "should not create a new resource for any relative file paths that do already have a resource" do file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_remote("first" => @resource) end it "should set the source of each resource to the source of the metadata" do file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path)) file.recurse_remote("first" => @resource) end # LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already # filed, and when it's fixed, we'll just fix the whole flow. with_digest_algorithms do it "it should set the checksum type to #{metadata[:digest_algorithm]} if the remote file is a file" do @first.stubs(:ftype).returns "file" file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:checksum, digest_algorithm.intern) file.recurse_remote("first" => @resource) end end it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do file.stubs(:perform_recursion).returns [@first] @resource.expects(:parameter).with(:source).returns @parameter @parameter.expects(:metadata=).with(@first) file.recurse_remote("first" => @resource) end it "should not create a new resource for the '.' file" do @first.stubs(:relative_path).returns "." file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_remote({}) end it "should store the metadata in the main file's source property if the relative path is '.'" do @first.stubs(:relative_path).returns "." file.stubs(:perform_recursion).returns [@first] file.parameter(:source).expects(:metadata=).with @first file.recurse_remote("first" => @resource) end describe "and multiple sources are provided" do let(:sources) do h = {} %w{/a /b /c /d}.each do |key| h[key] = URI.unescape(Puppet::Util.path_to_uri(File.expand_path(key)).to_s) end h end describe "and :sourceselect is set to :first" do it "should create file instances for the results for the first source to return any values" do data = Puppet::FileServing::Metadata.new(File.expand_path("/whatever"), :relative_path => "foobar") file[:source] = sources.keys.sort.map { |key| File.expand_path(key) } file.expects(:perform_recursion).with(sources['/a']).returns nil file.expects(:perform_recursion).with(sources['/b']).returns [] file.expects(:perform_recursion).with(sources['/c']).returns [data] file.expects(:perform_recursion).with(sources['/d']).never file.expects(:newchild).with("foobar").returns @resource file.recurse_remote({}) end end describe "and :sourceselect is set to :all" do before do file[:sourceselect] = :all end it "should return every found file that is not in a previous source" do klass = Puppet::FileServing::Metadata file[:source] = abs_path = %w{/a /b /c /d}.map {|f| File.expand_path(f) } file.stubs(:newchild).returns @resource one = [klass.new(abs_path[0], :relative_path => "a")] file.expects(:perform_recursion).with(sources['/a']).returns one file.expects(:newchild).with("a").returns @resource two = [klass.new(abs_path[1], :relative_path => "a"), klass.new(abs_path[1], :relative_path => "b")] file.expects(:perform_recursion).with(sources['/b']).returns two file.expects(:newchild).with("b").returns @resource three = [klass.new(abs_path[2], :relative_path => "a"), klass.new(abs_path[2], :relative_path => "c")] file.expects(:perform_recursion).with(sources['/c']).returns three file.expects(:newchild).with("c").returns @resource file.expects(:perform_recursion).with(sources['/d']).returns [] file.recurse_remote({}) end end end end describe "#perform_recursion" do it "should use Metadata to do its recursion" do Puppet::FileServing::Metadata.indirection.expects(:search) file.perform_recursion(file[:path]) end it "should use the provided path as the key to the search" do Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| key == "/foo" } file.perform_recursion("/foo") end it "should return the results of the metadata search" do Puppet::FileServing::Metadata.indirection.expects(:search).returns "foobar" file.perform_recursion(file[:path]).should == "foobar" end it "should pass its recursion value to the search" do file[:recurse] = true Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } file.perform_recursion(file[:path]) end it "should pass true if recursion is remote" do file[:recurse] = :remote Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } file.perform_recursion(file[:path]) end it "should pass its recursion limit value to the search" do file[:recurselimit] = 10 Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurselimit] == 10 } file.perform_recursion(file[:path]) end it "should configure the search to ignore or manage links" do file[:links] = :manage Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:links] == :manage } file.perform_recursion(file[:path]) end it "should pass its 'ignore' setting to the search if it has one" do file[:ignore] = %w{.svn CVS} Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } file.perform_recursion(file[:path]) end end describe "#remove_existing" do it "should do nothing if the file doesn't exist" do file.remove_existing(:file).should == false end it "should fail if it can't backup the file" do file.stubs(:stat).returns stub('stat', :ftype => 'file') file.stubs(:perform_backup).returns false expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up; will not replace/) end describe "backing up directories" do it "should not backup directories if force is false" do file[:force] = false file.stubs(:stat).returns stub('stat', :ftype => 'directory') file.expects(:perform_backup).never file.remove_existing(:file).should == false end it "should backup directories if force is true" do file[:force] = true FileUtils.expects(:rmtree).with(file[:path]) file.stubs(:stat).returns stub('stat', :ftype => 'directory') file.expects(:perform_backup).once.returns(true) file.remove_existing(:file).should == true end end it "should not do anything if the file is already the right type and not a link" do file.stubs(:stat).returns stub('stat', :ftype => 'file') file.remove_existing(:file).should == false end it "should not remove directories and should not invalidate the stat unless force is set" do # Actually call stat to set @needs_stat to nil file.stat file.stubs(:stat).returns stub('stat', :ftype => 'directory') file.remove_existing(:file) file.instance_variable_get(:@stat).should == nil @logs.should be_any {|log| log.level == :notice and log.message =~ /Not removing directory; use 'force' to override/} end it "should remove a directory if force is set" do file[:force] = true file.stubs(:stat).returns stub('stat', :ftype => 'directory') FileUtils.expects(:rmtree).with(file[:path]) file.remove_existing(:file).should == true end it "should remove an existing file" do file.stubs(:perform_backup).returns true FileUtils.touch(path) file.remove_existing(:directory).should == true Puppet::FileSystem.exist?(file[:path]).should == false end it "should remove an existing link", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file.stubs(:perform_backup).returns true target = tmpfile('link_target') FileUtils.touch(target) Puppet::FileSystem.symlink(target, path) file[:target] = target file.remove_existing(:directory).should == true Puppet::FileSystem.exist?(file[:path]).should == false end it "should fail if the file is not a file, link, or directory" do file.stubs(:stat).returns stub('stat', :ftype => 'socket') expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up files of type socket/) end it "should invalidate the existing stat of the file" do # Actually call stat to set @needs_stat to nil file.stat file.stubs(:stat).returns stub('stat', :ftype => 'file') Puppet::FileSystem.stubs(:unlink) file.remove_existing(:directory).should == true file.instance_variable_get(:@stat).should == :needs_stat end end describe "#retrieve" do it "should copy the source values if the 'source' parameter is set" do file[:source] = File.expand_path('/foo/bar') file.parameter(:source).expects(:copy_source_values) file.retrieve end end describe "#should_be_file?" do it "should have a method for determining if the file should be a normal file" do file.must respond_to(:should_be_file?) end it "should be a file if :ensure is set to :file" do file[:ensure] = :file file.must be_should_be_file end it "should be a file if :ensure is set to :present and the file exists as a normal file" do file.stubs(:stat).returns(mock('stat', :ftype => "file")) file[:ensure] = :present file.must be_should_be_file end it "should not be a file if :ensure is set to something other than :file" do file[:ensure] = :directory file.must_not be_should_be_file end it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do file.stubs(:stat).returns(mock('stat', :ftype => "directory")) file[:ensure] = :present file.must_not be_should_be_file end it "should be a file if :ensure is not set and :content is" do file[:content] = "foo" file.must be_should_be_file end it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do file.stubs(:stat).returns(mock("stat", :ftype => "file")) file.must be_should_be_file end it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do file.stubs(:stat).returns(mock("stat", :ftype => "directory")) file.must_not be_should_be_file end end describe "#stat", :if => described_class.defaultprovider.feature?(:manages_symlinks) do before do target = tmpfile('link_target') FileUtils.touch(target) Puppet::FileSystem.symlink(target, path) file[:target] = target file[:links] = :manage # so we always use :lstat end it "should stat the target if it is following links" do file[:links] = :follow file.stat.ftype.should == 'file' end it "should stat the link if is it not following links" do file[:links] = :manage file.stat.ftype.should == 'link' end it "should return nil if the file does not exist" do file[:path] = make_absolute('/foo/bar/baz/non-existent') file.stat.should be_nil end it "should return nil if the file cannot be stat'ed" do dir = tmpfile('link_test_dir') child = File.join(dir, 'some_file') Dir.mkdir(dir) File.chmod(0, dir) file[:path] = child file.stat.should be_nil # chmod it back so we can clean it up File.chmod(0777, dir) end it "should return nil if parts of path are no directories" do regular_file = tmpfile('ENOTDIR_test') FileUtils.touch(regular_file) impossible_child = File.join(regular_file, 'some_file') file[:path] = impossible_child file.stat.should be_nil end it "should return the stat instance" do file.stat.should be_a(File::Stat) end it "should cache the stat instance" do file.stat.should equal(file.stat) end end describe "#write" do describe "when validating the checksum" do before { file.stubs(:validate_checksum?).returns(true) } it "should fail if the checksum parameter and content checksums do not match" do checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b') file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') file.stubs(:property).with(:content).returns(property) expect { file.write :NOTUSED }.to raise_error(Puppet::Error) end end describe "when not validating the checksum" do before { file.stubs(:validate_checksum?).returns(false) } it "should not fail if the checksum property and content checksums do not match" do checksum = stub('checksum_parameter', :sum => 'checksum_b') file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') file.stubs(:property).with(:content).returns(property) expect { file.write :NOTUSED }.to_not raise_error end end describe "when resource mode is supplied" do before { file.stubs(:property_fix) } context "and writing temporary files" do before { file.stubs(:write_temporary_file?).returns(true) } it "should convert symbolic mode to int" do file[:mode] = 'oga=r' Puppet::Util.expects(:replace_file).with(file[:path], 0444) file.write :NOTUSED end it "should support int modes" do file[:mode] = '0444' Puppet::Util.expects(:replace_file).with(file[:path], 0444) file.write :NOTUSED end end context "and not writing temporary files" do before { file.stubs(:write_temporary_file?).returns(false) } it "should set a umask of 0" do file[:mode] = 'oga=r' Puppet::Util.expects(:withumask).with(0) file.write :NOTUSED end it "should convert symbolic mode to int" do file[:mode] = 'oga=r' File.expects(:open).with(file[:path], anything, 0444) file.write :NOTUSED end it "should support int modes" do file[:mode] = '0444' File.expects(:open).with(file[:path], anything, 0444) file.write :NOTUSED end end end describe "when resource mode is not supplied" do context "and content is supplied" do it "should default to 0644 mode" do file = described_class.new(:path => path, :content => "file content") file.write :NOTUSED expect(File.stat(file[:path]).mode & 0777).to eq(0644) end end context "and no content is supplied" do it "should use puppet's default umask of 022" do file = described_class.new(:path => path) umask_from_the_user = 0777 Puppet::Util.withumask(umask_from_the_user) do file.write :NOTUSED end expect(File.stat(file[:path]).mode & 0777).to eq(0644) end end end end describe "#fail_if_checksum_is_wrong" do it "should fail if the checksum of the file doesn't match the expected one" do expect do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns('wrong!!') fail_if_checksum_is_wrong(self[:path], 'anything!') end end.to raise_error(Puppet::Error, /File written to disk did not match checksum/) end it "should not fail if the checksum is correct" do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns('anything!') fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil end end it "should not fail if the checksum is absent" do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns(nil) fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil end end end describe "#write_content" do it "should delegate writing the file to the content property" do io = stub('io') file[:content] = "some content here" file.property(:content).expects(:write).with(io) file.send(:write_content, io) end end describe "#write_temporary_file?" do it "should be true if the file has specified content" do file[:content] = 'some content' file.send(:write_temporary_file?).should be_true end it "should be true if the file has specified source" do file[:source] = File.expand_path('/tmp/foo') file.send(:write_temporary_file?).should be_true end it "should be false if the file has neither content nor source" do file.send(:write_temporary_file?).should be_false end end describe "#property_fix" do { :mode => 0777, :owner => 'joeuser', :group => 'joeusers', :seluser => 'seluser', :selrole => 'selrole', :seltype => 'seltype', :selrange => 'selrange' }.each do |name,value| it "should sync the #{name} property if it's not in sync" do file[name] = value prop = file.property(name) prop.expects(:retrieve) prop.expects(:safe_insync?).returns false prop.expects(:sync) file.send(:property_fix) end end end describe "when autorequiring" do describe "target" do it "should require file resource when specified with the target property", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file = described_class.new(:path => File.expand_path("/foo"), :ensure => :directory) link = described_class.new(:path => File.expand_path("/bar"), :ensure => :link, :target => File.expand_path("/foo")) catalog.add_resource file catalog.add_resource link reqs = link.autorequire reqs.size.must == 1 reqs[0].source.must == file reqs[0].target.must == link end it "should require file resource when specified with the ensure property" do file = described_class.new(:path => File.expand_path("/foo"), :ensure => :directory) link = described_class.new(:path => File.expand_path("/bar"), :ensure => File.expand_path("/foo")) catalog.add_resource file catalog.add_resource link reqs = link.autorequire reqs.size.must == 1 reqs[0].source.must == file reqs[0].target.must == link end it "should not require target if target is not managed", :if => described_class.defaultprovider.feature?(:manages_symlinks) do link = described_class.new(:path => File.expand_path('/foo'), :ensure => :link, :target => '/bar') catalog.add_resource link link.autorequire.size.should == 0 end end describe "directories" do it "should autorequire its parent directory" do dir = described_class.new(:path => File.dirname(path)) catalog.add_resource file catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should autorequire its nearest ancestor directory" do dir = described_class.new(:path => File.dirname(path)) grandparent = described_class.new(:path => File.dirname(File.dirname(path))) catalog.add_resource file catalog.add_resource dir catalog.add_resource grandparent reqs = file.autorequire reqs.length.must == 1 reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire anything when there is no nearest ancestor directory" do catalog.add_resource file file.autorequire.should be_empty end it "should not autorequire its parent dir if its parent dir is itself" do file[:path] = File.expand_path('/') catalog.add_resource file file.autorequire.should be_empty end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do describe "when using UNC filenames" do it "should autorequire its parent directory" do file[:path] = '//localhost/foo/bar/baz' dir = described_class.new(:path => "//localhost/foo/bar") catalog.add_resource file catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should autorequire its nearest ancestor directory" do file = described_class.new(:path => "//localhost/foo/bar/baz/qux") dir = described_class.new(:path => "//localhost/foo/bar/baz") grandparent = described_class.new(:path => "//localhost/foo/bar") catalog.add_resource file catalog.add_resource dir catalog.add_resource grandparent reqs = file.autorequire reqs.length.must == 1 reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire anything when there is no nearest ancestor directory" do file = described_class.new(:path => "//localhost/foo/bar/baz/qux") catalog.add_resource file file.autorequire.should be_empty end it "should not autorequire its parent dir if its parent dir is itself" do file = described_class.new(:path => "//localhost/foo") catalog.add_resource file puts file.autorequire file.autorequire.should be_empty end end end end end describe "when managing links", :if => Puppet.features.manages_symlinks? do require 'tempfile' before :each do Dir.mkdir(path) @target = File.join(path, "target") @link = File.join(path, "link") target = described_class.new( :ensure => :file, :path => @target, :catalog => catalog, :content => 'yayness', :mode => '0644') catalog.add_resource target @link_resource = described_class.new( :ensure => :link, :path => @link, :target => @target, :catalog => catalog, :mode => '0755') catalog.add_resource @link_resource # to prevent the catalog from trying to write state.yaml Puppet::Util::Storage.stubs(:store) end it "should preserve the original file mode and ignore the one set by the link" do @link_resource[:links] = :manage # default catalog.apply # I convert them to strings so they display correctly if there's an error. (Puppet::FileSystem.stat(@target).mode & 007777).to_s(8).should == '644' end it "should manage the mode of the followed link" do pending("Windows cannot presently manage the mode when following symlinks", :if => Puppet.features.microsoft_windows?) do @link_resource[:links] = :follow catalog.apply (Puppet::FileSystem.stat(@target).mode & 007777).to_s(8).should == '755' end end end describe "when using source" do before do file[:source] = File.expand_path('/one') end Puppet::Type::File::ParameterChecksum.value_collection.values.reject {|v| v == :none}.each do |checksum_type| describe "with checksum '#{checksum_type}'" do before do file[:checksum] = checksum_type end it 'should validate' do expect { file.validate }.to_not raise_error end end end describe "with checksum 'none'" do before do file[:checksum] = :none end it 'should raise an exception when validating' do expect { file.validate }.to raise_error(/You cannot specify source when using checksum 'none'/) end end end describe "when using content" do before do file[:content] = 'file contents' end (Puppet::Type::File::ParameterChecksum.value_collection.values - SOURCE_ONLY_CHECKSUMS).each do |checksum_type| describe "with checksum '#{checksum_type}'" do before do file[:checksum] = checksum_type end it 'should validate' do expect { file.validate }.to_not raise_error end end end SOURCE_ONLY_CHECKSUMS.each do |checksum_type| describe "with checksum '#{checksum_type}'" do it 'should raise an exception when validating' do file[:checksum] = checksum_type expect { file.validate }.to raise_error(/You cannot specify content when using checksum '#{checksum_type}'/) end end end end describe "when auditing" do before :each do # to prevent the catalog from trying to write state.yaml Puppet::Util::Storage.stubs(:store) end it "should not fail if creating a new file if group is not set" do file = described_class.new(:path => path, :audit => 'all', :content => 'content') catalog.add_resource(file) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed File.read(path).should == 'content' end it "should not log errors if creating a new file with ensure present and no content" do file[:audit] = 'content' file[:ensure] = 'present' catalog.add_resource(file) catalog.apply Puppet::FileSystem.exist?(path).should be_true @logs.should_not be_any {|l| l.level != :notice } end end describe "when specifying both source and checksum" do it 'should use the specified checksum when source is first' do file[:source] = File.expand_path('/foo') file[:checksum] = :md5lite file[:checksum].should == :md5lite end it 'should use the specified checksum when source is last' do file[:checksum] = :md5lite file[:source] = File.expand_path('/foo') file[:checksum].should == :md5lite end end describe "when validating" do [[:source, :target], [:source, :content], [:target, :content]].each do |prop1,prop2| it "should fail if both #{prop1} and #{prop2} are specified" do file[prop1] = prop1 == :source ? File.expand_path("prop1 value") : "prop1 value" file[prop2] = "prop2 value" expect do file.validate end.to raise_error(Puppet::Error, /You cannot specify more than one of/) end end end end diff --git a/spec/unit/type/tidy_spec.rb b/spec/unit/type/tidy_spec.rb index 48a930b66..b0f40c280 100755 --- a/spec/unit/type/tidy_spec.rb +++ b/spec/unit/type/tidy_spec.rb @@ -1,433 +1,445 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/file_bucket/dipper' tidy = Puppet::Type.type(:tidy) describe tidy do include PuppetSpec::Files before do @basepath = make_absolute("/what/ever") Puppet.settings.stubs(:use) end it "should use :lstat when stating a file" do path = '/foo/bar' stat = mock 'stat' Puppet::FileSystem.expects(:lstat).with(path).returns stat resource = tidy.new :path => path, :age => "1d" resource.stat(path).should == stat end [:age, :size, :path, :matches, :type, :recurse, :rmdirs].each do |param| it "should have a #{param} parameter" do Puppet::Type.type(:tidy).attrclass(param).ancestors.should be_include(Puppet::Parameter) end it "should have documentation for its #{param} param" do Puppet::Type.type(:tidy).attrclass(param).doc.should be_instance_of(String) end end describe "when validating parameter values" do describe "for 'recurse'" do before do @tidy = Puppet::Type.type(:tidy).new :path => "/tmp", :age => "100d" end it "should allow 'true'" do lambda { @tidy[:recurse] = true }.should_not raise_error end it "should allow 'false'" do lambda { @tidy[:recurse] = false }.should_not raise_error end it "should allow integers" do lambda { @tidy[:recurse] = 10 }.should_not raise_error end it "should allow string representations of integers" do lambda { @tidy[:recurse] = "10" }.should_not raise_error end it "should allow 'inf'" do lambda { @tidy[:recurse] = "inf" }.should_not raise_error end it "should not allow arbitrary values" do lambda { @tidy[:recurse] = "whatever" }.should raise_error end end describe "for 'matches'" do before do @tidy = Puppet::Type.type(:tidy).new :path => "/tmp", :age => "100d" end it "should object if matches is given with recurse is not specified" do lambda { @tidy[:matches] = '*.doh' }.should raise_error end it "should object if matches is given and recurse is 0" do lambda { @tidy[:recurse] = 0; @tidy[:matches] = '*.doh' }.should raise_error end it "should object if matches is given and recurse is false" do lambda { @tidy[:recurse] = false; @tidy[:matches] = '*.doh' }.should raise_error end it "should not object if matches is given and recurse is > 0" do lambda { @tidy[:recurse] = 1; @tidy[:matches] = '*.doh' }.should_not raise_error end it "should not object if matches is given and recurse is true" do lambda { @tidy[:recurse] = true; @tidy[:matches] = '*.doh' }.should_not raise_error end end end describe "when matching files by age" do convertors = { :second => 1, :minute => 60 } convertors[:hour] = convertors[:minute] * 60 convertors[:day] = convertors[:hour] * 24 convertors[:week] = convertors[:day] * 7 convertors.each do |unit, multiple| it "should consider a #{unit} to be #{multiple} seconds" do @tidy = Puppet::Type.type(:tidy).new :path => @basepath, :age => "5#{unit.to_s[0..0]}" @tidy[:age].should == 5 * multiple end end end describe "when matching files by size" do convertors = { :b => 0, :kb => 1, :mb => 2, :gb => 3, :tb => 4 } convertors.each do |unit, multiple| it "should consider a #{unit} to be 1024^#{multiple} bytes" do @tidy = Puppet::Type.type(:tidy).new :path => @basepath, :size => "5#{unit}" total = 5 multiple.times { total *= 1024 } @tidy[:size].should == total end end end describe "when tidying" do before do @tidy = Puppet::Type.type(:tidy).new :path => @basepath @stat = stub 'stat', :ftype => "directory" lstat_is(@basepath, @stat) end describe "and generating files" do it "should set the backup on the file if backup is set on the tidy instance" do @tidy[:backup] = "whatever" Puppet::Type.type(:file).expects(:new).with { |args| args[:backup] == "whatever" } @tidy.mkfile(@basepath) end it "should set the file's path to the tidy's path" do Puppet::Type.type(:file).expects(:new).with { |args| args[:path] == @basepath } @tidy.mkfile(@basepath) end it "should configure the file for deletion" do Puppet::Type.type(:file).expects(:new).with { |args| args[:ensure] == :absent } @tidy.mkfile(@basepath) end it "should force deletion on the file" do Puppet::Type.type(:file).expects(:new).with { |args| args[:force] == true } @tidy.mkfile(@basepath) end it "should do nothing if the targeted file does not exist" do lstat_raises(@basepath, Errno::ENOENT) @tidy.generate.should == [] end end describe "and recursion is not used" do it "should generate a file resource if the file should be tidied" do @tidy.expects(:tidy?).with(@basepath).returns true file = Puppet::Type.type(:file).new(:path => @basepath+"/eh") @tidy.expects(:mkfile).with(@basepath).returns file @tidy.generate.should == [file] end it "should do nothing if the file should not be tidied" do @tidy.expects(:tidy?).with(@basepath).returns false @tidy.expects(:mkfile).never @tidy.generate.should == [] end end describe "and recursion is used" do before do @tidy[:recurse] = true Puppet::FileServing::Fileset.any_instance.stubs(:stat).returns mock("stat") @fileset = Puppet::FileServing::Fileset.new(@basepath) Puppet::FileServing::Fileset.stubs(:new).returns @fileset end it "should use a Fileset for infinite recursion" do Puppet::FileServing::Fileset.expects(:new).with(@basepath, :recurse => true).returns @fileset @fileset.expects(:files).returns %w{. one two} @tidy.stubs(:tidy?).returns false @tidy.generate end it "should use a Fileset for limited recursion" do @tidy[:recurse] = 42 Puppet::FileServing::Fileset.expects(:new).with(@basepath, :recurse => true, :recurselimit => 42).returns @fileset @fileset.expects(:files).returns %w{. one two} @tidy.stubs(:tidy?).returns false @tidy.generate end it "should generate a file resource for every file that should be tidied but not for files that should not be tidied" do @fileset.expects(:files).returns %w{. one two} @tidy.expects(:tidy?).with(@basepath).returns true @tidy.expects(:tidy?).with(@basepath+"/one").returns true @tidy.expects(:tidy?).with(@basepath+"/two").returns false file = Puppet::Type.type(:file).new(:path => @basepath+"/eh") @tidy.expects(:mkfile).with(@basepath).returns file @tidy.expects(:mkfile).with(@basepath+"/one").returns file @tidy.generate end end describe "and determining whether a file matches provided glob patterns" do before do @tidy = Puppet::Type.type(:tidy).new :path => @basepath, :recurse => 1 @tidy[:matches] = %w{*foo* *bar*} @stat = mock 'stat' @matcher = @tidy.parameter(:matches) end it "should always convert the globs to an array" do @matcher.value = "*foo*" @matcher.value.should == %w{*foo*} end it "should return true if any pattern matches the last part of the file" do @matcher.value = %w{*foo* *bar*} @matcher.must be_tidy("/file/yaybarness", @stat) end it "should return false if no pattern matches the last part of the file" do @matcher.value = %w{*foo* *bar*} @matcher.should_not be_tidy("/file/yayness", @stat) end end describe "and determining whether a file is too old" do before do @tidy = Puppet::Type.type(:tidy).new :path => @basepath @stat = stub 'stat' @tidy[:age] = "1s" @tidy[:type] = "mtime" @ager = @tidy.parameter(:age) end it "should use the age type specified" do @tidy[:type] = :ctime @stat.expects(:ctime).returns(Time.now) @ager.tidy?(@basepath, @stat) end it "should return false if the file is more recent than the specified age" do @stat.expects(:mtime).returns(Time.now) @ager.should_not be_tidy(@basepath, @stat) end it "should return true if the file is older than the specified age" do @stat.expects(:mtime).returns(Time.now - 10) @ager.must be_tidy(@basepath, @stat) end end describe "and determining whether a file is too large" do before do @tidy = Puppet::Type.type(:tidy).new :path => @basepath @stat = stub 'stat', :ftype => "file" @tidy[:size] = "1kb" @sizer = @tidy.parameter(:size) end it "should return false if the file is smaller than the specified size" do @stat.expects(:size).returns(4) # smaller than a kilobyte @sizer.should_not be_tidy(@basepath, @stat) end it "should return true if the file is larger than the specified size" do @stat.expects(:size).returns(1500) # larger than a kilobyte @sizer.must be_tidy(@basepath, @stat) end it "should return true if the file is equal to the specified size" do @stat.expects(:size).returns(1024) @sizer.must be_tidy(@basepath, @stat) end end describe "and determining whether a file should be tidied" do before do @tidy = Puppet::Type.type(:tidy).new :path => @basepath @stat = stub 'stat', :ftype => "file" lstat_is(@basepath, @stat) end it "should not try to recurse if the file does not exist" do @tidy[:recurse] = true lstat_is(@basepath, nil) @tidy.generate.should == [] end it "should not be tidied if the file does not exist" do lstat_raises(@basepath, Errno::ENOENT) @tidy.should_not be_tidy(@basepath) end it "should not be tidied if the user has no access to the file" do lstat_raises(@basepath, Errno::EACCES) @tidy.should_not be_tidy(@basepath) end it "should not be tidied if it is a directory and rmdirs is set to false" do stat = mock 'stat', :ftype => "directory" lstat_is(@basepath, stat) @tidy.should_not be_tidy(@basepath) end it "should return false if it does not match any provided globs" do @tidy[:recurse] = 1 @tidy[:matches] = "globs" matches = @tidy.parameter(:matches) matches.expects(:tidy?).with(@basepath, @stat).returns false @tidy.should_not be_tidy(@basepath) end it "should return false if it does not match aging requirements" do @tidy[:age] = "1d" ager = @tidy.parameter(:age) ager.expects(:tidy?).with(@basepath, @stat).returns false @tidy.should_not be_tidy(@basepath) end it "should return false if it does not match size requirements" do @tidy[:size] = "1b" sizer = @tidy.parameter(:size) sizer.expects(:tidy?).with(@basepath, @stat).returns false @tidy.should_not be_tidy(@basepath) end it "should tidy a file if age and size are set but only size matches" do @tidy[:size] = "1b" @tidy[:age] = "1d" @tidy.parameter(:size).stubs(:tidy?).returns true @tidy.parameter(:age).stubs(:tidy?).returns false @tidy.must be_tidy(@basepath) end it "should tidy a file if age and size are set but only age matches" do @tidy[:size] = "1b" @tidy[:age] = "1d" @tidy.parameter(:size).stubs(:tidy?).returns false @tidy.parameter(:age).stubs(:tidy?).returns true @tidy.must be_tidy(@basepath) end it "should tidy all files if neither age nor size is set" do @tidy.must be_tidy(@basepath) end it "should sort the results inversely by path length, so files are added to the catalog before their directories" do @tidy[:recurse] = true @tidy[:rmdirs] = true fileset = Puppet::FileServing::Fileset.new(@basepath) Puppet::FileServing::Fileset.expects(:new).returns fileset fileset.expects(:files).returns %w{. one one/two} @tidy.stubs(:tidy?).returns true @tidy.generate.collect { |r| r[:path] }.should == [@basepath+"/one/two", @basepath+"/one", @basepath] end end it "should configure directories to require their contained files if rmdirs is enabled, so the files will be deleted first" do @tidy[:recurse] = true @tidy[:rmdirs] = true fileset = mock 'fileset' Puppet::FileServing::Fileset.expects(:new).with(@basepath, :recurse => true).returns fileset fileset.expects(:files).returns %w{. one two one/subone two/subtwo one/subone/ssone} @tidy.stubs(:tidy?).returns true result = @tidy.generate.inject({}) { |hash, res| hash[res[:path]] = res; hash } { @basepath => [ @basepath+"/one", @basepath+"/two" ], @basepath+"/one" => [@basepath+"/one/subone"], @basepath+"/two" => [@basepath+"/two/subtwo"], @basepath+"/one/subone" => [@basepath+"/one/subone/ssone"] }.each do |parent, children| children.each do |child| ref = Puppet::Resource.new(:file, child) result[parent][:require].find { |req| req.to_s == ref.to_s }.should_not be_nil end end end + + it "should configure directories to require their contained files in sorted order" do + @tidy[:recurse] = true + @tidy[:rmdirs] = true + fileset = mock 'fileset' + Puppet::FileServing::Fileset.expects(:new).with(@basepath, :recurse => true).returns fileset + fileset.expects(:files).returns %w{. a a/2 a/1 a/3} + @tidy.stubs(:tidy?).returns true + + result = @tidy.generate.inject({}) { |hash, res| hash[res[:path]] = res; hash } + result[@basepath + '/a'][:require].collect{|a| a.name[('File//a/' + @basepath).length..-1]}.join().should == '321' + end end def lstat_is(path, stat) Puppet::FileSystem.stubs(:lstat).with(path).returns(stat) end def lstat_raises(path, error_class) Puppet::FileSystem.expects(:lstat).with(path).raises Errno::ENOENT end end diff --git a/spec/unit/util/command_line_spec.rb b/spec/unit/util/command_line_spec.rb index 9eb61b077..cb6ec7a48 100755 --- a/spec/unit/util/command_line_spec.rb +++ b/spec/unit/util/command_line_spec.rb @@ -1,192 +1,185 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/face' require 'puppet/util/command_line' describe Puppet::Util::CommandLine do include PuppetSpec::Files context "#initialize" do it "should pull off the first argument if it looks like a subcommand" do command_line = Puppet::Util::CommandLine.new("puppet", %w{ client --help whatever.pp }) command_line.subcommand_name.should == "client" command_line.args.should == %w{ --help whatever.pp } end it "should return nil if the first argument looks like a .pp file" do command_line = Puppet::Util::CommandLine.new("puppet", %w{ whatever.pp }) command_line.subcommand_name.should == nil command_line.args.should == %w{ whatever.pp } end - it "should return nil if the first argument looks like a .rb file" do - command_line = Puppet::Util::CommandLine.new("puppet", %w{ whatever.rb }) - - command_line.subcommand_name.should == nil - command_line.args.should == %w{ whatever.rb } - end - it "should return nil if the first argument looks like a flag" do command_line = Puppet::Util::CommandLine.new("puppet", %w{ --debug }) command_line.subcommand_name.should == nil command_line.args.should == %w{ --debug } end it "should return nil if the first argument is -" do command_line = Puppet::Util::CommandLine.new("puppet", %w{ - }) command_line.subcommand_name.should == nil command_line.args.should == %w{ - } end it "should return nil if the first argument is --help" do command_line = Puppet::Util::CommandLine.new("puppet", %w{ --help }) command_line.subcommand_name.should == nil end it "should return nil if there are no arguments" do command_line = Puppet::Util::CommandLine.new("puppet", []) command_line.subcommand_name.should == nil command_line.args.should == [] end it "should pick up changes to the array of arguments" do args = %w{subcommand} command_line = Puppet::Util::CommandLine.new("puppet", args) args[0] = 'different_subcommand' command_line.subcommand_name.should == 'different_subcommand' end end context "#execute" do %w{--version -V}.each do |arg| it "should print the version and exit if #{arg} is given" do expect do described_class.new("puppet", [arg]).execute end.to have_printed(/^#{Regexp.escape(Puppet.version)}$/) end end end describe "when dealing with puppet commands" do it "should return the executable name if it is not puppet" do command_line = Puppet::Util::CommandLine.new("puppetmasterd", []) command_line.subcommand_name.should == "puppetmasterd" end describe "when the subcommand is not implemented" do it "should find and invoke an executable with a hyphenated name" do commandline = Puppet::Util::CommandLine.new("puppet", ['whatever', 'argument']) Puppet::Util.expects(:which).with('puppet-whatever'). returns('/dev/null/puppet-whatever') Kernel.expects(:exec).with('/dev/null/puppet-whatever', 'argument') commandline.execute end describe "and an external implementation cannot be found" do before :each do Puppet::Util::CommandLine::UnknownSubcommand.any_instance.stubs(:console_has_color?).returns false end it "should abort and show the usage message" do Puppet::Util.expects(:which).with('puppet-whatever').returns(nil) commandline = Puppet::Util::CommandLine.new("puppet", ['whatever', 'argument']) commandline.expects(:exec).never expect { commandline.execute }.to have_printed(/Unknown Puppet subcommand 'whatever'/).and_exit_with(1) end it "should abort and show the help message" do Puppet::Util.expects(:which).with('puppet-whatever').returns(nil) commandline = Puppet::Util::CommandLine.new("puppet", ['whatever', 'argument']) commandline.expects(:exec).never expect { commandline.execute }.to have_printed(/See 'puppet help' for help on available puppet subcommands/).and_exit_with(1) end %w{--version -V}.each do |arg| it "should abort and display #{arg} information" do Puppet::Util.expects(:which).with('puppet-whatever').returns(nil) commandline = Puppet::Util::CommandLine.new("puppet", ['whatever', arg]) commandline.expects(:exec).never expect { commandline.execute }.to have_printed(%r[^#{Regexp.escape(Puppet.version)}$]).and_exit_with(1) end end end end describe 'when loading commands' do it "should deprecate the available_subcommands instance method" do Puppet::Application.expects(:available_application_names) Puppet.expects(:deprecation_warning).with("Puppet::Util::CommandLine#available_subcommands is deprecated; please use Puppet::Application.available_application_names instead.") command_line = Puppet::Util::CommandLine.new("foo", %w{ client --help whatever.pp }) command_line.available_subcommands end it "should deprecate the available_subcommands class method" do Puppet::Application.expects(:available_application_names) Puppet.expects(:deprecation_warning).with("Puppet::Util::CommandLine.available_subcommands is deprecated; please use Puppet::Application.available_application_names instead.") Puppet::Util::CommandLine.available_subcommands end end describe 'when setting process priority' do let(:command_line) do Puppet::Util::CommandLine.new("puppet", %w{ agent }) end before :each do Puppet::Util::CommandLine::ApplicationSubcommand.any_instance.stubs(:run) end it 'should never set priority by default' do Process.expects(:setpriority).never command_line.execute end it 'should lower the process priority if one has been specified' do Puppet[:priority] = 10 Process.expects(:setpriority).with(0, Process.pid, 10) command_line.execute end it 'should warn if trying to raise priority, but not privileged user' do Puppet[:priority] = -10 Process.expects(:setpriority).raises(Errno::EACCES, 'Permission denied') Puppet.expects(:warning).with("Failed to set process priority to '-10'") command_line.execute end it "should warn if the platform doesn't support `Process.setpriority`" do Puppet[:priority] = 15 Process.expects(:setpriority).raises(NotImplementedError, 'NotImplementedError: setpriority() function is unimplemented on this machine') Puppet.expects(:warning).with("Failed to set process priority to '15'") command_line.execute end end end end