diff --git a/lib/puppet/application.rb b/lib/puppet/application.rb index f0159a65d..17ad69cee 100644 --- a/lib/puppet/application.rb +++ b/lib/puppet/application.rb @@ -1,400 +1,407 @@ require 'optparse' # This class handles all the aspects of a Puppet application/executable # * setting up options # * setting up logs # * choosing what to run # * representing execution status # # === Usage # An application is a subclass of Puppet::Application. # # For legacy compatibility, # Puppet::Application[:example].run # is equivalent to # Puppet::Application::Example.new.run # # # class Puppet::Application::Example << Puppet::Application # # def preinit # # perform some pre initialization # @all = false # end # # # run_command is called to actually run the specified command # def run_command # send Puppet::Util::CommandLine.new.args.shift # end # # # option uses metaprogramming to create a method # # and also tells the option parser how to invoke that method # option("--arg ARGUMENT") do |v| # @args << v # end # # option("--debug", "-d") do |v| # @debug = v # end # # option("--all", "-a:) do |v| # @all = v # end # # def handle_unknown(opt,arg) # # last chance to manage an option # ... # # let's say to the framework we finally handle this option # true # end # # def read # # read action # end # # def write # # writeaction # end # # end # # === Preinit # The preinit block is the first code to be called in your application, before option parsing, # setup or command execution. # # === Options # Puppet::Application uses +OptionParser+ to manage the application options. # Options are defined with the +option+ method to which are passed various # arguments, including the long option, the short option, a description... # Refer to +OptionParser+ documentation for the exact format. # * If the option method is given a block, this one will be called whenever # the option is encountered in the command-line argument. # * If the option method has no block, a default functionnality will be used, that # stores the argument (or true/false if the option doesn't require an argument) in # the global (to the application) options array. # * If a given option was not defined by a the +option+ method, but it exists as a Puppet settings: # * if +unknown+ was used with a block, it will be called with the option name and argument # * if +unknown+ wasn't used, then the option/argument is handed to Puppet.settings.handlearg for # a default behavior # # --help is managed directly by the Puppet::Application class, but can be overriden. # # === Setup # Applications can use the setup block to perform any initialization. # The defaul +setup+ behaviour is to: read Puppet configuration and manage log level and destination # # === What and how to run # If the +dispatch+ block is defined it is called. This block should return the name of the registered command # to be run. # If it doesn't exist, it defaults to execute the +main+ command if defined. # # === Execution state # The class attributes/methods of Puppet::Application serve as a global place to set and query the execution # status of the application: stopping, restarting, etc. The setting of the application status does not directly # aftect its running status; it's assumed that the various components within the application will consult these # settings appropriately and affect their own processing accordingly. Control operations (signal handlers and # the like) should set the status appropriately to indicate to the overall system that it's the process of # stopping or restarting (or just running as usual). # # So, if something in your application needs to stop the process, for some reason, you might consider: # # def stop_me! # # indicate that we're stopping # Puppet::Application.stop! # # ...do stuff... # end # # And, if you have some component that involves a long-running process, you might want to consider: # # def my_long_process(giant_list_to_munge) # giant_list_to_munge.collect do |member| # # bail if we're stopping # return if Puppet::Application.stop_requested? # process_member(member) # end # end module Puppet class Application require 'puppet/util' include Puppet::Util DOCPATTERN = File.expand_path(File.dirname(__FILE__) + "/util/command_line/*" ) class << self include Puppet::Util attr_accessor :run_status def clear! self.run_status = nil end def stop! self.run_status = :stop_requested end def restart! self.run_status = :restart_requested end # Indicates that Puppet::Application.restart! has been invoked and components should # do what is necessary to facilitate a restart. def restart_requested? :restart_requested == run_status end # Indicates that Puppet::Application.stop! has been invoked and components should do what is necessary # for a clean stop. def stop_requested? :stop_requested == run_status end # Indicates that one of stop! or start! was invoked on Puppet::Application, and some kind of process # shutdown/short-circuit may be necessary. def interrupted? [:restart_requested, :stop_requested].include? run_status end # Indicates that Puppet::Application believes that it's in usual running run_mode (no stop/restart request # currently active). def clear? run_status.nil? end # Only executes the given block if the run status of Puppet::Application is clear (no restarts, stops, # etc. requested). # Upon block execution, checks the run status again; if a restart has been requested during the block's # execution, then controlled_run will send a new HUP signal to the current process. # Thus, long-running background processes can potentially finish their work before a restart. def controlled_run(&block) return unless clear? result = block.call Process.kill(:HUP, $PID) if restart_requested? result end def should_parse_config @parse_config = true end def should_not_parse_config @parse_config = false end def should_parse_config? @parse_config = true if ! defined?(@parse_config) @parse_config end # used to declare code that handle an option def option(*options, &block) long = options.find { |opt| opt =~ /^--/ }.gsub(/^--(?:\[no-\])?([^ =]+).*$/, '\1' ).gsub('-','_') fname = symbolize("handle_#{long}") if (block_given?) define_method(fname, &block) else define_method(fname) do |value| self.options["#{long}".to_sym] = value end end self.option_parser_commands << [options, fname] end def banner(banner = nil) @banner ||= banner end def option_parser_commands @option_parser_commands ||= ( superclass.respond_to?(:option_parser_commands) ? superclass.option_parser_commands.dup : [] ) @option_parser_commands end def find(name) - self.const_get(name.to_s.capitalize) - rescue + klass = name.to_s.capitalize + + # const_defined? is used before const_get since const_defined? will only + # check within our namespace, whereas const_get will check ancestor + # trees as well, resulting in unexpected behaviour. + if !self.const_defined?(klass) puts "Unable to find application '#{name.to_s}'." Kernel::exit(1) + end + + self.const_get(klass) end def [](name) find(name).new end # Sets or gets the run_mode name. Sets the run_mode name if a mode_name is # passed. Otherwise, gets the run_mode or a default run_mode # def run_mode( mode_name = nil) return @run_mode if @run_mode and not mode_name require 'puppet/util/run_mode' @run_mode = Puppet::Util::RunMode[ mode_name || :user ] end end attr_reader :options, :command_line # Every app responds to --version option("--version", "-V") do |arg| puts "#{Puppet.version}" exit end # Every app responds to --help option("--help", "-h") do |v| help end def should_parse_config? self.class.should_parse_config? end # override to execute code before running anything else def preinit end def initialize(command_line = nil) require 'puppet/util/command_line' @command_line = command_line || Puppet::Util::CommandLine.new @run_mode = self.class.run_mode @options = {} $puppet_application_mode = @run_mode $puppet_application_name = name if Puppet.respond_to? :settings # This is to reduce the amount of confusion in rspec # because it might have loaded defaults.rb before the globals were set # and thus have the wrong defaults for the current application Puppet.settings.set_value(:confdir, Puppet.run_mode.conf_dir, :mutable_defaults) Puppet.settings.set_value(:vardir, Puppet.run_mode.var_dir, :mutable_defaults) Puppet.settings.set_value(:name, Puppet.application_name.to_s, :mutable_defaults) Puppet.settings.set_value(:logdir, Puppet.run_mode.logopts, :mutable_defaults) Puppet.settings.set_value(:rundir, Puppet.run_mode.run_dir, :mutable_defaults) Puppet.settings.set_value(:run_mode, Puppet.run_mode.name.to_s, :mutable_defaults) end require 'puppet' end # This is the main application entry point def run exit_on_fail("initialize") { preinit } exit_on_fail("parse options") { parse_options } exit_on_fail("parse configuration file") { Puppet.settings.parse } if should_parse_config? exit_on_fail("prepare for execution") { setup } exit_on_fail("run") { run_command } end def main raise NotImplementedError, "No valid command or main" end def run_command main end def setup # Handle the logging settings if options[:debug] or options[:verbose] Puppet::Util::Log.newdestination(:console) if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end end Puppet::Util::Log.newdestination(:syslog) unless options[:setdest] end def parse_options # Create an option parser option_parser = OptionParser.new(self.class.banner) # Add all global options to it. Puppet.settings.optparse_addargs([]).each do |option| option_parser.on(*option) do |arg| handlearg(option[0], arg) end end # Add options that are local to this application, which were # created using the "option()" metaprogramming method. If there # are any conflicts, this application's options will be favored. self.class.option_parser_commands.each do |options, fname| option_parser.on(*options) do |value| # Call the method that "option()" created. self.send(fname, value) end end # scan command line. begin option_parser.parse!(self.command_line.args) rescue OptionParser::ParseError => detail $stderr.puts detail $stderr.puts "Try 'puppet #{command_line.subcommand_name} --help'" exit(1) end end def handlearg(opt, arg) # rewrite --[no-]option to --no-option if that's what was given if opt =~ /\[no-\]/ and !arg opt = opt.gsub(/\[no-\]/,'no-') end # otherwise remove the [no-] prefix to not confuse everybody opt = opt.gsub(/\[no-\]/, '') unless respond_to?(:handle_unknown) and send(:handle_unknown, opt, arg) # Puppet.settings.handlearg doesn't handle direct true/false :-) if arg.is_a?(FalseClass) arg = "false" elsif arg.is_a?(TrueClass) arg = "true" end Puppet.settings.handlearg(opt, arg) end end # this is used for testing def self.exit(code) exit(code) end def name self.class.to_s.sub(/.*::/,"").downcase.to_sym end def help if Puppet.features.usage? # RH:FIXME: My goodness, this is ugly. ::RDoc.const_set("PuppetSourceFile", name) #:stopdoc: # Issue #4161 def (::RDoc).caller docfile = `grep -l 'Puppet::Application\\[:#{::RDoc::PuppetSourceFile}\\]' #{DOCPATTERN}`.chomp super << "#{docfile}:0" end #:startdoc: ::RDoc::usage && exit else puts "No help available unless you have RDoc::usage installed" exit end rescue Errno::ENOENT puts "No help available for puppet #{name}" exit end private def exit_on_fail(message, code = 1) yield rescue RuntimeError, NotImplementedError => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Could not #{message}: #{detail}" exit(code) end end end diff --git a/lib/puppet/application/doc.rb b/lib/puppet/application/doc.rb index 1f6c63286..aaefd6e75 100644 --- a/lib/puppet/application/doc.rb +++ b/lib/puppet/application/doc.rb @@ -1,181 +1,178 @@ require 'puppet/application' class Puppet::Application::Doc < Puppet::Application should_not_parse_config run_mode :master attr_accessor :unknown_args, :manifest def preinit - {:references => [], :mode => :text, :format => :to_rest }.each do |name,value| + {:references => [], :mode => :text, :format => :to_markdown }.each do |name,value| options[name] = value end @unknown_args = [] @manifest = false end option("--all","-a") option("--outputdir OUTPUTDIR","-o") option("--verbose","-v") option("--debug","-d") option("--charset CHARSET") option("--format FORMAT", "-f") do |arg| method = "to_#{arg}" require 'puppet/util/reference' if Puppet::Util::Reference.method_defined?(method) options[:format] = method else raise "Invalid output format #{arg}" end end option("--mode MODE", "-m") do |arg| require 'puppet/util/reference' if Puppet::Util::Reference.modes.include?(arg) or arg.intern==:rdoc options[:mode] = arg.intern else raise "Invalid output mode #{arg}" end end option("--list", "-l") do |arg| require 'puppet/util/reference' puts Puppet::Util::Reference.references.collect { |r| Puppet::Util::Reference.reference(r).doc }.join("\n") exit(0) end option("--reference REFERENCE", "-r") do |arg| options[:references] << arg.intern end def handle_unknown( opt, arg ) @unknown_args << {:opt => opt, :arg => arg } true end def run_command return[:rdoc].include?(options[:mode]) ? send(options[:mode]) : other end def rdoc exit_code = 0 files = [] unless @manifest env = Puppet::Node::Environment.new files += env.modulepath files << File.dirname(env[:manifest]) end files += command_line.args Puppet.info "scanning: #{files.inspect}" Puppet.settings.setdefaults( "puppetdoc", "document_all" => [false, "Document all resources"] ) Puppet.settings[:document_all] = options[:all] || false begin require 'puppet/util/rdoc' if @manifest Puppet::Util::RDoc.manifestdoc(files) else options[:outputdir] = "doc" unless options[:outputdir] Puppet::Util::RDoc.rdoc(options[:outputdir], files, options[:charset]) end rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Could not generate documentation: #{detail}" exit_code = 1 end exit exit_code end def other text = "" with_contents = options[:references].length <= 1 exit_code = 0 require 'puppet/util/reference' options[:references].sort { |a,b| a.to_s <=> b.to_s }.each do |name| raise "Could not find reference #{name}" unless section = Puppet::Util::Reference.reference(name) begin # Add the per-section text, but with no ToC text += section.send(options[:format], with_contents) rescue => detail puts detail.backtrace $stderr.puts "Could not generate reference #{name}: #{detail}" exit_code = 1 next end end text += Puppet::Util::Reference.footer unless with_contents # We've only got one reference - # Replace the trac links, since they're invalid everywhere else - text.gsub!(/`\w+\s+([^`]+)`:trac:/) { |m| $1 } - if options[:mode] == :pdf Puppet::Util::Reference.pdf(text) else puts text end exit exit_code end def setup # sole manifest documentation if command_line.args.size > 0 options[:mode] = :rdoc @manifest = true end if options[:mode] == :rdoc setup_rdoc else setup_reference end end def setup_reference if options[:all] # Don't add dynamic references to the "all" list. require 'puppet/util/reference' options[:references] = Puppet::Util::Reference.references.reject do |ref| Puppet::Util::Reference.reference(ref).dynamic? end end options[:references] << :type if options[:references].empty? end def setup_rdoc(dummy_argument=:work_arround_for_ruby_GC_bug) # consume the unknown options # and feed them as settings if @unknown_args.size > 0 @unknown_args.each do |option| # force absolute path for modulepath when passed on commandline if option[:opt]=="--modulepath" or option[:opt] == "--manifestdir" option[:arg] = option[:arg].split(':').collect { |p| File.expand_path(p) }.join(':') end Puppet.settings.handlearg(option[:opt], option[:arg]) end end # Now parse the config Puppet.parse_config # Handle the logging settings. if options[:debug] or options[:verbose] if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end Puppet::Util::Log.newdestination(:console) end end end diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 400d59f15..764cbbe2b 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -1,797 +1,797 @@ # The majority of the system configuration parameters are set in this file. module Puppet setdefaults(:main, :confdir => [Puppet.run_mode.conf_dir, "The main Puppet configuration directory. The default for this parameter is calculated based on the user. If the process - is running as root or the user that `puppet master` 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 `~`."], + 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 => [Puppet.run_mode.var_dir, "Where Puppet stores dynamic and growing data. The default for this parameter is calculated specially, like `confdir`_."], :name => [Puppet.application_name.to_s, "The name of the application, if we are running as one. The default is essentially $0 without the path or `.rb`."], :run_mode => [Puppet.run_mode.name.to_s, "The effective 'run mode' of the application: master, agent, or user."] ) setdefaults(:main, :logdir => Puppet.run_mode.logopts) setdefaults(:main, :trace => [false, "Whether to print stack traces on some errors"], :autoflush => [false, "Whether log files should always flush to disk."], :syslogfacility => ["daemon", "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", :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 => Puppet.run_mode.run_dir, :mode => 01777, :desc => "Where Puppet PID files are kept." }, :genconfig => [false, "Whether to just print a configuration to stdout and exit. Only makes sense when used interactively. Takes into account arguments specified on the CLI."], :genmanifest => [false, "Whether to just print a manifest to stdout and exit. Only makes sense when used interactively. Takes into account arguments specified on the CLI."], :configprint => ["", "Print the value of a specific configuration parameter. If a parameter is provided for this, then the value is printed and puppet exits. Comma-separate multiple values. For a list of all values, specify 'all'. This feature is only available in Puppet versions higher than 0.18.4."], :color => ["ansi", "Whether to use colors when logging to the console. Valid values are `ansi` (equivalent to `true`), `html` (mostly used during testing with TextMate), and `false`, which produces no color."], :mkusers => [false, "Whether to create the necessary user and group that puppet agent will run as."], :manage_internal_file_permissions => [true, "Whether Puppet should manage the owner, group, and mode of files it uses internally" ], :onetime => {:default => false, :desc => "Run the configuration once, rather than as a long-running daemon. This is useful for interactively running puppetd.", :short => 'o' }, :path => {:default => "none", :desc => "The shell search path. Defaults to whatever is inherited from the parent process.", :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. :hook => proc do |value| ENV["PATH"] = "" if ENV["PATH"].nil? ENV["PATH"] = value unless value == "none" paths = ENV["PATH"].split(File::PATH_SEPARATOR) %w{/usr/sbin /sbin}.each do |path| ENV["PATH"] += File::PATH_SEPARATOR + path unless paths.include?(path) end value end }, :libdir => {: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", :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. :hook => proc do |value| $LOAD_PATH.delete(@oldlibdir) if defined?(@oldlibdir) and $LOAD_PATH.include?(@oldlibdir) @oldlibdir = value $LOAD_PATH << value end }, :ignoreimport => [false, "A parameter that can be used in commit hooks, since it enables you to parse-check a single file rather than requiring that all files exist."], :authconfig => [ "$confdir/namespaceauth.conf", "The configuration file that defines the rights to the different namespaces and methods. This can be used as a coarse-grained authorization system for both `puppet agent` and `puppet master`." ], :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." }, :diff_args => ["-u", "Which arguments to pass to the diff command when printing differences between files."], :diff => ["diff", "Which diff command to use when printing differences between files."], :show_diff => [false, "Whether to print a contextual diff when files are being replaced. The diff is printed on stdout, so this option is meaningless unless you are running Puppet interactively. This feature currently requires the `diff/lcs` Ruby library."], :daemonize => { :default => true, :desc => "Send the process into the background. This is the default.", :short => "D" }, :maximum_uid => [4294967290, "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."], :node_terminus => ["plain", "Where to find information about nodes."], :catalog_terminus => ["compiler", "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."], :facts_terminus => [Puppet.application_name.to_s == "master" ? 'yaml' : 'facter', "The node facts terminus."], :httplog => { :default => "$logdir/http.log", :owner => "root", :mode => 0640, :desc => "Where the puppet agent web server logs." }, :http_proxy_host => ["none", "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."], :http_proxy_port => [3128, "The HTTP proxy port to use for outgoing connections"], :filetimeout => [ 15, "The minimum time to wait (in seconds) 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." ], :queue_type => ["stomp", "Which type of queue to use for asynchronous processing."], :queue_type => ["stomp", "Which type of queue to use for asynchronous processing."], :queue_source => ["stomp://localhost:61613/", "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, :desc => "Whether to use a queueing system to provide asynchronous database integration. Requires that `puppetqd` be running and that 'PSON' support for ruby be installed.", :hook => proc do |value| if value # This reconfigures the terminii for Node, Facts, and Catalog Puppet.settings[:storeconfigs] = true # But then we modify the configuration Puppet::Resource::Catalog.cache_class = :queue else raise "Cannot disable asynchronous storeconfigs in a running process" end end }, :thin_storeconfigs => {:default => false, :desc => "Boolean; wether storeconfigs store in the database only the facts and exported resources. If true, then storeconfigs performance will be higher and still allow exported/collected resources, but other usage external to Puppet might not work", :hook => proc do |value| Puppet.settings[:storeconfigs] = true if value end }, :config_version => ["", "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."], :zlib => [true, "Boolean; whether to use the zlib library", ], :prerun_command => ["", "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 => ["", "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 => [false, "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."] ) hostname = Facter["hostname"].value domain = Facter["domain"].value if domain and domain != "" fqdn = [hostname, domain].join(".") else fqdn = hostname end Puppet.setdefaults( :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 => fqdn.downcase, :desc => "The name to use when handling certificates. Defaults to the fully qualified domain name.", :call_on_define => true, # Call our hook with the default value, so we're always downcased :hook => proc { |value| raise(ArgumentError, "Certificate names must be lower case; see #1168") unless value == value.downcase }}, :certdnsnames => ['', "The DNS names on the Server certificate as a colon-separated list. If it's anything other than an empty string, it will be used as an alias in the created certificate. By default, only the server gets an alias set up, and only for 'puppet'."], :certdir => { :default => "$ssldir/certs", :owner => "service", :desc => "The certificate directory." }, :ssldir => { :default => "$confdir/ssl", :mode => 0771, :owner => "service", :desc => "Where SSL certificates are kept." }, :publickeydir => { :default => "$ssldir/public_keys", :owner => "service", :desc => "The public key directory." }, :requestdir => { :default => "$ssldir/certificate_requests", :owner => "service", :desc => "Where host certificate requests are stored." }, :privatekeydir => { :default => "$ssldir/private_keys", :mode => 0750, :owner => "service", :desc => "The private key directory." }, :privatedir => { :default => "$ssldir/private", :mode => 0750, :owner => "service", :desc => "Where the client stores private certificate information." }, :passfile => { :default => "$privatedir/password", :mode => 0640, :owner => "service", :desc => "Where puppet agent stores the password for its private key. Generally unused." }, :hostcsr => { :default => "$ssldir/csr_$certname.pem", :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their certificate requests." }, :hostcert => { :default => "$certdir/$certname.pem", :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their certificates." }, :hostprivkey => { :default => "$privatekeydir/$certname.pem", :mode => 0600, :owner => "service", :desc => "Where individual hosts store and look for their private key." }, :hostpubkey => { :default => "$publickeydir/$certname.pem", :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their public key." }, :localcacert => { :default => "$certdir/ca.pem", :mode => 0644, :owner => "service", :desc => "Where each client stores the CA certificate." }, :hostcrl => { :default => "$ssldir/crl.pem", :mode => 0644, :owner => "service", :desc => "Where the host's certificate revocation list can be found. This is distinct from the certificate authority's CRL." }, :certificate_revocation => [true, "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."] ) setdefaults( :ca, :ca_name => ["Puppet CA: $certname", "The name to use the Certificate Authority certificate."], :cadir => { :default => "$ssldir/ca", :owner => "service", :group => "service", :mode => 0770, :desc => "The root directory for the certificate authority." }, :cacert => { :default => "$cadir/ca_crt.pem", :owner => "service", :group => "service", :mode => 0660, :desc => "The CA certificate." }, :cakey => { :default => "$cadir/ca_key.pem", :owner => "service", :group => "service", :mode => 0660, :desc => "The CA private key." }, :capub => { :default => "$cadir/ca_pub.pem", :owner => "service", :group => "service", :desc => "The CA public key." }, :cacrl => { :default => "$cadir/ca_crl.pem", :owner => "service", :group => "service", :mode => 0664, :desc => "The certificate revocation list (CRL) for the CA. Will be used if present but otherwise ignored.", :hook => proc do |value| if value == 'false' Puppet.warning "Setting the :cacrl to 'false' is deprecated; Puppet will just ignore the crl if yours is missing" end end }, :caprivatedir => { :default => "$cadir/private", :owner => "service", :group => "service", :mode => 0770, :desc => "Where the CA stores private certificate information." }, :csrdir => { :default => "$cadir/requests", :owner => "service", :group => "service", :desc => "Where the CA stores certificate requests" }, :signeddir => { :default => "$cadir/signed", :owner => "service", :group => "service", :mode => 0770, :desc => "Where the CA stores signed certificates." }, :capass => { :default => "$caprivatedir/ca.pass", :owner => "service", :group => "service", :mode => 0660, :desc => "Where the CA stores the password for the private key" }, :serial => { :default => "$cadir/serial", :owner => "service", :group => "service", :mode => 0644, :desc => "Where the serial number for certificates is stored." }, :autosign => { :default => "$confdir/autosign.conf", :mode => 0644, :desc => "Whether to enable autosign. Valid values are true (which autosigns any key request, and is a very bad idea), false (which never autosigns any key request), and the path to a file, which uses that configuration file to determine which keys to sign."}, :ca_days => ["", "How long a certificate should be valid. This parameter is deprecated, use ca_ttl instead"], :ca_ttl => ["5y", "The default TTL for new certificates; valid values must be an integer, optionally followed by one of the units 'y' (years of 365 days), 'd' (days), 'h' (hours), or 's' (seconds). The unit defaults to seconds. If this parameter is set, ca_days is ignored. Examples are '3600' (one hour) and '1825d', which is the same as '5y' (5 years) "], :ca_md => ["md5", "The type of hash used in certificates."], :req_bits => [2048, "The bit length of the certificates."], :keylength => [1024, "The bit length of keys."], :cert_inventory => { :default => "$cadir/inventory.txt", :mode => 0644, :owner => "service", :group => "service", :desc => "A Complete listing of all certificates" } ) # Define the config default. setdefaults( Puppet.settings[:name], :config => ["$confdir/puppet.conf", "The configuration file for #{Puppet[:name]}."], :pidfile => ["$rundir/$name.pid", "The pid file"], :bindaddress => ["", "The address a listening server should bind to. Mongrel servers default to 127.0.0.1 and WEBrick defaults to 0.0.0.0."], :servertype => {:default => "webrick", :desc => "The type of server to use. Currently supported options are webrick and mongrel. If you use mongrel, you will need a proxy in front of the process or processes, since Mongrel cannot speak SSL.", :call_on_define => true, # Call our hook with the default value, so we always get the correct bind address set. :hook => proc { |value| value == "webrick" ? Puppet.settings[:bindaddress] = "0.0.0.0" : Puppet.settings[:bindaddress] = "127.0.0.1" if Puppet.settings[:bindaddress] == "" } } ) setdefaults(:master, :user => ["puppet", "The user puppet master should run as."], :group => ["puppet", "The group puppet master should run as."], :manifestdir => ["$confdir/manifests", "Where puppet master looks for its manifests."], :manifest => ["$manifestdir/site.pp", "The entry-point manifest for puppet master."], :code => ["", "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", :owner => "service", :group => "service", :mode => 0660, :desc => "Where puppet master logs. This is generally not used, since syslog is the default log destination." }, :masterhttplog => { :default => "$logdir/masterhttp.log", :owner => "service", :group => "service", :mode => 0660, :create => true, :desc => "Where the puppet master web server logs." }, :masterport => [8140, "Which port puppet master listens on."], :parseonly => [false, "Just check the syntax of the manifests."], :node_name => ["cert", "How the puppetmaster 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", :mode => 0750, :owner => "service", :group => "service", :desc => "Where FileBucket files are stored." }, :rest_authconfig => [ "$confdir/auth.conf", "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 => [true, "Wether the master should function as a certificate authority."], :modulepath => {:default => "$confdir/modules:/usr/share/puppet/modules", :desc => "The search path for modules as a colon-separated list of directories.", :type => :setting }, # We don't want this to be considered a file, since it's multiple files. :ssl_client_header => ["HTTP_X_CLIENT_DN", "The header containing an authenticated client's SSL DN. Only used with Mongrel. This header must be set by the proxy to the authenticated client's SSL DN (e.g., `/CN=puppet.puppetlabs.com`). See http://projects.puppetlabs.com/projects/puppet/wiki/Using_Mongrel for more information."], :ssl_client_verify_header => ["HTTP_X_CLIENT_VERIFY", "The header containing the status message of the client verification. Only used with Mongrel. This header must be set by the proxy to 'SUCCESS' if the client successfully authenticated, and anything else otherwise. See http://projects.puppetlabs.com/projects/puppet/wiki/Using_Mongrel for more information."], # 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", :owner => "service", :group => "service", :mode => "750", :desc => "The directory in which YAML data is stored, usually in a subdirectory."}, :server_datadir => {:default => "$vardir/server_data", :owner => "service", :group => "service", :mode => "750", :desc => "The directory in which serialized data is stored, usually in a subdirectory."}, :reports => ["store", "The list of reports to generate. All reports are looked for in `puppet/reports/name.rb`, and multiple report names should be comma-separated (whitespace is okay)." ], :reportdir => {:default => "$vardir/reports", :mode => 0750, :owner => "service", :group => "service", :desc => "The directory in which to store reports received from the client. Each client gets a separate subdirectory."}, :reporturl => ["http://localhost:3000/reports", "The URL used by the http reports processor to send reports"], :fileserverconfig => ["$confdir/fileserver.conf", "Where the fileserver configuration is stored."], :strict_hostname_checking => [false, "Whether to only search for the complete hostname as it is in the certificate when searching for node information in the catalogs."] ) setdefaults(:metrics, :rrddir => {:default => "$vardir/rrd", :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 => ["$runinterval", "How often RRD should expect data. This should match how often the hosts report back to the server."] ) setdefaults(:agent, :localconfig => { :default => "$statedir/localconfig", :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", :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", :mode => "750", :desc => "The directory in which client-side YAML data is stored."}, :client_datadir => {:default => "$vardir/client_data", :mode => "750", :desc => "The directory in which serialized data is stored on the client."}, :classfile => { :default => "$statedir/classes.txt", :owner => "root", :mode => 0644, :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."}, :puppetdlog => { :default => "$logdir/puppetd.log", :owner => "root", :mode => 0640, :desc => "The log file for puppet agent. This is generally not used." }, :server => ["puppet", "The server to which server puppet agent should connect"], :ignoreschedules => [false, "Boolean; whether puppet agent should ignore schedules. This is useful for initial puppet agent runs."], :puppetport => [8139, "Which port puppet agent listens on."], :noop => [false, "Whether puppet agent should be run in noop mode."], :runinterval => [1800, # 30 minutes "How often puppet agent applies the client configuration; in seconds."], :listen => [false, "Whether puppet agent should listen for connections. If this is true, then by default only the `runner` server is started, which allows remote authorized and authenticated nodes to connect and trigger `puppet agent` runs."], :ca_server => ["$server", "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 => ["$masterport", "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.warning "Setting 'catalog_format' is deprecated; use 'preferred_serialization_format' instead." Puppet.settings[:preferred_serialization_format] = value end } }, :preferred_serialization_format => ["pson", "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."], :puppetdlockfile => [ "$statedir/puppetdlock", "A lock file to temporarily stop puppet agent from doing anything."], :usecacheonfailure => [true, "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 => [false, "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."], :ignorecache => [false, "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." ], :downcasefacts => [false, "Whether facts should be made all lowercase when sent to the server."], :dynamicfacts => ["memorysize,memoryfree,swapsize,swapfree", "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."], :splaylimit => ["$runinterval", "The maximum time to delay before runs. Defaults to being the same as the run interval."], :splay => [false, "Whether to sleep for a pseudo-random (but consistent) amount of time before a run."], :clientbucketdir => { :default => "$vardir/clientbucket", :mode => 0750, :desc => "Where FileBucket files are stored locally." }, :configtimeout => [120, "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." ], :reportserver => { :default => "$server", :call_on_define => false, :desc => "(Deprecated for 'report_server') The server to which to send transaction reports.", :hook => proc do |value| Puppet.settings[:report_server] = value if value end }, :report_server => ["$server", "The server to which to send transaction reports." ], :report_port => ["$masterport", "The port to communicate with the report_server." ], :report => [false, "Whether to send reports after every transaction." ], :graph => [false, "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 => ["$statedir/graphs", "Where to store dot-outputted graphs."], :http_compression => [false, "Allow http compression in REST communication with the master. This setting might improve performance for agent -> master communications over slow WANs. Your puppetmaster needs to support compression (usually by activating some settings in a reverse-proxy in front of the puppetmaster, 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."] ) setdefaults(:inspect, :archive_files => [false, "During an inspect run, whether to archive files whose contents are audited to a file bucket."], :archive_file_server => ["$server", "During an inspect run, the file bucket server to archive files to if archive_files is set."] ) # Plugin information. setdefaults( :main, :plugindest => ["$libdir", "Where Puppet should store plugins that it pulls down from the central server."], :pluginsource => ["puppet://$server/plugins", "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."], :pluginsync => [false, "Whether plugins should be synced with the central server."], :pluginsignore => [".svn CVS .git", "What files to ignore when pulling down plugins."] ) # Central fact information. setdefaults( :main, :factpath => {:default => "$vardir/lib/facter:$vardir/facts", :desc => "Where Puppet should look for facts. Multiple directories should be colon-separated, like normal PATH variables.", :call_on_define => true, # Call our hook with the default value, so we always get the value added to facter. :type => :setting, # Don't consider it a file, because it could be multiple colon-separated files :hook => proc { |value| Facter.search(value) if Facter.respond_to?(:search) }}, :factdest => ["$vardir/facts/", "Where Puppet should store facts that it pulls down from the central server."], :factsource => ["puppet://$server/facts/", "From where to retrieve facts. The standard Puppet `file` type is used for retrieval, so anything that is a valid file source can be used here."], :factsync => [false, "Whether facts should be synced with the central server."], :factsignore => [".svn CVS", "What files to ignore when pulling down facts."] ) setdefaults( :tagmail, :tagmap => ["$confdir/tagmail.conf", "The mapping between reporting tags and email addresses."], :sendmail => [which('sendmail') || '', "Where to find the sendmail binary with which to send email."], :reportfrom => ["report@" + [Facter["hostname"].value, Facter["domain"].value].join("."), "The 'from' email address for the reports."], :smtpserver => ["none", "The server through which to send email reports."] ) setdefaults( :rails, :dblocation => { :default => "$statedir/clientconfigs.sqlite3", :mode => 0660, :owner => "service", :group => "service", :desc => "The database cache for client configurations. Used for querying within the language." }, :dbadapter => [ "sqlite3", "The type of database to use." ], :dbmigrate => [ false, "Whether to automatically migrate the database." ], :dbname => [ "puppet", "The name of the database to use." ], :dbserver => [ "localhost", "The database server for caching. Only used when networked databases are used."], :dbport => [ "", "The database password for caching. Only used when networked databases are used."], :dbuser => [ "puppet", "The database user for caching. Only used when networked databases are used."], :dbpassword => [ "puppet", "The database password for caching. Only used when networked databases are used."], :dbconnections => [ '', "The number of database connections for networked databases. Will be ignored unless the value is a positive integer."], :dbsocket => [ "", "The database socket location. Only used when networked databases are used. Will be ignored if the value is an empty string."], :railslog => {:default => "$logdir/rails.log", :mode => 0600, :owner => "service", :group => "service", :desc => "Where Rails-specific logs are sent" }, :rails_loglevel => ["info", "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`."] ) setdefaults( :couchdb, :couchdb_url => ["http://127.0.0.1:5984/puppet", "The url where the puppet couchdb database will be created"] ) setdefaults( :transaction, :tags => ["", "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 => [false, "Whether each resource should log when it is being evaluated. This allows you to interactively see exactly what is being done."], :summarize => [false, "Whether to print a transaction summary." ] ) setdefaults( :main, :external_nodes => ["none", "An external command that can produce node information. The output must be a YAML dump of a hash, and that hash must have one or both of `classes` and `parameters`, where `classes` is an array and `parameters` is a hash. For unknown nodes, the commands 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."]) setdefaults( :ldap, :ldapnodes => [false, "Whether to search for node configurations in LDAP. See http://projects.puppetlabs.com/projects/puppet/wiki/LDAP_Nodes for more information."], :ldapssl => [false, "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 => [false, "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 => ["ldap", "The LDAP server. Only used if `ldapnodes` is enabled."], :ldapport => [389, "The LDAP port. Only used if `ldapnodes` is enabled."], :ldapstring => ["(&(objectclass=puppetClient)(cn=%s))", "The search string used to find an LDAP node."], :ldapclassattrs => ["puppetclass", "The LDAP attributes to use to define Puppet classes. Values should be comma-separated."], :ldapstackedattrs => ["puppetvar", "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 => ["all", "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 => ["parentnode", "The attribute to use to define the parent node."], :ldapuser => ["", "The user to use to connect to LDAP. Must be specified as a full DN."], :ldappassword => ["", "The password to use to connect to LDAP."], :ldapbase => ["", "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."] ) setdefaults(:master, :storeconfigs => {:default => false, :desc => "Whether to store each client's configuration. This requires ActiveRecord from Ruby on Rails.", :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. :hook => proc do |value| require 'puppet/node' require 'puppet/node/facts' if value require 'puppet/rails' raise "StoreConfigs not supported without ActiveRecord 2.1 or higher" unless Puppet.features.rails? Puppet::Resource::Catalog.cache_class = :active_record unless Puppet.settings[:async_storeconfigs] Puppet::Node::Facts.cache_class = :active_record Puppet::Node.cache_class = :active_record end end } ) # This doesn't actually work right now. setdefaults( :parser, :lexical => [false, "Whether to use lexical scoping (vs. dynamic)."], :templatedir => ["$vardir/templates", "Where Puppet looks for template files. Can be a list of colon-seperated directories." ] ) end diff --git a/lib/puppet/indirector/resource/ral.rb b/lib/puppet/indirector/resource/ral.rb index 1c2ab14ae..bc41d14ae 100644 --- a/lib/puppet/indirector/resource/ral.rb +++ b/lib/puppet/indirector/resource/ral.rb @@ -1,48 +1,53 @@ class Puppet::Resource::Ral < Puppet::Indirector::Code def find( request ) # find by name res = type(request).instances.find { |o| o.name == resource_name(request) } res ||= type(request).new(:name => resource_name(request), :audit => type(request).properties.collect { |s| s.name }) res.to_resource end def search( request ) conditions = request.options.dup conditions[:name] = resource_name(request) if resource_name(request) type(request).instances.map do |res| res.to_resource end.find_all do |res| conditions.all? {|property, value| res.to_resource[property].to_s == value.to_s} end.sort do |a,b| a.title <=> b.title end end def save( request ) # In RAL-land, to "save" means to actually try to change machine state res = request.instance ral_res = res.to_ral catalog = Puppet::Resource::Catalog.new catalog.add_resource ral_res catalog.apply ral_res.to_resource end private + # {type,resource}_name: the resource name may contain slashes: + # File["/etc/hosts"]. To handle, assume the type name does + # _not_ have any slashes in it, and split only on the first. + def type_name( request ) - request.key.split('/')[0] + request.key.split('/', 2)[0] end def resource_name( request ) - request.key.split('/')[1] + name = request.key.split('/', 2)[1] + name unless name == "" end def type( request ) Puppet::Type.type(type_name(request)) or raise Puppet::Error, "Could not find type #{type}" end end diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb index c60e1d4fb..fdabd05c9 100644 --- a/lib/puppet/parser/compiler.rb +++ b/lib/puppet/parser/compiler.rb @@ -1,474 +1,489 @@ # Created by Luke A. Kanies on 2007-08-13. # Copyright (c) 2007. All rights reserved. require 'puppet/node' require 'puppet/resource/catalog' require 'puppet/util/errors' require 'puppet/resource/type_collection_helper' # Maintain a graph of scopes, along with a bunch of data # about the individual catalog we're compiling. class Puppet::Parser::Compiler include Puppet::Util include Puppet::Util::Errors include Puppet::Resource::TypeCollectionHelper def self.compile(node) new(node).compile.to_resource rescue => detail puts detail.backtrace if Puppet[:trace] raise Puppet::Error, "#{detail} on node #{node.name}" ensure # We get these from the environment and only cache them in a thread # variable for the duration of the compilation. Thread.current[:known_resource_types] = nil Thread.current[:env_module_directories] = nil end attr_reader :node, :facts, :collections, :catalog, :node_scope, :resources, :relationships # Add a collection to the global list. def add_collection(coll) @collections << coll end def add_relationship(dep) @relationships << dep end # Store a resource override. def add_override(override) # If possible, merge the override in immediately. if resource = @catalog.resource(override.ref) resource.merge(override) else # Otherwise, store the override for later; these # get evaluated in Resource#finish. @resource_overrides[override.ref] << override end end # Store a resource in our resource table. def add_resource(scope, resource) @resources << resource # Note that this will fail if the resource is not unique. @catalog.add_resource(resource) # Add our container edge. If we're a class, then we get treated specially - we can # control the stage that the class is applied in. Otherwise, we just # get added to our parent container. return if resource.type.to_s.downcase == "stage" if resource.type.to_s.downcase != "class" raise ArgumentError, "Only classes can set 'stage'; normal resources like #{resource} cannot change run stage" if resource[:stage] return @catalog.add_edge(scope.resource, resource) end unless stage = @catalog.resource(:stage, resource[:stage] || (scope && scope.resource && scope.resource[:stage]) || :main) raise ArgumentError, "Could not find stage #{resource[:stage] || :main} specified by #{resource}" end resource[:stage] ||= stage.title unless stage.title == :main @catalog.add_edge(stage, resource) end # Do we use nodes found in the code, vs. the external node sources? def ast_nodes? known_resource_types.nodes? end # Store the fact that we've evaluated a class def add_class(name) @catalog.add_class(name) unless name == "" end # Return a list of all of the defined classes. def classlist @catalog.classes end # Compiler our catalog. This mostly revolves around finding and evaluating classes. # This is the main entry into our catalog. def compile # Set the client's parameters into the top scope. set_node_parameters create_settings_scope evaluate_main evaluate_ast_node evaluate_node_classes evaluate_generators finish fail_on_unevaluated @catalog end # LAK:FIXME There are no tests for this. def delete_collection(coll) @collections.delete(coll) if @collections.include?(coll) end # Return the node's environment. def environment unless defined?(@environment) @environment = (node.environment and node.environment != "") ? node.environment : nil end Puppet::Node::Environment.current = @environment @environment end # Evaluate all of the classes specified by the node. def evaluate_node_classes evaluate_classes(@node.classes, topscope) end # Evaluate each specified class in turn. If there are any classes we can't # find, just tag the catalog and move on. This method really just # creates resource objects that point back to the classes, and then the # resources are themselves evaluated later in the process. def evaluate_classes(classes, scope, lazy_evaluate = true) raise Puppet::DevError, "No source for scope passed to evaluate_classes" unless scope.source found = [] + param_classes = nil + # if we are a param class, save the classes hash + # and transform classes to be the keys + if classes.class == Hash + param_classes = classes + classes = classes.keys + end classes.each do |name| # If we can find the class, then make a resource that will evaluate it. if klass = scope.find_hostclass(name) - found << name and next if scope.class_scope(klass) - resource = klass.ensure_in_catalog(scope) + if param_classes + resource = klass.ensure_in_catalog(scope, param_classes[name] || {}) + else + found << name and next if scope.class_scope(klass) + resource = klass.ensure_in_catalog(scope) + end # If they've disabled lazy evaluation (which the :include function does), # then evaluate our resource immediately. resource.evaluate unless lazy_evaluate found << name else Puppet.info "Could not find class #{name} for #{node.name}" @catalog.tag(name) end end found end def evaluate_relationships @relationships.each { |rel| rel.evaluate(catalog) } end # Return a resource by either its ref or its type and title. def findresource(*args) @catalog.resource(*args) end def initialize(node, options = {}) @node = node options.each do |param, value| begin send(param.to_s + "=", value) rescue NoMethodError raise ArgumentError, "Compiler objects do not accept #{param}" end end initvars end # Create a new scope, with either a specified parent scope or # using the top scope. def newscope(parent, options = {}) parent ||= topscope options[:compiler] = self scope = Puppet::Parser::Scope.new(options) scope.parent = parent scope end # Return any overrides for the given resource. def resource_overrides(resource) @resource_overrides[resource.ref] end # The top scope is usually the top-level scope, but if we're using AST nodes, # then it is instead the node's scope. def topscope node_scope || @topscope end private # If ast nodes are enabled, then see if we can find and evaluate one. def evaluate_ast_node return unless ast_nodes? # Now see if we can find the node. astnode = nil @node.names.each do |name| break if astnode = known_resource_types.node(name.to_s.downcase) end unless (astnode ||= known_resource_types.node("default")) raise Puppet::ParseError, "Could not find default node or by name with '#{node.names.join(", ")}'" end # Create a resource to model this node, and then add it to the list # of resources. resource = astnode.ensure_in_catalog(topscope) resource.evaluate # Now set the node scope appropriately, so that :topscope can # behave differently. @node_scope = topscope.class_scope(astnode) end # Evaluate our collections and return true if anything returned an object. # The 'true' is used to continue a loop, so it's important. def evaluate_collections return false if @collections.empty? found_something = false exceptwrap do # We have to iterate over a dup of the array because # collections can delete themselves from the list, which # changes its length and causes some collections to get missed. @collections.dup.each do |collection| found_something = true if collection.evaluate end end found_something end # Make sure all of our resources have been evaluated into native resources. # We return true if any resources have, so that we know to continue the # evaluate_generators loop. def evaluate_definitions exceptwrap do !unevaluated_resources.each { |resource| resource.evaluate }.empty? end end # Iterate over collections and resources until we're sure that the whole # compile is evaluated. This is necessary because both collections # and defined resources can generate new resources, which themselves could # be defined resources. def evaluate_generators count = 0 loop do done = true # Call collections first, then definitions. done = false if evaluate_collections done = false if evaluate_definitions break if done count += 1 if count > 1000 raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host catalog" end end end # Find and evaluate our main object, if possible. def evaluate_main @main = known_resource_types.find_hostclass([""], "") || known_resource_types.add(Puppet::Resource::Type.new(:hostclass, "")) @topscope.source = @main @main_resource = Puppet::Parser::Resource.new("class", :main, :scope => @topscope, :source => @main) @topscope.resource = @main_resource add_resource(@topscope, @main_resource) @main_resource.evaluate end # Make sure the entire catalog is evaluated. def fail_on_unevaluated fail_on_unevaluated_overrides fail_on_unevaluated_resource_collections end # If there are any resource overrides remaining, then we could # not find the resource they were supposed to override, so we # want to throw an exception. def fail_on_unevaluated_overrides remaining = [] @resource_overrides.each do |name, overrides| remaining += overrides end unless remaining.empty? fail Puppet::ParseError, "Could not find resource(s) %s for overriding" % remaining.collect { |o| o.ref }.join(", ") end end # Make sure we don't have any remaining collections that specifically # look for resources, because we want to consider those to be # parse errors. def fail_on_unevaluated_resource_collections remaining = [] @collections.each do |coll| # We're only interested in the 'resource' collections, # which result from direct calls of 'realize'. Anything # else is allowed not to return resources. # Collect all of them, so we have a useful error. if r = coll.resources if r.is_a?(Array) remaining += r else remaining << r end end end raise Puppet::ParseError, "Failed to realize virtual resources #{remaining.join(', ')}" unless remaining.empty? end # Make sure all of our resources and such have done any last work # necessary. def finish evaluate_relationships resources.each do |resource| # Add in any resource overrides. if overrides = resource_overrides(resource) overrides.each do |over| resource.merge(over) end # Remove the overrides, so that the configuration knows there # are none left. overrides.clear end resource.finish if resource.respond_to?(:finish) end add_resource_metaparams end def add_resource_metaparams unless main = catalog.resource(:class, :main) raise "Couldn't find main" end names = [] Puppet::Type.eachmetaparam do |name| next if Puppet::Parser::Resource.relationship_parameter?(name) names << name end data = {} catalog.walk(main, :out) do |source, target| if source_data = data[source] || metaparams_as_data(source, names) # only store anything in the data hash if we've actually got # data data[source] ||= source_data source_data.each do |param, value| target[param] = value if target[param].nil? end data[target] = source_data.merge(metaparams_as_data(target, names)) end target.tag(*(source.tags)) end end def metaparams_as_data(resource, params) data = nil params.each do |param| unless resource[param].nil? # Because we could be creating a hash for every resource, # and we actually probably don't often have any data here at all, # we're optimizing a bit by only creating a hash if there's # any data to put in it. data ||= {} data[param] = resource[param] end end data end # Set up all of our internal variables. def initvars # The list of objects that will available for export. @exported_resources = {} # The list of overrides. This is used to cache overrides on objects # that don't exist yet. We store an array of each override. @resource_overrides = Hash.new do |overs, ref| overs[ref] = [] end # The list of collections that have been created. This is a global list, # but they each refer back to the scope that created them. @collections = [] # The list of relationships to evaluate. @relationships = [] # For maintaining the relationship between scopes and their resources. @catalog = Puppet::Resource::Catalog.new(@node.name) @catalog.version = known_resource_types.version # Create our initial scope and a resource that will evaluate main. @topscope = Puppet::Parser::Scope.new(:compiler => self) @main_stage_resource = Puppet::Parser::Resource.new("stage", :main, :scope => @topscope) @catalog.add_resource(@main_stage_resource) # local resource array to maintain resource ordering @resources = [] # Make sure any external node classes are in our class list - @catalog.add_class(*@node.classes) + if @node.classes.class == Hash + @catalog.add_class(*@node.classes.keys) + else + @catalog.add_class(*@node.classes) + end end # Set the node's parameters into the top-scope as variables. def set_node_parameters node.parameters.each do |param, value| @topscope.setvar(param, value) end # These might be nil. catalog.client_version = node.parameters["clientversion"] catalog.server_version = node.parameters["serverversion"] end def create_settings_scope unless settings_type = environment.known_resource_types.hostclass("settings") settings_type = Puppet::Resource::Type.new :hostclass, "settings" environment.known_resource_types.add(settings_type) end settings_resource = Puppet::Parser::Resource.new("class", "settings", :scope => @topscope) settings_type.evaluate_code(settings_resource) @catalog.add_resource(settings_resource) scope = @topscope.class_scope(settings_type) Puppet.settings.each do |name, setting| next if name.to_s == "name" scope.setvar name.to_s, environment[name] end end # Return an array of all of the unevaluated resources. These will be definitions, # which need to get evaluated into native resources. def unevaluated_resources # The order of these is significant for speed due to short-circuting resources.reject { |resource| resource.evaluated? or resource.virtual? or resource.builtin_type? } end end diff --git a/lib/puppet/parser/functions/defined.rb b/lib/puppet/parser/functions/defined.rb index 90632af2f..2aeaa9ba0 100644 --- a/lib/puppet/parser/functions/defined.rb +++ b/lib/puppet/parser/functions/defined.rb @@ -1,27 +1,49 @@ # Test whether a given class or definition is defined -Puppet::Parser::Functions::newfunction(:defined, :type => :rvalue, :doc => "Determine whether a given - type is defined, either as a native type or a defined type, or whether a class is defined. - This is useful for checking whether a class is defined and only including it if it is. - This function can also test whether a resource has been defined, using resource references - (e.g., `if defined(File['/tmp/myfile']) { ... }`). This function is unfortunately - dependent on the parse order of the configuration when testing whether a resource is defined.") do |vals| +Puppet::Parser::Functions::newfunction(:defined, :type => :rvalue, :doc => "Determine whether + a given class or resource type is defined. This function can also determine whether a + specific resource has been declared. Returns true or false. Accepts class names, + type names, and resource references. + + The `defined` function checks both native and defined types, including types + provided as plugins via modules. Types and classes are both checked using their names: + + defined(\"file\") + defined(\"customtype\") + defined(\"foo\") + defined(\"foo::bar\") + + Resource declarations are checked using resource references, e.g. + `defined( File['/tmp/myfile'] )`. Checking whether a given resource + has been declared is, unfortunately, dependent on the parse order of + the configuration, and the following code will not work: + + if defined(File['/tmp/foo']) { + notify(\"This configuration includes the /tmp/foo file.\") + } + file {\"/tmp/foo\": + ensure => present, + } + + However, this order requirement refers to parse order only, and ordering of + resources in the configuration graph (e.g. with `before` or `require`) does not + affect the behavior of `defined`.") do |vals| result = false vals = [vals] unless vals.is_a?(Array) vals.each do |val| case val when String if Puppet::Type.type(val) or find_definition(val) or find_hostclass(val) result = true break end when Puppet::Resource if findresource(val.to_s) result = true break end else raise ArgumentError, "Invalid argument of type '#{val.class}' to 'defined'" end end result end diff --git a/lib/puppet/parser/functions/fqdn_rand.rb b/lib/puppet/parser/functions/fqdn_rand.rb index 3e7018ac4..52946f2c1 100644 --- a/lib/puppet/parser/functions/fqdn_rand.rb +++ b/lib/puppet/parser/functions/fqdn_rand.rb @@ -1,9 +1,12 @@ Puppet::Parser::Functions::newfunction(:fqdn_rand, :type => :rvalue, :doc => - "Generates random numbers based on the node's fqdn. The first argument - sets the range. Additional (optional) arguments may be used to further - distinguish the seed.") do |args| + "Generates random numbers based on the node's fqdn. Generated random values + will be a range from 0 up to and excluding n, where n is the first parameter. + The second argument specifies a number to add to the seed and is optional, for example: + + $random_number = fqdn_rand(30) + $random_number_seed = fqdn_rand(30,30)") do |args| require 'md5' max = args.shift srand MD5.new([lookupvar('fqdn'),args].join(':')).to_s.hex rand(max).to_s end diff --git a/lib/puppet/property.rb b/lib/puppet/property.rb index 84e1a0360..12f496a6e 100644 --- a/lib/puppet/property.rb +++ b/lib/puppet/property.rb @@ -1,324 +1,339 @@ # The virtual base class for properties, which are the self-contained building # blocks for actually doing work on the system. require 'puppet' require 'puppet/parameter' class Puppet::Property < Puppet::Parameter require 'puppet/property/ensure' # Because 'should' uses an array, we have a special method for handling # it. We also want to keep copies of the original values, so that # they can be retrieved and compared later when merging. attr_reader :shouldorig attr_writer :noop class << self attr_accessor :unmanaged attr_reader :name # Return array matching info, defaulting to just matching # the first value. def array_matching @array_matching ||= :first end # Set whether properties should match all values or just the first one. 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 # Look up a value's name, so we can find options and such. def self.value_name(name) if value = value_collection.match?(name) value.name end end # Retrieve an option set when a value was defined. def self.value_option(name, option) if value = value_collection.value(name) value.send(option) end end # Define a new valid value for a property. You must provide the value itself, # usually as a symbol, or a regex to match the value. # # The first argument to the method is either the value itself or a regex. # The second argument is an option hash; valid options are: # * :method: The name of the method to define. Defaults to 'set_'. # * :required_features: A list of features this value requires. # * :event: The event that should be returned when this value is set. # * :call: When to call any associated block. The default value # is `instead`, which means to call the value instead of calling the # provider. You can also specify `before` or `after`, which will # call both the block and the provider, according to the order you specify # (the `first` refers to when the block is called, not the provider). 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 # Call the provider method. def call_provider(value) provider.send(self.class.name.to_s + "=", value) rescue NoMethodError self.fail "The #{provider.class.name} provider can not handle attribute #{self.class.name}" end # Call the dynamically-created method associated with our value, if # there is one. def call_valuemethod(name, value) if method = self.class.value_option(name, :method) and self.respond_to?(method) begin event = self.send(method) rescue Puppet::Error raise rescue => detail puts detail.backtrace if Puppet[:trace] error = Puppet::Error.new("Could not set '#{value} on #{self.class.name}: #{detail}", @resource.line, @resource.file) error.set_backtrace detail.backtrace 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 # How should a property change be printed as a string? def change_to_s(current_value, newvalue) begin if current_value == :absent return "defined '#{name}' as '#{should_to_s(newvalue)}'" elsif newvalue == :absent or newvalue == [:absent] return "undefined '#{name}' from '#{is_to_s(current_value)}'" else return "#{name} changed '#{is_to_s(current_value)}' to '#{should_to_s(newvalue)}'" end rescue Puppet::Error, Puppet::DevError raise rescue => detail puts detail.backtrace if Puppet[:trace] raise Puppet::DevError, "Could not convert change '#{name}' to string: #{detail}" end end # Figure out which event to return. 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 # Return a modified form of the resource event. def event resource.event :name => event_name, :desired_value => should, :property => self, :source_description => path end attr_reader :shadow # initialize our property def initialize(hash = {}) super if ! self.metaparam? and klass = Puppet::Type.metaparamclass(self.class.name) setup_shadow(klass) end end # Determine whether the property is in-sync or not. If @should is # not defined or is set to a non-true value, then we do not have # a valid value for it and thus consider the property to be in-sync # since we cannot fix it. Otherwise, we expect our should value # to be an array, and if @is matches any of those values, then # we consider it to be in-sync. - def insync?(is) + # + # Don't 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 + + def self.method_added(sym) + raise "Puppet::Property#safe_insync? shouldn't be overridden; please override insync? instead" if sym == :safe_insync? + end + + # This method should be overridden by derived classes if necessary + # to provide extra logic to determine whether the property is in + # sync. + 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 return (is == @should or is == @should.collect { |v| v.to_s }) if match_all? @should.each { |val| return true if is == val or is == val.to_s } # otherwise, return false false end # because the @should and @is vars might be in weird formats, # we need to set up a mechanism for pretty printing of the values # default to just the values, but this way individual properties can # override these methods def is_to_s(currentvalue) currentvalue end # Send a log message. def log(msg) Puppet::Util::Log.create( :level => resource[:loglevel], :message => msg, :source => self ) end # Should we match all values, or just the first? def match_all? self.class.array_matching == :all end # Execute our shadow's munge code, too, if we have one. def munge(value) self.shadow.munge(value) if self.shadow super end # each property class must define the name method, and property instances # do not change that name # this implicitly means that a given object can only have one property # instance of a given property class def name self.class.name end # for testing whether we should actually do anything 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 # By default, call the method associated with the property name on our # provider. In other words, if the property name is 'gid', we'll call # 'provider.gid' to retrieve the current value. def retrieve provider.send(self.class.name) end # Set our value, using the provider, an associated block, or both. 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 # If there's a shadowing metaparam, instantiate it now. # This allows us to create a property or parameter with the # same name as a metaparameter, and the metaparam will only be # stored as a shadow. def setup_shadow(klass) @shadow = klass.new(:resource => self.resource) end # Only return the first value 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 # Set the should value. 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 def should_to_s(newvalue) [newvalue].flatten.join(" ") end def sync devfail "Got a nil value for should" unless should set(should) end # Verify that the passed value is valid. # If the developer uses a 'validate' hook, this method will get overridden. def unsafe_validate(value) super validate_features_per_value(value) end # Make sure that we've got all of the required features for a given value. 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 # Just return any should value we might have. def value self.should end # Match the Parameter interface, but we really just use 'should' internally. # Note that the should= method does all of the validation and such. def value=(value) self.should = value end end diff --git a/lib/puppet/property/keyvalue.rb b/lib/puppet/property/keyvalue.rb index 0181708f9..57d0ea2d9 100644 --- a/lib/puppet/property/keyvalue.rb +++ b/lib/puppet/property/keyvalue.rb @@ -1,88 +1,86 @@ #This subclass of property manages string key value pairs. #In order to use this property: # - the @should value must be an array of keyvalue pairs separated by the 'separator' # - the retrieve method should return a hash with the keys as symbols # IMPORTANT NOTE: In order for this property to work there must also be a 'membership' parameter # The class that inherits from property should override that method with the symbol for the membership require 'puppet/property' module Puppet class Property class KeyValue < Property def hash_to_key_value_s(hash) hash.select { |k,v| true }.map { |pair| pair.join(separator) }.join(delimiter) end def should_to_s(should_value) hash_to_key_value_s(should_value) end def is_to_s(current_value) hash_to_key_value_s(current_value) end def membership :key_value_membership end def inclusive? @resource[membership] == :inclusive end def hashify(key_value_array) #turns string array into a hash key_value_array.inject({}) do |hash, key_value| tmp = key_value.split(separator) hash[tmp[0].intern] = tmp[1] hash end end def process_current_hash(current) return {} if current == :absent #inclusive means we are managing everything so if it isn't in should, its gone current.each_key { |key| current[key] = nil } if inclusive? current end def should return nil unless @should members = hashify(@should) current = process_current_hash(retrieve) #shared keys will get overwritten by members current.merge(members) end def separator "=" end def delimiter ";" end def retrieve #ok, some 'convention' if the keyvalue property is named properties, provider should implement a properties method if key_hash = provider.send(name) and key_hash != :absent return key_hash else return :absent end end def insync?(is) - return true unless @should - return true unless is (is == self.should) end end end end diff --git a/lib/puppet/property/list.rb b/lib/puppet/property/list.rb index dcee85db7..b86dc87f2 100644 --- a/lib/puppet/property/list.rb +++ b/lib/puppet/property/list.rb @@ -1,77 +1,75 @@ require 'puppet/property' module Puppet class Property class List < Property def should_to_s(should_value) #just return the should value should_value end def is_to_s(currentvalue) if currentvalue == :absent return "absent" else return currentvalue.join(delimiter) end end def membership :membership end def add_should_with_current(should, current) should += current if current.is_a?(Array) should.uniq end def inclusive? @resource[membership] == :inclusive end #dearrayify was motivated because to simplify the implementation of the OrderedList property def dearrayify(array) array.sort.join(delimiter) end def should return nil unless @should members = @should #inclusive means we are managing everything so if it isn't in should, its gone members = add_should_with_current(members, retrieve) if ! inclusive? dearrayify(members) end def delimiter "," end def retrieve #ok, some 'convention' if the list property is named groups, provider should implement a groups method if tmp = provider.send(name) and tmp != :absent return tmp.split(delimiter) else return :absent end end def prepare_is_for_comparison(is) if is == :absent is = [] end dearrayify(is) end def insync?(is) - return true unless @should - return true unless is (prepare_is_for_comparison(is) == self.should) end end end end diff --git a/lib/puppet/provider/file/posix.rb b/lib/puppet/provider/file/posix.rb index 6cbf98e9a..f7b8c9797 100644 --- a/lib/puppet/provider/file/posix.rb +++ b/lib/puppet/provider/file/posix.rb @@ -1,99 +1,97 @@ Puppet::Type.type(:file).provide :posix do desc "Uses POSIX functionality to manage file's users and rights." confine :feature => :posix include Puppet::Util::POSIX include Puppet::Util::Warnings require 'etc' def id2name(id) return id.to_s if id.is_a?(Symbol) return nil if id > Puppet[:maximum_uid].to_i begin user = Etc.getpwuid(id) rescue TypeError return nil rescue ArgumentError return nil end if user.uid == "" return nil else return user.name end end - def insync?(current, should) - return true unless should - + def is_owner_insync?(current, should) should.each do |value| if value =~ /^\d+$/ uid = Integer(value) elsif value.is_a?(String) fail "Could not find user #{value}" unless uid = uid(value) else uid = value end return true if uid == current end unless Puppet.features.root? warnonce "Cannot manage ownership unless running as root" return true end false end # Determine if the user is valid, and if so, return the UID def validuser?(value) Integer(value) rescue uid(value) || false end def retrieve(resource) unless stat = resource.stat(false) return :absent end currentvalue = stat.uid # On OS X, files that are owned by -2 get returned as really # large UIDs instead of negative ones. This isn't a Ruby bug, # it's an OS X bug, since it shows up in perl, too. if currentvalue > Puppet[:maximum_uid].to_i self.warning "Apparently using negative UID (#{currentvalue}) on a platform that does not consistently handle them" currentvalue = :silly end currentvalue end def sync(path, links, should) # Set our method appropriately, depending on links. if links == :manage method = :lchown else method = :chown end uid = nil should.each do |user| break if uid = validuser?(user) end raise Puppet::Error, "Could not find user(s) #{should.join(",")}" unless uid begin File.send(method, uid, nil, path) rescue => detail raise Puppet::Error, "Failed to set owner to '#{uid}': #{detail}" end :file_changed end end diff --git a/lib/puppet/provider/file/win32.rb b/lib/puppet/provider/file/win32.rb index 8ead69a89..21e7ca974 100644 --- a/lib/puppet/provider/file/win32.rb +++ b/lib/puppet/provider/file/win32.rb @@ -1,74 +1,72 @@ Puppet::Type.type(:file).provide :microsoft_windows do desc "Uses Microsoft Windows functionality to manage file's users and rights." confine :feature => :microsoft_windows include Puppet::Util::Warnings require 'sys/admin' if Puppet.features.microsoft_windows? def id2name(id) return id.to_s if id.is_a?(Symbol) return nil if id > Puppet[:maximum_uid].to_i # should translate ID numbers to usernames id end - def insync?(current, should) - return true unless should - + def is_owner_insync?(current, should) should.each do |value| if value =~ /^\d+$/ uid = Integer(value) elsif value.is_a?(String) fail "Could not find user #{value}" unless uid = uid(value) else uid = value end return true if uid == current end unless Puppet.features.root? warnonce "Cannot manage ownership unless running as root" return true end false end # Determine if the user is valid, and if so, return the UID def validuser?(value) info "Is '#{value}' a valid user?" return 0 begin number = Integer(value) return number rescue ArgumentError number = nil end (number = uid(value)) && number end def retrieve(resource) unless stat = resource.stat(false) return :absent end currentvalue = stat.uid # On OS X, files that are owned by -2 get returned as really # large UIDs instead of negative ones. This isn't a Ruby bug, # it's an OS X bug, since it shows up in perl, too. if currentvalue > Puppet[:maximum_uid].to_i self.warning "Apparently using negative UID (#{currentvalue}) on a platform that does not consistently handle them" currentvalue = :silly end currentvalue end def sync(path, links, should) info("should set '%s'%%owner to '%s'" % [path, should]) end end diff --git a/lib/puppet/provider/mount.rb b/lib/puppet/provider/mount.rb index 8c7b24bd4..c979f742f 100644 --- a/lib/puppet/provider/mount.rb +++ b/lib/puppet/provider/mount.rb @@ -1,51 +1,53 @@ # Created by Luke Kanies on 2006-11-12. # Copyright (c) 2006. All rights reserved. require 'puppet' # A module just to store the mount/unmount methods. Individual providers # still need to add the mount commands manually. module Puppet::Provider::Mount # This only works when the mount point is synced to the fstab. def mount # Manually pass the mount options in, since some OSes *cough*OS X*cough* don't # read from /etc/fstab but still want to use this type. args = [] args << "-o" << self.options if self.options and self.options != :absent args << resource[:name] flush if respond_to?(:flush) mountcmd(*args) end def remount info "Remounting" if resource[:remounts] == :true mountcmd "-o", "remount", resource[:name] else unmount mount end end # This only works when the mount point is synced to the fstab. def unmount umount resource[:name] end # Is the mount currently mounted? def mounted? platform = Facter.value("operatingsystem") name = resource[:name] mounts = mountcmd.split("\n").find do |line| case platform when "Darwin" line =~ / on #{name} / or line =~ %r{ on /private/var/automount#{name}} when "Solaris", "HP-UX" line =~ /^#{name} on / + when "AIX" + line.split(/\s+/)[1] == name else line =~ / on #{name} / end end end end diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb index 965a2aa60..b01880360 100644 --- a/lib/puppet/provider/nameservice/directoryservice.rb +++ b/lib/puppet/provider/nameservice/directoryservice.rb @@ -1,516 +1,516 @@ # Created by Jeff McCune on 2007-07-22 # Copyright (c) 2007. All rights reserved. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation (version 2 of the License) # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston MA 02110-1301 USA require 'puppet' require 'puppet/provider/nameservice' require 'facter/util/plist' require 'cgi' class Puppet::Provider::NameService class DirectoryService < Puppet::Provider::NameService # JJM: Dive into the singleton_class class << self # JJM: This allows us to pass information when calling # Puppet::Type.type # e.g. Puppet::Type.type(:user).provide :directoryservice, :ds_path => "Users" # This is referenced in the get_ds_path class method attr_writer :ds_path attr_writer :macosx_version_major end initvars commands :dscl => "/usr/bin/dscl" commands :dseditgroup => "/usr/sbin/dseditgroup" commands :sw_vers => "/usr/bin/sw_vers" confine :operatingsystem => :darwin defaultfor :operatingsystem => :darwin # JJM 2007-07-25: This map is used to map NameService attributes to their # corresponding DirectoryService attribute names. # See: http://images.apple.com/server/docs.Open_Directory_v10.4.pdf # JJM: Note, this is de-coupled from the Puppet::Type, and must # be actively maintained. There may also be collisions with different # types (Users, Groups, Mounts, Hosts, etc...) @@ds_to_ns_attribute_map = { 'RecordName' => :name, 'PrimaryGroupID' => :gid, 'NFSHomeDirectory' => :home, 'UserShell' => :shell, 'UniqueID' => :uid, 'RealName' => :comment, 'Password' => :password, 'GeneratedUID' => :guid, 'IPAddress' => :ip_address, 'ENetAddress' => :en_address, 'GroupMembership' => :members, } # JJM The same table as above, inverted. @@ns_to_ds_attribute_map = { :name => 'RecordName', :gid => 'PrimaryGroupID', :home => 'NFSHomeDirectory', :shell => 'UserShell', :uid => 'UniqueID', :comment => 'RealName', :password => 'Password', :guid => 'GeneratedUID', :en_address => 'ENetAddress', :ip_address => 'IPAddress', :members => 'GroupMembership', } @@password_hash_dir = "/var/db/shadow/hash" def self.instances # JJM Class method that provides an array of instance objects of this # type. # JJM: Properties are dependent on the Puppet::Type we're managine. type_property_array = [:name] + @resource_type.validproperties # Create a new instance of this Puppet::Type for each object present # on the system. list_all_present.collect do |name_string| self.new(single_report(name_string, *type_property_array)) end end def self.get_ds_path # JJM: 2007-07-24 This method dynamically returns the DS path we're concerned with. # For example, if we're working with an user type, this will be /Users # with a group type, this will be /Groups. # @ds_path is an attribute of the class itself. return @ds_path if defined?(@ds_path) # JJM: "Users" or "Groups" etc ... (Based on the Puppet::Type) # Remember this is a class method, so self.class is Class # Also, @resource_type seems to be the reference to the # Puppet::Type this class object is providing for. @resource_type.name.to_s.capitalize + "s" end def self.get_macosx_version_major return @macosx_version_major if defined?(@macosx_version_major) begin # Make sure we've loaded all of the facts Facter.loadfacts if Facter.value(:macosx_productversion_major) product_version_major = Facter.value(:macosx_productversion_major) else # TODO: remove this code chunk once we require Facter 1.5.5 or higher. Puppet.warning("DEPRECATION WARNING: Future versions of the directoryservice provider will require Facter 1.5.5 or newer.") product_version = Facter.value(:macosx_productversion) fail("Could not determine OS X version from Facter") if product_version.nil? product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".") end fail("#{product_version_major} is not supported by the directoryservice provider") if %w{10.0 10.1 10.2 10.3}.include?(product_version_major) @macosx_version_major = product_version_major return @macosx_version_major rescue Puppet::ExecutionFailure => detail fail("Could not determine OS X version: #{detail}") end end def self.list_all_present # JJM: List all objects of this Puppet::Type already present on the system. begin dscl_output = execute(get_exec_preamble("-list")) rescue Puppet::ExecutionFailure => detail fail("Could not get #{@resource_type.name} list from DirectoryService") end dscl_output.split("\n") end def self.parse_dscl_url_data(dscl_output) # we need to construct a Hash from the dscl -url output to match # that returned by the dscl -plist output for 10.5+ clients. # # Nasty assumptions: # a) no values *end* in a colon ':', only keys # b) if a line ends in a colon and the next line does start with # a space, then the second line is a value of the first. # c) (implied by (b)) keys don't start with spaces. dscl_plist = {} dscl_output.split("\n").inject([]) do |array, line| if line =~ /^\s+/ # it's a value array[-1] << line # add the value to the previous key else array << line end array end.compact dscl_output.each do |line| # This should be a 'normal' entry. key and value on one line. # We split on ': ' to deal with keys/values with a colon in them. split_array = line.split(/:\s+/) key = split_array.first value = CGI::unescape(split_array.last.strip.chomp) # We need to treat GroupMembership separately as it is currently # the only attribute we care about multiple values for, and # the values can never contain spaces (shortnames) # We also make every value an array to be consistent with the # output of dscl -plist under 10.5 if key == "GroupMembership" dscl_plist[key] = value.split(/\s/) else dscl_plist[key] = [value] end end dscl_plist end def self.parse_dscl_plist_data(dscl_output) Plist.parse_xml(dscl_output) end def self.generate_attribute_hash(input_hash, *type_properties) attribute_hash = {} input_hash.keys.each do |key| ds_attribute = key.sub("dsAttrTypeStandard:", "") next unless (@@ds_to_ns_attribute_map.keys.include?(ds_attribute) and type_properties.include? @@ds_to_ns_attribute_map[ds_attribute]) ds_value = input_hash[key] case @@ds_to_ns_attribute_map[ds_attribute] when :members ds_value = ds_value # only members uses arrays so far when :gid, :uid # OS X stores objects like uid/gid as strings. # Try casting to an integer for these cases to be # consistent with the other providers and the group type # validation begin ds_value = Integer(ds_value[0]) rescue ArgumentError ds_value = ds_value[0] end else ds_value = ds_value[0] end attribute_hash[@@ds_to_ns_attribute_map[ds_attribute]] = ds_value end # NBK: need to read the existing password here as it's not actually # stored in the user record. It is stored at a path that involves the # UUID of the user record for non-Mobile local acccounts. # Mobile Accounts are out of scope for this provider for now attribute_hash[:password] = self.get_password(attribute_hash[:guid]) if @resource_type.validproperties.include?(:password) and Puppet.features.root? attribute_hash end def self.single_report(resource_name, *type_properties) # JJM 2007-07-24: # Given a the name of an object and a list of properties of that # object, return all property values in a hash. # # This class method returns nil if the object doesn't exist # Otherwise, it returns a hash of the object properties. all_present_str_array = list_all_present # NBK: shortcut the process if the resource is missing return nil unless all_present_str_array.include? resource_name dscl_vector = get_exec_preamble("-read", resource_name) begin dscl_output = execute(dscl_vector) rescue Puppet::ExecutionFailure => detail fail("Could not get report. command execution failed.") end # Two code paths is ugly, but until we can drop 10.4 support we don't # have a lot of choice. Ultimately this should all be done using Ruby # to access the DirectoryService APIs directly, but that's simply not # feasible for a while yet. case self.get_macosx_version_major when "10.4" dscl_plist = self.parse_dscl_url_data(dscl_output) when "10.5", "10.6" dscl_plist = self.parse_dscl_plist_data(dscl_output) end self.generate_attribute_hash(dscl_plist, *type_properties) end def self.get_exec_preamble(ds_action, resource_name = nil) # JJM 2007-07-24 # DSCL commands are often repetitive and contain the same positional # arguments over and over. See http://developer.apple.com/documentation/Porting/Conceptual/PortingUnix/additionalfeatures/chapter_10_section_9.html # for an example of what I mean. # This method spits out proper DSCL commands for us. # We EXPECT name to be @resource[:name] when called from an instance object. # 10.4 doesn't support the -plist option for dscl, and 10.5 has a # different format for the -url output with objects with spaces in # their values. *sigh*. Use -url for 10.4 in the hope this can be # deprecated one day, and use -plist for 10.5 and higher. case self.get_macosx_version_major when "10.4" command_vector = [ command(:dscl), "-url", "." ] when "10.5", "10.6" command_vector = [ command(:dscl), "-plist", "." ] end # JJM: The actual action to perform. See "man dscl" # Common actiosn: -create, -delete, -merge, -append, -passwd command_vector << ds_action # JJM: get_ds_path will spit back "Users" or "Groups", # etc... Depending on the Puppet::Type of our self. if resource_name command_vector << "/#{get_ds_path}/#{resource_name}" else command_vector << "/#{get_ds_path}" end # JJM: This returns most of the preamble of the command. # e.g. 'dscl / -create /Users/mccune' command_vector end def self.set_password(resource_name, guid, password_hash) password_hash_file = "#{@@password_hash_dir}/#{guid}" begin File.open(password_hash_file, 'w') { |f| f.write(password_hash)} rescue Errno::EACCES => detail fail("Could not write to password hash file: #{detail}") end # NBK: For shadow hashes, the user AuthenticationAuthority must contain a value of # ";ShadowHash;". The LKDC in 10.5 makes this more interesting though as it # will dynamically generate ;Kerberosv5;;username@LKDC:SHA1 attributes if # missing. Thus we make sure we only set ;ShadowHash; if it is missing, and # we can do this with the merge command. This allows people to continue to # use other custom AuthenticationAuthority attributes without stomping on them. # # There is a potential problem here in that we're only doing this when setting # the password, and the attribute could get modified at other times while the # hash doesn't change and so this doesn't get called at all... but # without switching all the other attributes to merge instead of create I can't # see a simple enough solution for this that doesn't modify the user record # every single time. This should be a rather rare edge case. (famous last words) dscl_vector = self.get_exec_preamble("-merge", resource_name) dscl_vector << "AuthenticationAuthority" << ";ShadowHash;" begin dscl_output = execute(dscl_vector) rescue Puppet::ExecutionFailure => detail fail("Could not set AuthenticationAuthority.") end end def self.get_password(guid) password_hash = nil password_hash_file = "#{@@password_hash_dir}/#{guid}" if File.exists?(password_hash_file) and File.file?(password_hash_file) fail("Could not read password hash file at #{password_hash_file}") if not File.readable?(password_hash_file) f = File.new(password_hash_file) password_hash = f.read f.close end password_hash end def ensure=(ensure_value) super # We need to loop over all valid properties for the type we're # managing and call the method which sets that property value # dscl can't create everything at once unfortunately. if ensure_value == :present @resource.class.validproperties.each do |name| next if name == :ensure # LAK: We use property.sync here rather than directly calling # the settor method because the properties might do some kind # of conversion. In particular, the user gid property might # have a string and need to convert it to a number if @resource.should(name) @resource.property(name).sync elsif value = autogen(name) self.send(name.to_s + "=", value) else next end end end end def password=(passphrase) exec_arg_vector = self.class.get_exec_preamble("-read", @resource.name) exec_arg_vector << @@ns_to_ds_attribute_map[:guid] begin guid_output = execute(exec_arg_vector) guid_plist = Plist.parse_xml(guid_output) # Although GeneratedUID like all DirectoryService values can be multi-valued # according to the schema, in practice user accounts cannot have multiple UUIDs # otherwise Bad Things Happen, so we just deal with the first value. guid = guid_plist["dsAttrTypeStandard:#{@@ns_to_ds_attribute_map[:guid]}"][0] self.class.set_password(@resource.name, guid, passphrase) rescue Puppet::ExecutionFailure => detail fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}") end end # NBK: we override @parent.set as we need to execute a series of commands # to deal with array values, rather than the single command nameservice.rb # expects to be returned by modifycmd. Thus we don't bother defining modifycmd. def set(param, value) self.class.validate(param, value) current_members = @property_value_cache_hash[:members] if param == :members # If we are meant to be authoritative for the group membership # then remove all existing members who haven't been specified # in the manifest. remove_unwanted_members(current_members, value) if @resource[:auth_membership] and not current_members.nil? # if they're not a member, make them one. add_members(current_members, value) else exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name]) # JJM: The following line just maps the NS name to the DS name # e.g. { :uid => 'UniqueID' } exec_arg_vector << @@ns_to_ds_attribute_map[symbolize(param)] # JJM: The following line sends the actual value to set the property to exec_arg_vector << value.to_s begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}") end end end # NBK: we override @parent.create as we need to execute a series of commands # to create objects with dscl, rather than the single command nameservice.rb # expects to be returned by addcmd. Thus we don't bother defining addcmd. def create if exists? info "already exists" return nil end # NBK: First we create the object with a known guid so we can set the contents # of the password hash if required # Shelling out sucks, but for a single use case it doesn't seem worth # requiring people install a UUID library that doesn't come with the system. # This should be revisited if Puppet starts managing UUIDs for other platform # user records. guid = %x{/usr/bin/uuidgen}.chomp exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name]) exec_arg_vector << @@ns_to_ds_attribute_map[:guid] << guid begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail fail("Could not set GeneratedUID for #{@resource.class.name} #{@resource.name}: #{detail}") end if value = @resource.should(:password) and value != "" self.class.set_password(@resource[:name], guid, value) end # Now we create all the standard properties Puppet::Type.type(@resource.class.name).validproperties.each do |property| next if property == :ensure if value = @resource.should(property) and value != "" if property == :members add_members(nil, value) else exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name]) exec_arg_vector << @@ns_to_ds_attribute_map[symbolize(property)] next if property == :password # skip setting the password here exec_arg_vector << value.to_s begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail fail("Could not create #{@resource.class.name} #{@resource.name}: #{detail}") end end end end end def remove_unwanted_members(current_members, new_members) current_members.each do |member| - if not new_members.include?(member) + if not new_members.flatten.include?(member) cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-d", member, @resource[:name]] begin execute(cmd) rescue Puppet::ExecutionFailure => detail fail("Could not remove #{member} from group: #{@resource.name}, #{detail}") end end end end def add_members(current_members, new_members) - new_members.each do |new_member| + new_members.flatten.each do |new_member| if current_members.nil? or not current_members.include?(new_member) cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-a", new_member, @resource[:name]] begin execute(cmd) rescue Puppet::ExecutionFailure => detail fail("Could not add #{new_member} to group: #{@resource.name}, #{detail}") end end end end def deletecmd # JJM: Like addcmd, only called when deleting the object itself # Note, this isn't used to delete properties of the object, # at least that's how I understand it... self.class.get_exec_preamble("-delete", @resource[:name]) end def getinfo(refresh = false) # JJM 2007-07-24: # Override the getinfo method, which is also defined in nameservice.rb # This method returns and sets @infohash # I'm not re-factoring the name "getinfo" because this method will be # most likely called by nameservice.rb, which I didn't write. if refresh or (! defined?(@property_value_cache_hash) or ! @property_value_cache_hash) # JJM 2007-07-24: OK, there's a bit of magic that's about to # happen... Let's see how strong my grip has become... =) # # self is a provider instance of some Puppet::Type, like # Puppet::Type::User::ProviderDirectoryservice for the case of the # user type and this provider. # # self.class looks like "user provider directoryservice", if that # helps you ... # # self.class.resource_type is a reference to the Puppet::Type class, # probably Puppet::Type::User or Puppet::Type::Group, etc... # # self.class.resource_type.validproperties is a class method, # returning an Array of the valid properties of that specific # Puppet::Type. # # So... something like [:comment, :home, :password, :shell, :uid, # :groups, :ensure, :gid] # # Ultimately, we add :name to the list, delete :ensure from the # list, then report on the remaining list. Pretty whacky, ehh? type_properties = [:name] + self.class.resource_type.validproperties type_properties.delete(:ensure) if type_properties.include? :ensure type_properties << :guid # append GeneratedUID so we just get the report here @property_value_cache_hash = self.class.single_report(@resource[:name], *type_properties) [:uid, :gid].each do |param| @property_value_cache_hash[param] = @property_value_cache_hash[param].to_i if @property_value_cache_hash and @property_value_cache_hash.include?(param) end end @property_value_cache_hash end end end diff --git a/lib/puppet/provider/package/freebsd.rb b/lib/puppet/provider/package/freebsd.rb index 2f012a4ed..e10a20b04 100755 --- a/lib/puppet/provider/package/freebsd.rb +++ b/lib/puppet/provider/package/freebsd.rb @@ -1,50 +1,50 @@ Puppet::Type.type(:package).provide :freebsd, :parent => :openbsd do desc "The specific form of package management on FreeBSD. This is an extremely quirky packaging system, in that it freely mixes between ports and packages. Apparently all of the tools are written in Ruby, so there are plans to rewrite this support to directly use those libraries." commands :pkginfo => "/usr/sbin/pkg_info", :pkgadd => "/usr/sbin/pkg_add", :pkgdelete => "/usr/sbin/pkg_delete" confine :operatingsystem => :freebsd def self.listcmd command(:pkginfo) end def install should = @resource.should(:ensure) if @resource[:source] =~ /\/$/ if @resource[:source] =~ /^(ftp|https?):/ - withenv :PACKAGESITE => @resource[:source] do + Puppet::Util::Execution::withenv :PACKAGESITE => @resource[:source] do pkgadd "-r", @resource[:name] end else - withenv :PKG_PATH => @resource[:source] do + Puppet::Util::Execution::withenv :PKG_PATH => @resource[:source] do pkgadd @resource[:name] end end else Puppet.warning "source is defined but does not have trailing slash, ignoring #{@resource[:source]}" if @resource[:source] pkgadd "-r", @resource[:name] end end def query self.class.instances.each do |provider| if provider.name == @resource.name return provider.properties end end nil end def uninstall pkgdelete "#{@resource[:name]}-#{@resource.should(:ensure)}" end end diff --git a/lib/puppet/provider/zone/solaris.rb b/lib/puppet/provider/zone/solaris.rb index c11444993..f46337b14 100644 --- a/lib/puppet/provider/zone/solaris.rb +++ b/lib/puppet/provider/zone/solaris.rb @@ -1,257 +1,257 @@ Puppet::Type.type(:zone).provide(:solaris) do desc "Provider for Solaris Zones." commands :adm => "/usr/sbin/zoneadm", :cfg => "/usr/sbin/zonecfg" defaultfor :operatingsystem => :solaris mk_resource_methods # Convert the output of a list into a hash def self.line2hash(line) fields = [:id, :name, :ensure, :path] properties = {} line.split(":").each_with_index { |value, index| next unless fields[index] properties[fields[index]] = value } # Configured but not installed zones do not have IDs properties.delete(:id) if properties[:id] == "-" properties[:ensure] = symbolize(properties[:ensure]) properties end def self.instances # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = adm(:list, "-cp").split("\n").collect do |line| new(line2hash(line)) end end # Perform all of our configuration steps. def configure # If the thing is entirely absent, then we need to create the config. # Is there someway to get this on one line? str = "create -b #{@resource[:create_args]}\nset zonepath=#{@resource[:path]}\n" # Then perform all of our configuration steps. It's annoying # that we need this much internal info on the resource. @resource.send(:properties).each do |property| - str += property.configtext + "\n" if property.is_a? ZoneConfigProperty and ! property.insync?(properties[property.name]) + str += property.configtext + "\n" if property.is_a? ZoneConfigProperty and ! property.safe_insync?(properties[property.name]) end str += "commit\n" setconfig(str) end def destroy zonecfg :delete, "-F" end def exists? properties[:ensure] != :absent end # Clear out the cached values. def flush @property_hash.clear end def install(dummy_argument=:work_arround_for_ruby_GC_bug) if @resource[:clone] # TODO: add support for "-s snapshot" zoneadm :clone, @resource[:clone] elsif @resource[:install_args] zoneadm :install, @resource[:install_args].split(" ") else zoneadm :install end end # Look up the current status. def properties if @property_hash.empty? @property_hash = status || {} if @property_hash.empty? @property_hash[:ensure] = :absent else @resource.class.validproperties.each do |name| @property_hash[name] ||= :absent end end end @property_hash.dup end # We need a way to test whether a zone is in process. Our 'ensure' # property models the static states, but we need to handle the temporary ones. def processing? if hash = status case hash[:ensure] when "incomplete", "ready", "shutting_down" true else false end else false end end # Collect the configuration of the zone. def getconfig output = zonecfg :info name = nil current = nil hash = {} output.split("\n").each do |line| case line when /^(\S+):\s*$/ name = $1 current = nil # reset it when /^(\S+):\s*(.+)$/ hash[$1.intern] = $2 when /^\s+(\S+):\s*(.+)$/ if name hash[name] = [] unless hash.include? name unless current current = {} hash[name] << current end current[$1.intern] = $2 else err "Ignoring '#{line}'" end else debug "Ignoring zone output '#{line}'" end end hash end # Execute a configuration string. Can't be private because it's called # by the properties. def setconfig(str) command = "#{command(:cfg)} -z #{@resource[:name]} -f -" debug "Executing '#{command}' in zone #{@resource[:name]} with '#{str}'" IO.popen(command, "w") do |pipe| pipe.puts str end unless $CHILD_STATUS == 0 raise ArgumentError, "Failed to apply configuration" end end def start # Check the sysidcfg stuff if cfg = @resource[:sysidcfg] zoneetc = File.join(@resource[:path], "root", "etc") sysidcfg = File.join(zoneetc, "sysidcfg") # if the zone root isn't present "ready" the zone # which makes zoneadmd mount the zone root zoneadm :ready unless File.directory?(zoneetc) unless File.exists?(sysidcfg) begin File.open(sysidcfg, "w", 0600) do |f| f.puts cfg end rescue => detail puts detail.stacktrace if Puppet[:debug] raise Puppet::Error, "Could not create sysidcfg: #{detail}" end end end zoneadm :boot end # Return a hash of the current status of this zone. def status begin output = adm "-z", @resource[:name], :list, "-p" rescue Puppet::ExecutionFailure return nil end main = self.class.line2hash(output.chomp) # Now add in the configuration information config_status.each do |name, value| main[name] = value end main end def ready zoneadm :ready end def stop zoneadm :halt end def unconfigure zonecfg :delete, "-F" end def uninstall zoneadm :uninstall, "-F" end private # Turn the results of getconfig into status information. def config_status config = getconfig result = {} result[:autoboot] = config[:autoboot] ? config[:autoboot].intern : :absent result[:pool] = config[:pool] result[:shares] = config[:shares] if dir = config["inherit-pkg-dir"] result[:inherit] = dir.collect { |dirs| dirs[:dir] } end result[:iptype] = config[:"ip-type"] if net = config["net"] result[:ip] = net.collect do |params| if params[:defrouter] "#{params[:physical]}:#{params[:address]}:#{params[:defrouter]}" elsif params[:address] "#{params[:physical]}:#{params[:address]}" else params[:physical] end end end result end def zoneadm(*cmd) adm("-z", @resource[:name], *cmd) rescue Puppet::ExecutionFailure => detail self.fail "Could not #{cmd[0]} zone: #{detail}" end def zonecfg(*cmd) # You apparently can't get the configuration of the global zone return "" if self.name == "global" begin cfg("-z", self.name, *cmd) rescue Puppet::ExecutionFailure => detail self.fail "Could not #{cmd[0]} zone: #{detail}" end end end diff --git a/lib/puppet/reference/configuration.rb b/lib/puppet/reference/configuration.rb index e6a8dc20f..c8ff145ba 100644 --- a/lib/puppet/reference/configuration.rb +++ b/lib/puppet/reference/configuration.rb @@ -1,144 +1,144 @@ config = Puppet::Util::Reference.newreference(:configuration, :depth => 1, :doc => "A reference for all configuration parameters") do docs = {} Puppet.settings.each do |name, object| docs[name] = object end str = "" docs.sort { |a, b| a[0].to_s <=> b[0].to_s }.each do |name, object| # Make each name an anchor header = name.to_s str += h(header, 3) # Print the doc string itself begin str += object.desc.gsub(/\n/, " ") rescue => detail puts detail.backtrace puts detail end str += "\n\n" # Now print the data about the item. str += "" val = object.default if name.to_s == "vardir" val = "/var/lib/puppet" elsif name.to_s == "confdir" val = "/etc/puppet" end # Leave out the section information; it was apparently confusing people. #str += "- **Section**: #{object.section}\n" unless val == "" str += "- *Default*: #{val}\n" end str += "\n" end return str end config.header = " ## Specifying Configuration Parameters ### On The Command-Line Every Puppet executable (with the exception of `puppetdoc`) accepts all of the parameters below, but not all of the arguments make sense for every executable. I have tried to be as thorough as possible in the descriptions of the arguments, so it should be obvious whether an argument is appropriate or not. These parameters can be supplied to the executables either as command-line options or in the configuration file. For instance, the command-line invocation below would set the configuration directory to `/private/puppet`: $ puppet agent --confdir=/private/puppet Note that boolean options are turned on and off with a slightly different syntax on the command line: $ puppet agent --storeconfigs $ puppet agent --no-storeconfigs The invocations above will enable and disable, respectively, the storage of the client configuration. ### Configuration Files As mentioned above, the configuration parameters can also be stored in a configuration file, located in the configuration directory. As root, the default configuration directory is `/etc/puppet`, and as a regular user, the default configuration directory is `~user/.puppet`. As of 0.23.0, all executables look for `puppet.conf` in their configuration directory (although they previously looked for separate files). For example, `puppet.conf` is located at `/etc/puppet/puppet.conf` as `root` and `~user/.puppet/puppet.conf` as a regular user by default. All executables will set any parameters set within the `[main]` section, and each executable will also use one of the `[master]`, `[agent]`. #### File Format The file follows INI-style formatting. Here is an example of a very simple `puppet.conf` file: [main] confdir = /private/puppet storeconfigs = true Note that boolean parameters must be explicitly specified as `true` or `false` as seen above. -If you need to change file parameters (e.g., reset the mode or owner), do +If you need to change file or directory parameters (e.g., reset the mode or owner), do so within curly braces on the same line: [main] - myfile = /tmp/whatever {owner = root, mode = 644} + vardir = /new/vardir {owner = root, mode = 644} If you're starting out with a fresh configuration, you may wish to let the executable generate a template configuration file for you by invoking the executable in question with the `--genconfig` command. The executable will print a template configuration to standard output, which can be redirected to a file like so: $ puppet agent --genconfig > /etc/puppet/puppet.conf Note that this invocation will replace the contents of any pre-existing `puppet.conf` file, so make a backup of your present config if it contains valuable information. Like the `--genconfig` argument, the executables also accept a `--genmanifest` argument, which will generate a manifest that can be used to manage all of Puppet's directories and files and prints it to standard output. This can likewise be redirected to a file: $ puppet agent --genmanifest > /etc/puppet/manifests/site.pp Puppet can also create user and group accounts for itself (one `puppet` group and one `puppet` user) if it is invoked as `root` with the `--mkusers` argument: $ puppet agent --mkusers ## Signals The `puppet agent` and `puppet master` executables catch some signals for special handling. Both daemons catch (`SIGHUP`), which forces the server to restart tself. Predictably, interrupt and terminate (`SIGINT` and `SIGTERM`) will shut down the server, whether it be an instance of `puppet agent` or `puppet master`. Sending the `SIGUSR1` signal to an instance of `puppet agent` will cause it to immediately begin a new configuration transaction with the server. This signal has no effect on `puppet master`. ## Configuration Parameter Reference Below is a list of all documented parameters. Not all of them are valid with all Puppet executables, but the executables will ignore any inappropriate values. " diff --git a/lib/puppet/reference/function.rb b/lib/puppet/reference/function.rb index 1333e0d26..7d39bebd5 100644 --- a/lib/puppet/reference/function.rb +++ b/lib/puppet/reference/function.rb @@ -1,13 +1,17 @@ function = Puppet::Util::Reference.newreference :function, :doc => "All functions available in the parser" do Puppet::Parser::Functions.functiondocs end function.header = " There are two types of functions in Puppet: Statements and rvalues. Statements stand on their own and do not return arguments; they are used for performing stand-alone work like importing. Rvalues return values and can only be used in a statement requiring a value, such as an assignment or a case statement. +Functions execute on the Puppet master. They do not execute on the Puppet agent. +Hence they only have access to the commands and data available on the Puppet master +host. + Here are the functions available in Puppet: " diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb index b0a3ecee6..e832804f5 100644 --- a/lib/puppet/resource.rb +++ b/lib/puppet/resource.rb @@ -1,420 +1,425 @@ require 'puppet' require 'puppet/util/tagging' require 'puppet/util/pson' # The simplest resource class. Eventually it will function as the # base class for all resource-like behaviour. class Puppet::Resource include Puppet::Util::Tagging require 'puppet/resource/type_collection_helper' include Puppet::Resource::TypeCollectionHelper extend Puppet::Util::Pson include Enumerable attr_accessor :file, :line, :catalog, :exported, :virtual, :validate_parameters, :strict attr_reader :type, :title require 'puppet/indirector' extend Puppet::Indirector indirects :resource, :terminus_class => :ral ATTRIBUTES = [:file, :line, :exported] def self.from_pson(pson) raise ArgumentError, "No resource type provided in pson data" unless type = pson['type'] raise ArgumentError, "No resource title provided in pson data" unless title = pson['title'] resource = new(type, title) if params = pson['parameters'] params.each { |param, value| resource[param] = value } end if tags = pson['tags'] tags.each { |tag| resource.tag(tag) } end ATTRIBUTES.each do |a| if value = pson[a.to_s] resource.send(a.to_s + "=", value) end end resource.exported ||= false resource end def inspect "#{@type}[#{@title}]#{to_hash.inspect}" end def to_pson_data_hash data = ([:type, :title, :tags] + ATTRIBUTES).inject({}) do |hash, param| next hash unless value = self.send(param) hash[param.to_s] = value hash end data["exported"] ||= false params = self.to_hash.inject({}) do |hash, ary| param, value = ary # Don't duplicate the title as the namevar next hash if param == namevar and value == title hash[param] = Puppet::Resource.value_to_pson_data(value) hash end data["parameters"] = params unless params.empty? data end def self.value_to_pson_data(value) if value.is_a? Array value.map{|v| value_to_pson_data(v) } elsif value.is_a? Puppet::Resource value.to_s else value end end def yaml_property_munge(x) case x when Hash x.inject({}) { |h,kv| k,v = kv h[k] = self.class.value_to_pson_data(v) h } else self.class.value_to_pson_data(x) end end def to_pson(*args) to_pson_data_hash.to_pson(*args) end # Proxy these methods to the parameters hash. It's likely they'll # be overridden at some point, but this works for now. %w{has_key? keys length delete empty? <<}.each do |method| define_method(method) do |*args| @parameters.send(method, *args) end end # Set a given parameter. Converts all passed names # to lower-case symbols. def []=(param, value) validate_parameter(param) if validate_parameters @parameters[parameter_name(param)] = value end # Return a given parameter's value. Converts all passed names # to lower-case symbols. def [](param) @parameters[parameter_name(param)] end def ==(other) return false unless other.respond_to?(:title) and self.type == other.type and self.title == other.title return false unless to_hash == other.to_hash true end # Compatibility method. def builtin? builtin_type? end # Is this a builtin resource type? def builtin_type? resource_type.is_a?(Class) end # Iterate over each param/value pair, as required for Enumerable. def each @parameters.each { |p,v| yield p, v } end def include?(parameter) super || @parameters.keys.include?( parameter_name(parameter) ) end # These two methods are extracted into a Helper # module, but file load order prevents me # from including them in the class, and I had weird # behaviour (i.e., sometimes it didn't work) when # I directly extended each resource with the helper. def environment Puppet::Node::Environment.new(@environment) end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = env else @environment = env.name end end %w{exported virtual strict}.each do |m| define_method(m+"?") do self.send(m) end end # This stub class is only needed for serialization compatibility with 0.25.x class Reference attr_accessor :type,:title def initialize(type,title) @type,@title = type,title end end # Create our resource. def initialize(type, title = nil, attributes = {}) @parameters = {} # Set things like strictness first. attributes.each do |attr, value| next if attr == :parameters send(attr.to_s + "=", value) end @type, @title = extract_type_and_title(type, title) @type = munge_type_name(@type) if @type == "Class" @title = :main if @title == "" @title = munge_type_name(@title) end if params = attributes[:parameters] extract_parameters(params) end tag(self.type) tag(self.title) if valid_tag?(self.title) @reference = Reference.new(@type,@title) # for serialization compatibility with 0.25.x - - raise ArgumentError, "Invalid resource type #{type}" if strict? and ! resource_type + if strict? and ! resource_type + if @type == 'Class' + raise ArgumentError, "Could not find declared class #{title}" + else + raise ArgumentError, "Invalid resource type #{type}" + end + end end def ref to_s end # Find our resource. def resolve return(catalog ? catalog.resource(to_s) : nil) end def resource_type case type when "Class"; known_resource_types.hostclass(title == :main ? "" : title) when "Node"; known_resource_types.node(title) else Puppet::Type.type(type.to_s.downcase.to_sym) || known_resource_types.definition(type) end end # Produce a simple hash of our parameters. def to_hash parse_title.merge @parameters end def to_s "#{type}[#{title}]" end def uniqueness_key # Temporary kludge to deal with inconsistant use patters h = self.to_hash h[namevar] ||= h[:name] h[:name] ||= h[namevar] h.values_at(*key_attributes.sort_by { |k| k.to_s }) end def key_attributes return(resource_type.respond_to? :key_attributes) ? resource_type.key_attributes : [:name] end # Convert our resource to Puppet code. def to_manifest "%s { '%s':\n%s\n}" % [self.type.to_s.downcase, self.title, @parameters.collect { |p, v| if v.is_a? Array " #{p} => [\'#{v.join("','")}\']" else " #{p} => \'#{v}\'" end }.join(",\n") ] end def to_ref ref end # Convert our resource to a RAL resource instance. Creates component # instances for resource types that don't exist. def to_ral if typeklass = Puppet::Type.type(self.type) return typeklass.new(self) else return Puppet::Type::Component.new(self) end end # Translate our object to a backward-compatible transportable object. def to_trans if builtin_type? and type.downcase.to_s != "stage" result = to_transobject else result = to_transbucket end result.file = self.file result.line = self.line result end def to_trans_ref [type.to_s, title.to_s] end # Create an old-style TransObject instance, for builtin resource types. def to_transobject # Now convert to a transobject result = Puppet::TransObject.new(title, type) to_hash.each do |p, v| if v.is_a?(Puppet::Resource) v = v.to_trans_ref elsif v.is_a?(Array) v = v.collect { |av| av = av.to_trans_ref if av.is_a?(Puppet::Resource) av } end # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. result[p.to_s] = if v.is_a?(Array) and v.length == 1 v[0] else v end end result.tags = self.tags result end def name # this is potential namespace conflict # between the notion of an "indirector name" # and a "resource name" [ type, title ].join('/') end def to_resource self end def valid_parameter?(name) resource_type.valid_parameter?(name) end def validate_parameter(name) raise ArgumentError, "Invalid parameter #{name}" unless valid_parameter?(name) end private # Produce a canonical method name. def parameter_name(param) param = param.to_s.downcase.to_sym if param == :name and n = namevar param = namevar end param end # The namevar for our resource type. If the type doesn't exist, # always use :name. def namevar if builtin_type? and t = resource_type and t.key_attributes.length == 1 t.key_attributes.first else :name end end # Create an old-style TransBucket instance, for non-builtin resource types. def to_transbucket bucket = Puppet::TransBucket.new([]) bucket.type = self.type bucket.name = self.title # TransBuckets don't support parameters, which is why they're being deprecated. bucket end def extract_parameters(params) params.each do |param, value| validate_parameter(param) if strict? self[param] = value end end def extract_type_and_title(argtype, argtitle) if (argtitle || argtype) =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ] elsif argtitle then [ argtype, argtitle ] elsif argtype.is_a?(Puppet::Type) then [ argtype.class.name, argtype.title ] elsif argtype.is_a?(Hash) then raise ArgumentError, "Puppet::Resource.new does not take a hash as the first argument. "+ "Did you mean (#{(argtype[:type] || argtype["type"]).inspect}, #{(argtype[:title] || argtype["title"]).inspect }) ?" else raise ArgumentError, "No title provided and #{argtype.inspect} is not a valid resource reference" end end def munge_type_name(value) return :main if value == :main return "Class" if value == "" or value.nil? or value.to_s.downcase == "component" value.to_s.split("::").collect { |s| s.capitalize }.join("::") end def parse_title h = {} type = resource_type if type.respond_to? :title_patterns type.title_patterns.each { |regexp, symbols_and_lambdas| if captures = regexp.match(title.to_s) symbols_and_lambdas.zip(captures[1..-1]).each { |symbol_and_lambda,capture| sym, lam = symbol_and_lambda #self[sym] = lam.call(capture) h[sym] = lam.call(capture) } return h end } else return { :name => title.to_s } end end end diff --git a/lib/puppet/resource/type.rb b/lib/puppet/resource/type.rb index d40adc145..34fddf135 100644 --- a/lib/puppet/resource/type.rb +++ b/lib/puppet/resource/type.rb @@ -1,339 +1,347 @@ require 'puppet/parser/parser' require 'puppet/util/warnings' require 'puppet/util/errors' require 'puppet/util/inline_docs' require 'puppet/parser/ast/leaf' require 'puppet/dsl' class Puppet::Resource::Type Puppet::ResourceType = self include Puppet::Util::InlineDocs include Puppet::Util::Warnings include Puppet::Util::Errors RESOURCE_SUPERTYPES = [:hostclass, :node, :definition] attr_accessor :file, :line, :doc, :code, :ruby_code, :parent, :resource_type_collection, :module_name attr_reader :type, :namespace, :arguments, :behaves_like RESOURCE_SUPERTYPES.each do |t| define_method("#{t}?") { self.type == t } end require 'puppet/indirector' extend Puppet::Indirector indirects :resource_type, :terminus_class => :parser def self.from_pson(data) name = data.delete('name') or raise ArgumentError, "Resource Type names must be specified" type = data.delete('type') || "definition" data = data.inject({}) { |result, ary| result[ary[0].intern] = ary[1]; result } new(type, name, data) end def to_pson_data_hash data = [:code, :doc, :line, :file, :parent].inject({}) do |hash, param| next hash unless value = self.send(param) hash[param.to_s] = value hash end data['arguments'] = arguments.dup data['name'] = name data['type'] = type data end def to_pson(*args) to_pson_data_hash.to_pson(*args) 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) scope = resource.scope if tmp = evaluate_parent_type(resource) scope = tmp end scope = subscope(scope, resource) unless resource.title == :main scope.compiler.add_class(name) unless definition? set_resource_parameters(resource, scope) code.safeevaluate(scope) if code 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_SUPERTYPES.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]) 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? @name =~ 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 array_class = Puppet::Parser::AST::ASTArray self.code = array_class.new(:children => [self.code]) unless self.code.is_a?(array_class) if other.code.is_a?(array_class) code.children += other.code.children else code.children << other.code end 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) + 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. - if resource = scope.catalog.resource(resource_type, name) + # 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) + if parameters + parameters.each do |k,v| + resource.set_parameter(k,v) + end + end 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 # 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.setvar(param.to_s, value) } set[param] = true end + if @type == :hostclass + scope.setvar("title", resource.title.to_s.downcase) unless set.include? :title + scope.setvar("name", resource.name.to_s.downcase ) unless set.include? :name + else + scope.setvar("title", resource.title ) unless set.include? :title + scope.setvar("name", resource.name ) unless set.include? :name + end + scope.setvar("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.setvar("caller_module_name", caller_name) + end + scope.class_set(self.name,scope) if hostclass? or node? # Verify that all required arguments are either present or # have been provided with defaults. arguments.each do |param, default| param = param.to_sym next if set.include?(param) # Even if 'default' is a false value, it's an AST value, so this works fine fail Puppet::ParseError, "Must pass #{param} to #{resource.ref}" unless default value = default.safeevaluate(scope) scope.setvar(param.to_s, value) # Set it in the resource, too, so the value makes it to the client. resource[param] = value end - if @type == :hostclass - scope.setvar("title", resource.title.to_s.downcase) unless set.include? :title - scope.setvar("name", resource.name.to_s.downcase ) unless set.include? :name - else - scope.setvar("title", resource.title ) unless set.include? :title - scope.setvar("name", resource.name ) unless set.include? :name - end - scope.setvar("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.setvar("caller_module_name", caller_name) - end - scope.class_set(self.name,scope) if hostclass? or node? end # Create a new subscope in which to evaluate our code. def subscope(scope, resource) scope.newscope :resource => resource, :namespace => self.namespace, :source => self 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 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" 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/resource_harness.rb b/lib/puppet/transaction/resource_harness.rb index c1b980632..4a3d35e0d 100644 --- a/lib/puppet/transaction/resource_harness.rb +++ b/lib/puppet/transaction/resource_harness.rb @@ -1,196 +1,178 @@ require 'puppet/resource/status' class Puppet::Transaction::ResourceHarness extend Forwardable def_delegators :@transaction, :relationship_graph attr_reader :transaction def allow_changes?(resource) if resource.purging? and resource.deleting? and deps = relationship_graph.dependents(resource) \ and ! deps.empty? and deps.detect { |d| ! d.deleting? } deplabel = deps.collect { |r| r.ref }.join(",") plurality = deps.length > 1 ? "":"s" resource.warning "#{deplabel} still depend#{plurality} on me -- not purging" false else true end end # Used mostly for scheduling and auditing at this point. def cached(resource, name) Puppet::Util::Storage.cache(resource)[name] end # Used mostly for scheduling and auditing at this point. def cache(resource, name, value) Puppet::Util::Storage.cache(resource)[name] = value end def perform_changes(resource) current = resource.retrieve_resource cache resource, :checked, Time.now return [] if ! allow_changes?(resource) current_values = current.to_hash historical_values = Puppet::Util::Storage.cache(resource).dup desired_values = {} resource.properties.each do |property| desired_values[property.name] = property.should end audited_params = (resource[:audit] || []).map { |p| p.to_sym } synced_params = [] # Record the current state in state.yml. audited_params.each do |param| cache(resource, param, current_values[param]) end # Update the machine state & create logs/events events = [] ensure_param = resource.parameter(:ensure) - if desired_values[:ensure] && !ensure_param.insync?(current_values[:ensure]) + if desired_values[:ensure] && !ensure_param.safe_insync?(current_values[:ensure]) events << apply_parameter(ensure_param, current_values[:ensure], audited_params.include?(:ensure), historical_values[:ensure]) synced_params << :ensure elsif current_values[:ensure] != :absent work_order = resource.properties # Note: only the resource knows what order to apply changes in work_order.each do |param| - if desired_values[param.name] && !param.insync?(current_values[param.name]) + if desired_values[param.name] && !param.safe_insync?(current_values[param.name]) events << apply_parameter(param, current_values[param.name], audited_params.include?(param.name), historical_values[param.name]) synced_params << param.name end end end # Add more events to capture audit results audited_params.each do |param_name| if historical_values.include?(param_name) if historical_values[param_name] != current_values[param_name] && !synced_params.include?(param_name) event = create_change_event(resource.parameter(param_name), current_values[param_name], true, historical_values[param_name]) event.send_log events << event end else resource.property(param_name).notice "audit change: newly-recorded value #{current_values[param_name]}" end end events end def create_change_event(property, current_value, do_audit, historical_value) event = property.event event.previous_value = current_value event.desired_value = property.should event.historical_value = historical_value if do_audit event.audited = true event.status = "audit" if historical_value != current_value event.message = "audit change: previously recorded value #{property.is_to_s(historical_value)} has been changed to #{property.is_to_s(current_value)}" end end event end def apply_parameter(property, current_value, do_audit, historical_value) event = create_change_event(property, current_value, do_audit, historical_value) if do_audit && historical_value && historical_value != current_value brief_audit_message = " (previously recorded value was #{property.is_to_s(historical_value)})" else brief_audit_message = "" end if property.noop event.message = "current_value #{property.is_to_s(current_value)}, should be #{property.should_to_s(property.should)} (noop)#{brief_audit_message}" event.status = "noop" else property.sync event.message = [ property.change_to_s(current_value, property.should), brief_audit_message ].join event.status = "success" end event rescue => detail puts detail.backtrace if Puppet[:trace] event.status = "failure" event.message = "change from #{property.is_to_s(current_value)} to #{property.should_to_s(property.should)} failed: #{detail}" event ensure event.send_log end def evaluate(resource) start = Time.now status = Puppet::Resource::Status.new(resource) perform_changes(resource).each do |event| status << event end if status.changed? && ! resource.noop? cache(resource, :synced, Time.now) resource.flush if resource.respond_to?(:flush) end return status rescue => detail resource.fail "Could not create resource status: #{detail}" unless status puts detail.backtrace if Puppet[:trace] resource.err "Could not evaluate: #{detail}" status.failed = true return status ensure (status.evaluation_time = Time.now - start) if status end def initialize(transaction) @transaction = transaction end def scheduled?(status, resource) return true if Puppet[:ignoreschedules] return true unless schedule = schedule(resource) # We use 'checked' here instead of 'synced' because otherwise we'll # end up checking most resources most times, because they will generally # have been synced a long time ago (e.g., a file only gets updated # once a month on the server and its schedule is daily; the last sync time # will have been a month ago, so we'd end up checking every run). schedule.match?(cached(resource, :checked).to_i) end def schedule(resource) unless resource.catalog resource.warning "Cannot schedule without a schedule-containing catalog" return nil end return nil unless name = resource[:schedule] resource.catalog.resource(:schedule, name) || resource.fail("Could not find schedule #{name}") end - - private - - def absent_and_not_being_created?(current, param) - current[:ensure] == :absent and param.should.nil? - end - - def ensure_is_insync?(current, param) - param.insync?(current[:ensure]) - end - - def ensure_should_be_absent?(current, param) - param.should == :absent - end - - def param_is_insync?(current, param) - param.insync?(current[param.name]) - end end diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index ea3944b4e..e03650b54 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1,1897 +1,1897 @@ 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/log_paths' require 'puppet/util/logging' require 'puppet/util/cacher' require 'puppet/file_collection/lookup' require 'puppet/util/tagging' # see the bottom of the file for the rest of the inclusions module Puppet class Type include Puppet::Util include Puppet::Util::Errors include Puppet::Util::LogPaths include Puppet::Util::Logging include Puppet::Util::Cacher include Puppet::FileCollection::Lookup include Puppet::Util::Tagging ############################### # Code related to resource type attributes. class << self include Puppet::Util::ClassGen include Puppet::Util::Warnings attr_reader :properties end def self.states warnonce "The states method is deprecated; use properties" properties end # All parameters, in the appropriate order. The key_attributes come first, then # the provider, then the properties, and finally the params and metaparams # in the order they were specified in the files. def self.allattrs key_attributes | (parameters & [:provider]) | properties.collect { |property| property.name } | parameters | metaparams end # Retrieve an attribute alias, if there is one. def self.attr_alias(param) @attr_aliases[symbolize(param)] end # Create an alias to an existing attribute. This will cause the aliased # attribute to be valid when setting and retrieving values on the instance. def self.set_attr_alias(hash) hash.each do |new, old| @attr_aliases[symbolize(new)] = symbolize(old) end end # Find the class associated with any given 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 # What type of parameter are we dealing with? Cache the results, because # this method gets called so many times. 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 def self.eachmetaparam @@metaparams.each { |p| yield p.name } end # Create the 'ensure' class. This is a separate method so other types # can easily call it and create their own 'ensure' values. 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 # Should we add the 'ensure' property to this class? def self.ensurable? # If the class has all three of these methods defined, then it's # ensurable. ens = [:exists?, :create, :destroy].inject { |set, method| set &&= self.public_method_defined?(method) } ens end # Deal with any options passed into parameters. 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 parameter in question a meta-parameter? def self.metaparam?(param) @@metaparamhash.include?(symbolize(param)) end # Find the metaparameter class associated with a given metaparameter name. def self.metaparamclass(name) @@metaparamhash[symbolize(name)] end def self.metaparams @@metaparams.collect { |param| param.name } end def self.metaparamdoc(metaparam) @@metaparamhash[metaparam].doc end # Create a new metaparam. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newmetaparam(name, options = {}, &block) @@metaparams ||= [] @@metaparamhash ||= {} name = symbolize(name) 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 def self.key_attribute_parameters @key_attribute_parameters ||= ( params = @parameters.find_all { |param| param.isnamevar? or param.name == :name } ) end def self.key_attributes key_attribute_parameters.collect { |p| p.name } end def self.title_patterns case key_attributes.length when 0; [] when 1; identity = lambda {|x| x} [ [ /(.*)/m, [ [key_attributes.first, identity ] ] ] ] else raise Puppet::DevError,"you must specify title patterns when there are two or more key attributes" end end def uniqueness_key to_resource.uniqueness_key end # Create a new parameter. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. 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 def self.newstate(name, options = {}, &block) Puppet.warning "newstate() has been deprecrated; use newproperty(#{name})" newproperty(name, options, &block) end # Create a new property. The first parameter must be the name of the property; # this is how users will refer to the property when creating new instances. # The second parameter is a hash of options; the options are: # * :parent: The parent class for the property. Defaults to Puppet::Property. # * :retrieve: The method to call on the provider or @parent object (if # the provider is not set) to retrieve the current value. def self.newproperty(name, options = {}, &block) name = symbolize(name) # 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 the parameter names def self.parameters return [] unless defined?(@parameters) @parameters.collect { |klass| klass.name } end # Find the parameter class associated with a given parameter name. def self.paramclass(name) @paramhash[name] end # Return the property class associated with a name def self.propertybyname(name) @validproperties[name] end def self.validattr?(name) name = symbolize(name) 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 # does the name reflect a valid property? def self.validproperty?(name) name = symbolize(name) @validproperties.include?(name) && @validproperties[name] end # Return the list of validproperties def self.validproperties return {} unless defined?(@parameters) @validproperties.keys end # does the name reflect a valid 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 # 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 either the attribute alias or the attribute. def attr_alias(name) name = symbolize(name) if synonym = self.class.attr_alias(name) return synonym else return name end end # Are we deleting this resource? def deleting? obj = @parameters[:ensure] and obj.should == :absent end # Create a new property if it is valid but doesn't exist # Returns: 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 # # The name_var is the key_attribute in the case that there is only one. # def name_var key_attributes = self.class.key_attributes (key_attributes.length == 1) && key_attributes.first end # abstract accessing parameters and properties, and normalize # access to always be symbols, not strings # This returns a value, not an object. It returns the 'is' # value, but you can also specifically return 'is' and 'should' # values using 'object.is(:property)' or 'object.should(:property)'. def [](name) name = attr_alias(name) fail("Invalid parameter #{name}(#{name.inspect})") unless self.class.validattr?(name) if name == :name name = name_var 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 # Abstract setting parameters and properties, and normalize # access to always be symbols, not strings. This sets the 'should' # value on properties, and otherwise just sets the appropriate parameter. def []=(name,value) name = attr_alias(name) fail("Invalid parameter #{name}") unless self.class.validattr?(name) if name == :name name = name_var 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 => detail error = Puppet::Error.new("Parameter #{name} failed: #{detail}") error.set_backtrace(detail.backtrace) raise error end end nil end # remove a property from the object; useful in testing or in cleanup # when an error has been encountered def delete(attr) attr = symbolize(attr) if @parameters.has_key?(attr) @parameters.delete(attr) else raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end # iterate across the existing properties def eachproperty # properties is a private method properties.each { |property| yield property } end # Create a transaction event. Called by Transaction or by # a property. def event(options = {}) Puppet::Transaction::Event.new({:resource => self, :file => file, :line => line, :tags => tags}.merge(options)) end # Let the catalog determine whether a given cached value is # still valid or has expired. def expirer catalog end # retrieve the 'should' value for a specified property def should(name) name = attr_alias(name) (prop = @parameters[name] and prop.is_a?(Puppet::Property)) ? prop.should : nil end # Create the actual attribute instance. Requires either the attribute # name or class as the first argument, then an optional hash of # attributes to set during initialization. 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) } info "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 # return the value of a parameter def parameter(name) @parameters[name.to_sym] end def parameters @parameters.dup end # Is the named property defined? def propertydefined?(name) name = name.intern unless name.is_a? Symbol @parameters.include?(name) end # Return an actual property instance by name; to return the value, use 'resource[param]' # LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method, # this one should probably go away at some point. def property(name) (obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property)) ? obj : nil end # 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. 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 # Convert our object to a hash. This just includes properties. def to_hash rethash = {} @parameters.each do |name, obj| rethash[name] = obj.value end rethash end def type self.class.name end # Return a specific value for an attribute. def value(name) name = attr_alias(name) (obj = @parameters[name] and obj.respond_to?(:value)) ? obj.value : nil end def version return 0 unless catalog catalog.version end # Return all of the property objects, in the order specified in the # class. def properties self.class.properties.collect { |prop| @parameters[prop.name] }.compact end # Is this type's name isomorphic with the object? That is, if the # name conflicts, does it necessarily mean that the objects conflict? # Defaults to true. def self.isomorphic? if defined?(@isomorphic) return @isomorphic else return true end end def isomorphic? self.class.isomorphic? end # is the instance 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 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. # this is a retarded hack method to get around the difference between # component children and file children def self.depthfirst? @depthfirst end def depthfirst? self.class.depthfirst? end # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. 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. # Flush the provider, if it supports it. This is called by the # transaction. def flush self.provider.flush if self.provider and self.provider.respond_to?(:flush) end # if all contained objects are in sync, then we're in sync # FIXME I don't think this is used on the type instances any more, # it's really only used for testing 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.insync?(ensureis) and property.should == :absent + 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.insync?(propis) + 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 # retrieve the current value of all contained properties 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(type, 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 def retrieve_resource resource = retrieve resource = Resource.new(type, title, :parameters => resource) if resource.is_a? Hash resource end # Get a hash of the current properties. Returns a hash with # the actual property instance as the key and the current value # as the, um, 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 # Are we 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 def noop noop? end ############################### # Code related to managing resource instances. require 'puppet/transportable' # retrieve a named instance of the current type def self.[](name) raise "Global resource access is deprecated" @objects[name] || @aliases[name] end # add an instance by name to the class list of instances def self.[]=(name,object) raise "Global resource storage is deprecated" newobj = nil if object.is_a?(Puppet::Type) newobj = object else raise Puppet::DevError, "must pass a Puppet::Type object" end if exobj = @objects[name] and self.isomorphic? msg = "Object '#{newobj.class.name}[#{name}]' already exists" msg += ("in file #{object.file} at line #{object.line}") if exobj.file and exobj.line msg += ("and cannot be redefined in file #{object.file} at line #{object.line}") if object.file and object.line error = Puppet::Error.new(msg) raise error else #Puppet.info("adding %s of type %s to class list" % # [name,object.class]) @objects[name] = newobj end end # Create an alias. We keep these in a separate hash so that we don't encounter # the objects multiple times when iterating over them. def self.alias(name, obj) raise "Global resource aliasing is deprecated" if @objects.include?(name) unless @objects[name] == obj raise Puppet::Error.new( "Cannot create alias #{name}: object already exists" ) end end if @aliases.include?(name) unless @aliases[name] == obj raise Puppet::Error.new( "Object #{@aliases[name].name} already has alias #{name}" ) end end @aliases[name] = obj end # remove all of the instances of a single type def self.clear raise "Global resource removal is deprecated" if defined?(@objects) @objects.each do |name, obj| obj.remove(true) end @objects.clear end @aliases.clear if defined?(@aliases) end # Force users to call this, so that we can merge objects if # necessary. def self.create(args) # LAK:DEP Deprecation notice added 12/17/2008 Puppet.warning "Puppet::Type.create is deprecated; use Puppet::Type.new" new(args) end # remove a specified object def self.delete(resource) raise "Global resource removal is deprecated" return unless defined?(@objects) @objects.delete(resource.title) if @objects.include?(resource.title) @aliases.delete(resource.title) if @aliases.include?(resource.title) if @aliases.has_value?(resource) names = [] @aliases.each do |name, otherres| if otherres == resource names << name end end names.each { |name| @aliases.delete(name) } end end # iterate across each of the type's instances def self.each raise "Global resource iteration is deprecated" return unless defined?(@objects) @objects.each { |name,instance| yield instance } end # does the type have an object with the given name? def self.has_key?(name) raise "Global resource access is deprecated" @objects.has_key?(name) end # Retrieve all known instances. Either requires providers or must be overridden. 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| 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.warning "%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 new(:name => instance.name, :provider => instance, :audit => :all) end end.flatten.compact end # Return a list of one suitable provider per source, with the default provider first. def self.providers_by_source # Put the default provider first, then the rest of the suitable providers. sources = [] [defaultprovider, suitableprovider].flatten.uniq.collect do |provider| next if sources.include?(provider.source) sources << provider.source provider end.compact end # Convert a simple hash into a Resource instance. 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.name, title) [:catalog].each do |attribute| if value = hash[attribute] hash.delete(attribute) resource.send(attribute.to_s + "=", value) end end hash.each do |param, value| resource[param] = value end resource end # Create the path for logging and such. 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 "Boolean flag indicating whether work should actually be done." 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 "On what schedule the object should be managed. You must create a schedule object, and then reference the name of that object to use that for your schedule: schedule { daily: period => daily, range => \"2-4\" } exec { \"/usr/bin/apt-get update\": schedule => daily } The creation of the schedule object does not need to appear in the configuration before objects that use it." end newmetaparam(:audit) do desc "Audit specified attributes of resources over time, and report if any have changed. This attribute can be used to track changes to any resource over time, and can provide an audit trail of every change that happens on any given machine. Note that you cannot both audit and manage an attribute - managing it guarantees the value, and any changes already get logged." 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(:check) do desc "Audit specified attributes of resources over time, and report if any have changed. This parameter has been deprecated in favor of 'audit'." munge do |args| resource.warning "'check' attribute is deprecated; use 'audit' instead" resource[:audit] = args 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)." 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 "Creates an alias for the object. Puppet uses this internally when you provide a symbolic name: 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 name, and the library sets that as an alias for the file so the dependency lookup for `sshd` works. You can use this parameter yourself, but note that only the library can use these aliases; for instance, 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. See the [Language Tutorial](http://docs.puppetlabs.com/guides/language_tutorial.html) for more information. " 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. Tags are currently useful for things like applying a subset of a host's configuration: puppet agent --test --tags mytag This way, when you're testing a configuration you can run just the portion you're testing." munge do |tags| tags = [tags] unless tags.is_a? Array tags.each do |tag| @resource.tag(tag) end end end class RelationshipMetaparam < Puppet::Parameter class << self attr_accessor :direction, :events, :callback, :subclasses end @subclasses = [] def self.inherited(sub) @subclasses << sub end 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 def validate_relationship @value.each do |ref| unless @resource.catalog.resource(ref.to_s) description = self.class.direction == :in ? "dependency" : "dependent" fail "Could not find #{description} #{ref} for #{resource.ref}" end end end # Create edges from each of our relationships. :in # relationships are specified by the event-receivers, and :out # relationships are specified by the event generator. This # way 'source' and 'target' are consistent terms in both edges # and events -- that is, 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. 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 rel = Puppet::Relationship.new(source, target, subargs) end end end 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 objects that this object depends on. This is used purely for guaranteeing that changes to required objects happen before the dependent object. For instance: # Create the destination directory before you copy things down file { \"/usr/local/scripts\": ensure => directory } file { \"/usr/local/scripts/myscript\": source => \"puppet://server/module/myscript\", mode => 755, require => File[\"/usr/local/scripts\"] } Multiple dependencies can be specified by providing a comma-seperated list of resources, enclosed in square brackets: require => [ File[\"/usr/local\"], File[\"/usr/local/scripts\"] ] Note that Puppet will autorequire everything that it can, and there are hooks in place so that it's easy for resources to add new ways to autorequire objects, so if you think Puppet could be smarter here, let us know. In fact, the above code was redundant -- Puppet will autorequire any parent directories that are being managed; it will automatically realize that the parent directory should be created before the script is pulled down. Currently, exec resources will autorequire their CWD (if it is specified) plus any fully qualified paths that appear in the command. For instance, if you had an `exec` command that ran the `myscript` mentioned above, the above code that pulls the file down would be automatically listed as a requirement to the `exec` code, so that you would always be running againts the most recent version. " end newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) do desc "One or more objects that this object depends on. Changes in the subscribed to objects result in the dependent objects being refreshed (e.g., a service will get restarted). For instance: class nagios { file { \"/etc/nagios/nagios.conf\": source => \"puppet://server/module/nagios.conf\", alias => nagconf # just to make things easier for me } service { nagios: ensure => running, subscribe => File[nagconf] } } Currently the `exec`, `mount` and `service` type support refreshing. " end newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) do desc %{This parameter is the opposite of **require** -- it guarantees that the specified object is applied later than the specifying object: file { "/var/nagios/configuration": source => "...", recurse => true, before => Exec["nagios-rebuid"] } exec { "nagios-rebuild": command => "/usr/bin/make", cwd => "/var/nagios/configuration" } This will make sure all of the files are up to date before the make command is run.} end newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) do desc %{This parameter is the opposite of **subscribe** -- it sends events to the specified object: file { "/etc/sshd_config": source => "....", notify => Service[sshd] } service { sshd: ensure => running } This will restart the sshd service if the sshd config file changes.} end newmetaparam(:stage) do desc %{Which run stage a given resource should reside in. This just creates a dependency on or from the named milestone. For instance, saying that this is in the 'bootstrap' stage creates a dependency on the 'bootstrap' milestone. By default, all classes get directly added to the 'main' stage. You can create new stages as resources: stage { [pre, post]: } To order stages, use standard relationships: stage { pre: before => Stage[main] } Or use the new relationship syntax: Stage[pre] -> Stage[main] -> Stage[post] Then use the new class parameters to specify a stage: class { foo: stage => pre } Stages can only be set on classes, not individual resources. This will fail: file { '/foo': stage => pre, ensure => file } } 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 attr_reader :provider # the Type class attribute accessors class << self attr_accessor :providerloader attr_writer :defaultprovider end # Find the default provider. def self.defaultprovider unless @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 } retval = nil 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}" ) retval = defaults.shift elsif defaults.length == 1 retval = defaults.shift else raise Puppet::DevError, "Could not find a default provider for #{self.name}" end @defaultprovider = retval end @defaultprovider end def self.provider_hash_by_type(type) @provider_hashes ||= {} @provider_hashes[type] ||= {} end def self.provider_hash Puppet::Type.provider_hash_by_type(self.name) end # Retrieve a provider by name. def self.provider(name) name = Puppet::Util.symbolize(name) # If we don't have it yet, try loading it. @providerloader.load(name) unless provider_hash.has_key?(name) provider_hash[name] end # Just list all of the providers. def self.providers provider_hash.keys end def self.validprovider?(name) name = Puppet::Util.symbolize(name) (provider_hash.has_key?(name) && provider_hash[name].suitable?) end # Create a new provider of a type. This method must be called # directly on the type that it's implementing. def self.provide(name, options = {}, &block) name = Puppet::Util.symbolize(name) if obj = provider_hash[name] Puppet.debug "Reloading #{name} #{self.name} provider" unprovide(name) 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 # Make sure we have a :provider parameter defined. Only gets called if there # are providers. def self.providify return if @paramhash.has_key? :provider newparam(:provider) do desc "The specific backend for #{self.name.to_s} to use. You will seldom need to specify this -- Puppet will usually discover the appropriate provider for your platform." # This is so we can refer back to the type to get a list of # providers for documentation. class << self attr_accessor :parenttype end # We need to add documentation for each provider. def self.doc @doc + " Available providers are:\n\n" + parenttype.providers.sort { |a,b| a.to_s <=> b.to_s }.collect { |i| "* **#{i}**: #{parenttype().provider(i).doc}" }.join("\n") end defaultto { @resource.class.defaultprovider.name } 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 provider = @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 def self.unprovide(name) if provider_hash.has_key? name rmclass( name, :hash => provider_hash, :prefix => "Provider" ) if @defaultprovider and @defaultprovider.name == name @defaultprovider = nil end end end # Return an array of all of the 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 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. # Specify a block for generating a list of objects to autorequire. This # makes it so that you don't have to manually specify things that you clearly # require. def self.autorequire(name, &block) @autorequires ||= {} @autorequires[name] = block end # Yield each of those autorequires in turn, yo. def self.eachautorequire @autorequires ||= {} @autorequires.each { |type, block| yield(type, block) } end # Figure out of there are any objects we can automatically add as # dependencies. 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 typeobj = 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| obj = nil # 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 # Build the dependencies associated with an individual object. 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 # Define the initial list of tags. def tags=(list) tag(self.class.name) tag(*list) end # 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. # 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. attr_writer :title attr_writer :noop include Enumerable # class methods dealing with Type management public # the Type class attribute accessors class << self attr_reader :name attr_accessor :self_refresh include Enumerable, Puppet::Util::ClassGen include Puppet::MetaType::Manager include Puppet::Util include Puppet::Util::Logging end # all of the variables that must be initialized for each subclass def self.initvars # all of the instances of this class @objects = Hash.new @aliases = Hash.new @defaults = {} @parameters ||= [] @validproperties = {} @properties = [] @parameters = [] @paramhash = {} @attr_aliases = {} @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 def self.to_s if defined?(@name) "Puppet::Type::#{@name.to_s.capitalize}" else super end end # Create a block to validate that our object is set up entirely. This will # be run before the object is operated on. def self.validate(&block) define_method(:validate, &block) #@validate = block end # The catalog that this resource is stored in. attr_accessor :catalog # is the resource exported attr_accessor :exported # is the resource virtual (it should not :-)) attr_accessor :virtual # create a log at specified level 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 attr_reader :original_parameters # initialize the type instance def initialize(resource) raise Puppet::DevError, "Got TransObject instead of Resource or hash" if resource.is_a?(Puppet::TransObject) 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) self.validate if self.respond_to?(:validate) end private # Set our resource's name. def set_name(hash) self[name_var] = hash.delete(name_var) if name_var end # Set all of the parameters from a hash, in the appropriate order. 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 # Set up all of our autorequires. def finish # 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 # For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. def name self[:name] end # Look up our parent in the catalog, if we have one. def parent return nil unless catalog unless defined?(@parent) if parents = catalog.adjacent(self, :direction => :in) # We should never have more than one parent, so let's just ignore # it if we happen to. @parent = parents.shift else @parent = nil end end @parent end # Return the "type[name]" style reference. def ref "#{self.class.name.to_s.capitalize}[#{self.title}]" end def self_refresh? self.class.self_refresh end # Mark that we're purging. def purging @purging = true end # Is this resource being purged? Used by transactions to forbid # deletion when there are dependencies. def purging? if defined?(@purging) @purging else false end end # Retrieve the title of an object. If no title was set separately, # then use the object's name. 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 # convert to a string def to_s self.ref end # Convert to a transportable object def to_trans(ret = true) trans = TransObject.new(self.title, self.class.name) values = retrieve_resource values.each do |name, value| name = name.name if name.respond_to? :name trans[name] = value end @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) trans[name] = param.value end trans.tags = self.tags # FIXME I'm currently ignoring 'parent' and 'path' trans end def to_resource # this 'type instance' versus 'resource' distinction seems artificial # I'd like to see it collapsed someday ~JW self.to_trans.to_resource end def virtual?; !!@virtual; end def exported?; !!@exported; end end end require 'puppet/provider' # Always load these types. require 'puppet/type/component' diff --git a/lib/puppet/type/cron.rb b/lib/puppet/type/cron.rb index 76399d693..4b18e71f9 100755 --- a/lib/puppet/type/cron.rb +++ b/lib/puppet/type/cron.rb @@ -1,414 +1,410 @@ require 'etc' require 'facter' require 'puppet/util/filetype' Puppet::Type.newtype(:cron) do @doc = "Installs and manages cron jobs. All fields except the command and the user are optional, although specifying no periodic fields would result in the command being executed every minute. While the name of the cron job is not part of the actual job, it is used by Puppet to store and retrieve it. If you specify a cron job that matches an existing job in every way except name, then the jobs will be considered equivalent and the new name will be permanently associated with that job. Once this association is made and synced to disk, you can then manage the job normally (e.g., change the schedule of the job). Example: cron { logrotate: command => \"/usr/sbin/logrotate\", user => root, hour => 2, minute => 0 } Note that all cron values can be specified as an array of values: cron { logrotate: command => \"/usr/sbin/logrotate\", user => root, hour => [2, 4] } Or using ranges, or the step syntax `*/2` (although there's no guarantee that your `cron` daemon supports it): cron { logrotate: command => \"/usr/sbin/logrotate\", user => root, hour => ['2-4'], minute => '*/10' } " ensurable # A base class for all of the Cron parameters, since they all have # similar argument checking going on. class CronParam < Puppet::Property class << self attr_accessor :boundaries, :default end # We have to override the parent method, because we consume the entire # "should" array def insync?(is) - if @should - self.is_to_s(is) == self.should_to_s - else - true - end + self.is_to_s(is) == self.should_to_s end # A method used to do parameter input handling. Converts integers # in string form to actual integers, and returns the value if it's # an integer or false if it's just a normal string. def numfix(num) if num =~ /^\d+$/ return num.to_i elsif num.is_a?(Integer) return num else return false end end # Verify that a number is within the specified limits. Return the # number if it is, or false if it is not. def limitcheck(num, lower, upper) (num >= lower and num <= upper) && num end # Verify that a value falls within the specified array. Does case # insensitive matching, and supports matching either the entire word # or the first three letters of the word. def alphacheck(value, ary) tmp = value.downcase # If they specified a shortened version of the name, then see # if we can lengthen it (e.g., mon => monday). if tmp.length == 3 ary.each_with_index { |name, index| if name =~ /#{tmp}/i return index end } else return ary.index(tmp) if ary.include?(tmp) end false end def should_to_s(newvalue = @should) if newvalue newvalue = [newvalue] unless newvalue.is_a?(Array) if self.name == :command or newvalue[0].is_a? Symbol newvalue[0] else newvalue.join(",") end else nil end end def is_to_s(currentvalue = @is) if currentvalue return currentvalue unless currentvalue.is_a?(Array) if self.name == :command or currentvalue[0].is_a? Symbol currentvalue[0] else currentvalue.join(",") end else nil end end def should if @should and @should[0] == :absent :absent else @should end end def should=(ary) super @should.flatten! end # The method that does all of the actual parameter value # checking; called by all of the +param=+ methods. # Requires the value, type, and bounds, and optionally supports # a boolean of whether to do alpha checking, and if so requires # the ary against which to do the checking. munge do |value| # Support 'absent' as a value, so that they can remove # a value if value == "absent" or value == :absent return :absent end # Allow the */2 syntax if value =~ /^\*\/[0-9]+$/ return value end # Allow ranges if value =~ /^[0-9]+-[0-9]+$/ return value end # Allow ranges + */2 if value =~ /^[0-9]+-[0-9]+\/[0-9]+$/ return value end if value == "*" return :absent end return value unless self.class.boundaries lower, upper = self.class.boundaries retval = nil if num = numfix(value) retval = limitcheck(num, lower, upper) elsif respond_to?(:alpha) # If it has an alpha method defined, then we check # to see if our value is in that list and if so we turn # it into a number retval = alphacheck(value, alpha) end if retval return retval.to_s else self.fail "#{value} is not a valid #{self.class.name}" end end end # Somewhat uniquely, this property does not actually change anything -- it # just calls +@resource.sync+, which writes out the whole cron tab for # the user in question. There is no real way to change individual cron # jobs without rewriting the entire cron file. # # Note that this means that managing many cron jobs for a given user # could currently result in multiple write sessions for that user. newproperty(:command, :parent => CronParam) do desc "The command to execute in the cron job. The environment provided to the command varies by local system rules, and it is best to always provide a fully qualified command. The user's profile is not sourced when the command is run, so if the user's environment is desired it should be sourced manually. All cron parameters support `absent` as a value; this will remove any existing values for that field." def retrieve return_value = super return_value = return_value[0] if return_value && return_value.is_a?(Array) return_value end def should if @should if @should.is_a? Array @should[0] else devfail "command is not an array" end else nil end end end newproperty(:special) do desc "Special schedules" def specials %w{reboot yearly annually monthly weekly daily midnight hourly} end validate do |value| raise ArgumentError, "Invalid special schedule #{value.inspect}" unless specials.include?(value) end end newproperty(:minute, :parent => CronParam) do self.boundaries = [0, 59] desc "The minute at which to run the cron job. Optional; if specified, must be between 0 and 59, inclusive." end newproperty(:hour, :parent => CronParam) do self.boundaries = [0, 23] desc "The hour at which to run the cron job. Optional; if specified, must be between 0 and 23, inclusive." end newproperty(:weekday, :parent => CronParam) do def alpha %w{sunday monday tuesday wednesday thursday friday saturday} end self.boundaries = [0, 7] desc "The weekday on which to run the command. Optional; if specified, must be between 0 and 7, inclusive, with 0 (or 7) being Sunday, or must be the name of the day (e.g., Tuesday)." end newproperty(:month, :parent => CronParam) do def alpha %w{january february march april may june july august september october november december} end self.boundaries = [1, 12] desc "The month of the year. Optional; if specified must be between 1 and 12 or the month name (e.g., December)." end newproperty(:monthday, :parent => CronParam) do self.boundaries = [1, 31] desc "The day of the month on which to run the command. Optional; if specified, must be between 1 and 31." end newproperty(:environment) do desc "Any environment settings associated with this cron job. They will be stored between the header and the job in the crontab. There can be no guarantees that other, earlier settings will not also affect a given cron job. Also, Puppet cannot automatically determine whether an existing, unmanaged environment setting is associated with a given cron job. If you already have cron jobs with environment settings, then Puppet will keep those settings in the same place in the file, but will not associate them with a specific job. Settings should be specified exactly as they should appear in the crontab, e.g., `PATH=/bin:/usr/bin:/usr/sbin`." validate do |value| unless value =~ /^\s*(\w+)\s*=\s*(.*)\s*$/ or value == :absent or value == "absent" raise ArgumentError, "Invalid environment setting #{value.inspect}" end end def insync?(is) if is.is_a? Array return is.sort == @should.sort else return is == @should end end def is_to_s(newvalue) if newvalue if newvalue.is_a?(Array) newvalue.join(",") else newvalue end else nil end end def should @should end def should_to_s(newvalue = @should) if newvalue newvalue.join(",") else nil end end end newparam(:name) do desc "The symbolic name of the cron job. This name is used for human reference only and is generated automatically for cron jobs found on the system. This generally won't matter, as Puppet will do its best to match existing cron jobs against specified jobs (and Puppet adds a comment to cron jobs it adds), but it is at least possible that converting from unmanaged jobs to managed jobs might require manual intervention." isnamevar end newproperty(:user) do desc "The user to run the command as. This user must be allowed to run cron jobs, which is not currently checked by Puppet. The user defaults to whomever Puppet is running as." defaultto { Etc.getpwuid(Process.uid).name || "root" } end newproperty(:target) do desc "Where the cron job should be stored. For crontab-style entries this is the same as the user and defaults that way. Other providers default accordingly." defaultto { if provider.is_a?(@resource.class.provider(:crontab)) if val = @resource.should(:user) val else raise ArgumentError, "You must provide a user with crontab entries" end elsif provider.class.ancestors.include?(Puppet::Provider::ParsedFile) provider.class.default_target else nil end } end # We have to reorder things so that :provide is before :target attr_accessor :uid def value(name) name = symbolize(name) ret = nil if obj = @parameters[name] ret = obj.should ret ||= obj.retrieve if ret == :absent ret = nil end end unless ret case name when :command devfail "No command, somehow" when :special # nothing else #ret = (self.class.validproperty?(name).default || "*").to_s ret = "*" end end ret end end diff --git a/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb index 606888c75..daa49e223 100755 --- a/lib/puppet/type/exec.rb +++ b/lib/puppet/type/exec.rb @@ -1,679 +1,649 @@ module Puppet newtype(:exec) do include Puppet::Util::Execution require 'timeout' @doc = "Executes external commands. It is critical that all commands executed using this mechanism can be run multiple times without harm, i.e., they are *idempotent*. One useful way to create idempotent commands is to use the checks like `creates` to avoid running the command unless some condition is met. - Note also that you can restrict an `exec` to only run when it receives + Note that you can restrict an `exec` to only run when it receives events by using the `refreshonly` parameter; this is a useful way to have your configuration respond to events with arbitrary commands. - It is worth noting that `exec` is special, in that it is not - currently considered an error to have multiple `exec` instances - with the same name. This was done purely because it had to be this - way in order to get certain functionality, but it complicates things. - In particular, you will not be able to use `exec` instances that - share their commands with other instances as a dependency, since - Puppet has no way of knowing which instance you mean. - - For example: - - # defined in the production class - exec { \"make\": - cwd => \"/prod/build/dir\", - path => \"/usr/bin:/usr/sbin:/bin\" - } - - . etc. . - - # defined in the test class - exec { \"make\": - cwd => \"/test/build/dir\", - path => \"/usr/bin:/usr/sbin:/bin\" - } - - Any other type would throw an error, complaining that you had - the same instance being managed in multiple places, but these are - obviously different images, so `exec` had to be treated specially. - - It is recommended to avoid duplicate names whenever possible. - - Note that if an `exec` receives an event from another resource, + Note also that if an `exec` receives an event from another resource, it will get executed again (or execute the command specified in `refresh`, if there is one). There is a strong tendency to use `exec` to do whatever work Puppet can't already do; while this is obviously acceptable (and unavoidable) in the short term, it is highly recommended to migrate work from `exec` to native Puppet types as quickly as possible. If you find that you are doing a lot of work with `exec`, please at least notify us at Puppet Labs what you are doing, and hopefully we can work with you to get a native resource type for the work you are doing." require 'open3' # Create a new check mechanism. It's basically just a parameter that # provides one extra 'check' method. def self.newcheck(name, &block) @checks ||= {} check = newparam(name, &block) @checks[name] = check end def self.checks @checks.keys end newproperty(:returns, :array_matching => :all, :event => :executed_command) do |property| include Puppet::Util::Execution munge do |value| value.to_s end def event_name :executed_command end defaultto "0" attr_reader :output desc "The expected return code(s). An error will be returned if the executed command returns something else. Defaults to 0. Can be specified as an array of acceptable return codes or a single value." # Make output a bit prettier def change_to_s(currentvalue, newvalue) "executed successfully" end # First verify that all of our checks pass. def retrieve # Default to somethinng if @resource.check return :notrun else return self.should end end # Actually execute the command. def sync olddir = nil # We need a dir to change to, even if it's just the cwd dir = self.resource[:cwd] || Dir.pwd event = :executed_command tries = self.resource[:tries] try_sleep = self.resource[:try_sleep] begin tries.times do |try| # Only add debug messages for tries > 1 to reduce log spam. debug("Exec try #{try+1}/#{tries}") if tries > 1 @output, @status = @resource.run(self.resource[:command]) break if self.should.include?(@status.exitstatus.to_s) if try_sleep > 0 and tries > 1 debug("Sleeping for #{try_sleep} seconds between tries") sleep try_sleep end end rescue Timeout::Error self.fail "Command exceeded timeout" % value.inspect end if log = @resource[:logoutput] case log when :true log = @resource[:loglevel] when :on_failure unless self.should.include?(@status.exitstatus.to_s) log = @resource[:loglevel] else log = :false end end unless log == :false @output.split(/\n/).each { |line| self.send(log, line) } end end unless self.should.include?(@status.exitstatus.to_s) self.fail("#{self.resource[:command]} returned #{@status.exitstatus} instead of one of [#{self.should.join(",")}]") end event end end newparam(:command) do isnamevar desc "The actual command to execute. Must either be fully qualified or a search path for the command must be provided. If the command succeeds, any output produced will be logged at the instance's normal log level (usually `notice`), but if the command fails (meaning its return code does not match the specified code) then any output is logged at the `err` log level." end newparam(:path) do desc "The search path used for command execution. Commands must be fully qualified if no path is specified. Paths can be specified as an array or as a colon-separated list." # Support both arrays and colon-separated fields. def value=(*values) @value = values.flatten.collect { |val| if val =~ /;/ # recognize semi-colon separated paths val.split(";") elsif val =~ /^\w:[^:]*$/ # heuristic to avoid splitting a driveletter away val else val.split(":") end }.flatten end end newparam(:user) do desc "The user to run the command as. Note that if you use this then any error output is not currently captured. This is because of a bug within Ruby. If you are using Puppet to create this user, the exec will automatically require the user, as long as it is specified by name." # Most validation is handled by the SUIDManager class. validate do |user| self.fail "Only root can execute commands as other users" unless Puppet.features.root? end end newparam(:group) do desc "The group to run the command as. This seems to work quite haphazardly on different platforms -- it is a platform issue not a Ruby or Puppet one, since the same variety exists when running commnands as different users in the shell." # Validation is handled by the SUIDManager class. end newparam(:cwd) do desc "The directory from which to run the command. If this directory does not exist, the command will fail." validate do |dir| unless dir =~ /^#{File::SEPARATOR}/ self.fail("CWD must be a fully qualified path") end end munge do |dir| dir = dir[0] if dir.is_a?(Array) dir end end newparam(:logoutput) do desc "Whether to log output. Defaults to logging output at the loglevel for the `exec` resource. Use *on_failure* to only log the output when the command reports an error. Values are **true**, *false*, *on_failure*, and any legal log level." newvalues(:true, :false, :on_failure) end newparam(:refresh) do desc "How to refresh this command. By default, the exec is just called again when it receives an event from another resource, but this parameter allows you to define a different command for refreshing." validate do |command| @resource.validatecmd(command) end end newparam(:env) do desc "This parameter is deprecated. Use 'environment' instead." munge do |value| warning "'env' is deprecated on exec; use 'environment' instead." resource[:environment] = value end end newparam(:environment) do desc "Any additional environment variables you want to set for a command. Note that if you use this to set PATH, it will override the `path` attribute. Multiple environment variables should be specified as an array." validate do |values| values = [values] unless values.is_a? Array values.each do |value| unless value =~ /\w+=/ raise ArgumentError, "Invalid environment setting '#{value}'" end end end end newparam(:timeout) do desc "The maximum time the command should take. If the command takes longer than the timeout, the command is considered to have failed and will be stopped. Use any negative number to disable the timeout. The time is specified in seconds." munge do |value| value = value.shift if value.is_a?(Array) if value.is_a?(String) unless value =~ /^[-\d.]+$/ raise ArgumentError, "The timeout must be a number." end Float(value) else value end end defaultto 300 end newparam(:tries) do desc "The number of times execution of the command should be tried. Defaults to '1'. This many attempts will be made to execute the command until an acceptable return code is returned. Note that the timeout paramater applies to each try rather than to the complete set of tries." munge do |value| if value.is_a?(String) unless value =~ /^[\d]+$/ raise ArgumentError, "Tries must be an integer" end value = Integer(value) end raise ArgumentError, "Tries must be an integer >= 1" if value < 1 value end defaultto 1 end newparam(:try_sleep) do desc "The time to sleep in seconds between 'tries'." munge do |value| if value.is_a?(String) unless value =~ /^[-\d.]+$/ raise ArgumentError, "try_sleep must be a number" end value = Float(value) end raise ArgumentError, "try_sleep cannot be a negative number" if value < 0 value end defaultto 0 end newcheck(:refreshonly) do desc "The command should only be run as a refresh mechanism for when a dependent object is changed. It only makes sense to use this option when this command depends on some other object; it is useful for triggering an action: # Pull down the main aliases file file { \"/etc/aliases\": source => \"puppet://server/module/aliases\" - } + } # Rebuild the database, but only when the file changes exec { newaliases: path => [\"/usr/bin\", \"/usr/sbin\"], subscribe => File[\"/etc/aliases\"], refreshonly => true } Note that only `subscribe` and `notify` can trigger actions, not `require`, so it only makes sense to use `refreshonly` with `subscribe` or `notify`." newvalues(:true, :false) # We always fail this test, because we're only supposed to run # on refresh. def check(value) # We have to invert the values. if value == :true false else true end end end newcheck(:creates) do desc "A file that this command creates. If this parameter is provided, then the command will only be run if the specified file does not exist: exec { \"tar xf /my/tar/file.tar\": cwd => \"/var/tmp\", creates => \"/var/tmp/myfile\", path => [\"/usr/bin\", \"/usr/sbin\"] } " # FIXME if they try to set this and fail, then we should probably # fail the entire exec, right? validate do |files| files = [files] unless files.is_a? Array files.each do |file| self.fail("'creates' must be set to a fully qualified path") unless file unless file =~ %r{^#{File::SEPARATOR}} self.fail "'creates' files must be fully qualified." end end end # If the file exists, return false (i.e., don't run the command), # else return true def check(value) ! FileTest.exists?(value) end end newcheck(:unless) do desc "If this parameter is set, then this `exec` will run unless the command returns 0. For example: exec { \"/bin/echo root >> /usr/lib/cron/cron.allow\": path => \"/usr/bin:/usr/sbin:/bin\", unless => \"grep root /usr/lib/cron/cron.allow 2>/dev/null\" } This would add `root` to the cron.allow file (on Solaris) unless `grep` determines it's already there. Note that this command follows the same rules as the main command, which is to say that it must be fully qualified if the path is not set. " validate do |cmds| cmds = [cmds] unless cmds.is_a? Array cmds.each do |cmd| @resource.validatecmd(cmd) end end # Return true if the command does not return 0. def check(value) begin output, status = @resource.run(value, true) rescue Timeout::Error err "Check #{value.inspect} exceeded timeout" return false end status.exitstatus != 0 end end newcheck(:onlyif) do desc "If this parameter is set, then this `exec` will only run if the command returns 0. For example: exec { \"logrotate\": path => \"/usr/bin:/usr/sbin:/bin\", onlyif => \"test `du /var/log/messages | cut -f1` -gt 100000\" } This would run `logrotate` only if that test returned true. Note that this command follows the same rules as the main command, which is to say that it must be fully qualified if the path is not set. Also note that onlyif can take an array as its value, e.g.: onlyif => [\"test -f /tmp/file1\", \"test -f /tmp/file2\"] This will only run the exec if /all/ conditions in the array return true. " validate do |cmds| cmds = [cmds] unless cmds.is_a? Array cmds.each do |cmd| @resource.validatecmd(cmd) end end # Return true if the command returns 0. def check(value) begin output, status = @resource.run(value, true) rescue Timeout::Error err "Check #{value.inspect} exceeded timeout" return false end status.exitstatus == 0 end end # Exec names are not isomorphic with the objects. @isomorphic = false validate do validatecmd(self[:command]) end # FIXME exec should autorequire any exec that 'creates' our cwd autorequire(:file) do reqs = [] # Stick the cwd in there if we have it reqs << self[:cwd] if self[:cwd] self[:command].scan(/^(#{File::SEPARATOR}\S+)/) { |str| reqs << str } self[:command].scan(/^"([^"]+)"/) { |str| reqs << str } [:onlyif, :unless].each { |param| next unless tmp = self[param] tmp = [tmp] unless tmp.is_a? Array tmp.each do |line| # And search the command line for files, adding any we # find. This will also catch the command itself if it's # fully qualified. It might not be a bad idea to add # unqualified files, but, well, that's a bit more annoying # to do. reqs += line.scan(%r{(#{File::SEPARATOR}\S+)}) end } # For some reason, the += isn't causing a flattening reqs.flatten! reqs end autorequire(:user) do # Autorequire users if they are specified by name if user = self[:user] and user !~ /^\d+$/ user end end def self.instances [] end # Verify that we pass all of the checks. The argument determines whether # we skip the :refreshonly check, which is necessary because we now check # within refresh def check(refreshing = false) self.class.checks.each { |check| next if refreshing and check == :refreshonly if @parameters.include?(check) val = @parameters[check].value val = [val] unless val.is_a? Array val.each do |value| return false unless @parameters[check].check(value) end end } true end # Verify that we have the executable def checkexe(cmd) exe = extractexe(cmd) if self[:path] if Puppet.features.posix? and !File.exists?(exe) withenv :PATH => self[:path].join(File::PATH_SEPARATOR) do exe = which(exe) || raise(ArgumentError,"Could not find command '#{exe}'") end elsif Puppet.features.microsoft_windows? and !File.exists?(exe) self[:path].each do |path| [".exe", ".ps1", ".bat", ".com", ""].each do |extension| file = File.join(path, exe+extension) return if File.exists?(file) end end end end raise ArgumentError, "Could not find executable '#{exe}'" unless FileTest.exists?(exe) unless FileTest.executable?(exe) raise ArgumentError, "'#{exe}' is not executable" end end def output if self.property(:returns).nil? return nil else return self.property(:returns).output end end # Run the command, or optionally run a separately-specified command. def refresh if self.check(true) if cmd = self[:refresh] self.run(cmd) else self.property(:returns).sync end end end # Run a command. def run(command, check = false) output = nil status = nil dir = nil checkexe(command) if dir = self[:cwd] unless File.directory?(dir) if check dir = nil else self.fail "Working directory '#{dir}' does not exist" end end end dir ||= Dir.pwd if check debug "Executing check '#{command}'" else debug "Executing '#{command}'" end begin # Do our chdir Dir.chdir(dir) do environment = {} environment[:PATH] = self[:path].join(":") if self[:path] if envlist = self[:environment] envlist = [envlist] unless envlist.is_a? Array envlist.each do |setting| if setting =~ /^(\w+)=((.|\n)+)$/ name = $1 value = $2 if environment.include? name warning( "Overriding environment setting '#{name}' with '#{value}'" ) end environment[name] = value else warning "Cannot understand environment setting #{setting.inspect}" end end end withenv environment do Timeout::timeout(self[:timeout]) do output, status = Puppet::Util::SUIDManager.run_and_capture( [command], self[:user], self[:group] ) end # The shell returns 127 if the command is missing. if status.exitstatus == 127 raise ArgumentError, output end end end rescue Errno::ENOENT => detail self.fail detail.to_s end return output, status end def validatecmd(cmd) exe = extractexe(cmd) # if we're not fully qualified, require a path - self.fail "'#{cmd}' is both unqualifed and specified no search path" if File.expand_path(exe) != exe and self[:path].nil? + self.fail "'#{cmd}' is not qualified and no path was specified. Please qualify the command or specify a path." if File.expand_path(exe) != exe and self[:path].nil? end def extractexe(cmd) # easy case: command was quoted if cmd =~ /^"([^"]+)"/ $1 else cmd.split(/ /)[0] end end end end diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index eee948cd5..cbb51bbed 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,801 +1,791 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'puppet/network/handler' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/network/client' require 'puppet/util/backups' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups @doc = "Manages local files, including setting ownership and permissions, creation of both files and directories, and retrieving entire files from remote servers. As Puppet matures, it expected that the `file` resource will be used less and less to manage content, and instead native resources will be used to do so. If you find that you are often copying files in from a central location, rather than using native resources, please contact Puppet Labs and we can hopefully work with you to develop a native resource to support what you are doing." def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path, lambda{|x| x} ] ] ] ] end newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| # accept various path syntaxes: lone slash, posix, win32, unc - unless (Puppet.features.posix? and (value =~ /^\/$/ or value =~ /^\/[^\/]/)) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/)) + unless (Puppet.features.posix? and value =~ /^\//) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/)) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end # convert the current path in an index into the collection and the last # path name. The aim is to use less storage for all common paths in a hierarchy munge do |value| path, name = File.split(value.gsub(/\/+/,'/')) { :index => Puppet::FileCollection.collection.index(path), :name => name } end # and the reverse unmunge do |value| basedir = Puppet::FileCollection.collection.path(value[:index]) # a lone slash as :name indicates a root dir on windows if value[:name] == '/' basedir else File.join( basedir, value[:name] ) end end end newparam(:backup) do desc "Whether files should be backed up before being replaced. The preferred method of backing files up is via a `filebucket`, which stores files by their MD5 sums and allows easy retrieval without littering directories with backups. You can specify a local filebucket or a network-accessible server-based filebucket by setting `backup => bucket-name`. Alternatively, if you specify any value that begins with a `.` (e.g., `.puppet-bak`), then Puppet will use copy the file in the same directory with that value as the extension of the backup. Setting `backup => false` disables all backups of the file in question. Puppet automatically creates a local filebucket named `puppet` and defaults to backing up there. To use a server-based filebucket, you must specify one in your configuration filebucket { main: server => puppet } The `puppet master` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the benefits of using a filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, and you can restore any given file manually, no matter how old. Eventually, transactional support will be able to automatically restore filebucketed files. " 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 and how deeply to do recursive management." newvalues(:true, :false, :inf, :remote, /^[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 :remote; :remote when Integer, Fixnum, Bignum self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true when /^\d+$/ self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" value = Integer(value) # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true else self.fail "Invalid recurse value #{value.inspect}" end end end newparam(:recurselimit) do desc "How deeply to do recursive management." 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) do desc "Whether or not to replace a file that is sourced but exists. This is useful for using file sources purely for initialization." newvalues(:true, :false) aliasvalue(:yes, :true) aliasvalue(:no, :false) defaultto :true end newparam(:force, :boolean => true) do desc "Force the file operation. Currently only used when replacing directories with links." newvalues(:true, :false) 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) do desc "Whether unmanaged files should be purged. If you have a filebucket configured the purged files will be uploaded, but if you do not, this will destroy data. Only use this option for generated files unless you really know what you are doing. This option only makes sense when recursively managing directories. Note that when using `purge` with `source`, Puppet will purge any files that are not on the remote system." defaultto :false newvalues(:true, :false) end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter is only used in recursive copies; by default, the first valid source is the only one used as a recursive source, but if this parameter is set to `all`, then all valid sources will have all of their contents copied to the local host, and for sources that have the same file, the source earlier in the list will be used." defaultto :first newvalues(:first, :all) end # Autorequire any parent directories. autorequire(:file) do basedir = File.dirname(self[:path]) if basedir != self[:path] basedir else nil end 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 - count = 0 + creator_count = 0 CREATORS.each do |param| - count += 1 if self.should(param) + creator_count += 1 if self.should(param) end - count += 1 if @parameters.include?(:source) - self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if count > 1 + 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] end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end - # List files, but only one level deep. - def self.instances(base = "/") - return [] unless FileTest.directory?(base) - - files = [] - Dir.entries(base).reject { |e| - e == "." or e == ".." - }.each do |name| - path = File.join(base, name) - if obj = self[path] - obj[:audit] = :all - files << obj - else - files << self.new( - :name => path, :audit => :all - ) - end - end - files + def self.instances(base = '/') + return self.new(:name => base, :recurse => true, :recurselimit => 1, :audit => :all).recurse_local.values end @depthfirst = false # 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 # 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 #recurse.reject do |resource| # catalog.resource(:file, resource[:path]) #end.each do |child| # catalog.add_resource child # catalog.relationship_graph.add_edge self, child #end 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 = nil 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] = :symlink elsif self[:content] self[:ensure] = :file end end @stat = nil 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 # Should we be purging? def purge? @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") 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 = {} children = recurse_local if self[:recurse] != :remote 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? 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) 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? return false unless @parameters.include?(:recurse) val = @parameters[:recurse].value !!(val and (val == true or val == :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 # 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 children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file" children[meta.relative_path].parameter(:source).metadata = meta end children end def perform_recursion(path) Puppet::FileServing::Metadata.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 ) end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat self.fail "Could not back up; will not replace" unless perform_backup unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory" if self[:force] == :true debug "Removing existing directory for replacement with #{should}" FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" end when "link", "file" debug "Removing existing #{s.ftype} for replacement with #{should}" File.unlink(self[:path]) else self.fail "Could not back up files of type #{s.ftype}" end expire 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). cached_attr(:stat) do 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 path = self[:path] begin File.send(method, self[:path]) rescue Errno::ENOENT => error return nil rescue Errno::EACCES => error warning "Could not stat; permission denied" return nil end end # We have to hack this just a little bit, because otherwise we'll get # an error when the target and the contents are created as properties on # the far side. def to_trans(retrieve = true) obj = super obj.delete(:target) if obj[:target] == :notlink obj 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) use_temporary_file = write_temporary_file? if use_temporary_file path = "#{self[:path]}.puppettmp_#{rand(10000)}" path = "#{self[:path]}.puppettmp_#{rand(10000)}" while File.exists?(path) or File.symlink?(path) else path = self[:path] end mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 mode_int = mode ? mode.to_i(8) : nil content_checksum = Puppet::Util.withumask(umask) { File.open(path, 'w', mode_int ) { |f| write_content(f) } } # And put our new file in place if use_temporary_file # This is only not true when our file is empty. begin fail_if_checksum_is_wrong(path, content_checksum) if validate_checksum? File.rename(path, self[:path]) rescue => detail fail "Could not rename temporary file #{path} to #{self[:path]}: #{detail}" ensure # Make sure the created file gets removed File.unlink(path) if FileTest.exists?(path) end end # make sure all of the modes are actually correct property_fix end private # 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 private 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) || (s = @parameters[:source] and 1) 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 expire currentvalue = thing.retrieve - thing.sync unless thing.insync?(currentvalue) + 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/file/checksum.rb b/lib/puppet/type/file/checksum.rb index 732460738..5586b1383 100755 --- a/lib/puppet/type/file/checksum.rb +++ b/lib/puppet/type/file/checksum.rb @@ -1,33 +1,33 @@ require 'puppet/util/checksums' # Specify which checksum algorithm to use when checksumming # files. Puppet::Type.type(:file).newparam(:checksum) do include Puppet::Util::Checksums desc "The checksum type to use when checksumming a file. The default checksum parameter, if checksums are enabled, is md5." - newvalues "md5", "md5lite", "timestamp", "mtime", "time", "none" + newvalues "md5", "md5lite", "mtime", "ctime", "none" defaultto :md5 def sum(content) type = value || :md5 # because this might be called before defaults are set "{#{type}}" + send(type, content) end def sum_file(path) type = value || :md5 # because this might be called before defaults are set method = type.to_s + "_file" "{#{type}}" + send(method, path).to_s end def sum_stream(&block) type = value || :md5 # same comment as above method = type.to_s + "_stream" checksum = send(method, &block) "{#{type}}#{checksum}" end end diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index b8f30a9c7..cf924f371 100755 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,216 +1,217 @@ require 'net/http' require 'uri' require 'tempfile' require 'puppet/util/checksums' require 'puppet/network/http/api/v1' require 'puppet/network/http/compression' module Puppet Puppet::Type.type(:file).newproperty(:content) do include Puppet::Util::Diff include Puppet::Util::Checksums include Puppet::Network::HTTP::API::V1 include Puppet::Network::HTTP::Compression.module attr_reader :actual_content desc "Specify the contents of a file as a string. Newlines, tabs, and spaces can be specified using the escaped syntax (e.g., \\n for a newline). The primary purpose of this parameter is to provide a kind of limited templating:: define resolve(nameserver1, nameserver2, domain, search) { $str = \"search $search domain $domain nameserver $nameserver1 nameserver $nameserver2 \" file { \"/etc/resolv.conf\": content => $str } } This attribute is especially useful when used with `PuppetTemplating templating`:trac:." # Store a checksum as the value, rather than the actual content. # Simplifies everything. munge do |value| if value == :absent value elsif checksum?(value) # XXX This is potentially dangerous because it means users can't write a file whose # entire contents are a plain checksum value else @actual_content = value resource.parameter(:checksum).sum(value) end end # Checksums need to invert how changes are printed. def change_to_s(currentvalue, newvalue) # Our "new" checksum value is provided by the source. if source = resource.parameter(:source) and tmp = source.checksum newvalue = tmp end if currentvalue == :absent return "defined content as '#{newvalue}'" elsif newvalue == :absent return "undefined content from '#{currentvalue}'" else return "content changed '#{currentvalue}' to '#{newvalue}'" end end def checksum_type if source = resource.parameter(:source) result = source.checksum else checksum = resource.parameter(:checksum) result = resource[:checksum] end if result =~ /^\{(\w+)\}.+/ return $1.to_sym else return result end end def length (actual_content and actual_content.length) || 0 end def content self.should end # Override this method to provide diffs if asked for. # Also, fix #872: when content is used, and replace is true, the file # should be insync when it exists def insync?(is) if resource.should_be_file? return false if is == :absent else return true end return true if ! @resource.replace? - return true unless self.should result = super if ! result and Puppet[:show_diff] write_temporarily do |path| print diff(@resource[:path], path) end end result end def retrieve return :absent unless stat = @resource.stat ftype = stat.ftype # Don't even try to manage the content on directories or links return nil if ["directory","link"].include?(ftype) begin resource.parameter(:checksum).sum_file(resource[:path]) rescue => detail raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}" end end # Make sure we're also managing the checksum property. def should=(value) @resource.newattr(:checksum) unless @resource.parameter(:checksum) super end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created # We're safe not testing for the 'source' if there's no 'should' # because we wouldn't have gotten this far if there weren't at least # one valid value somewhere. @resource.write(:content) return_event end def write_temporarily tempfile = Tempfile.new("puppet-file") tempfile.open write(tempfile) tempfile.close yield tempfile.path tempfile.delete end def write(file) resource.parameter(:checksum).sum_stream { |sum| each_chunk_from(actual_content || resource.parameter(:source)) { |chunk| sum << chunk file.print chunk } } end def self.standalone? Puppet.settings[:name] == "apply" end def each_chunk_from(source_or_content) if source_or_content.is_a?(String) yield source_or_content + elsif source_or_content.nil? && resource.parameter(:ensure) && [:present, :file].include?(resource.parameter(:ensure).value) + yield '' elsif source_or_content.nil? yield read_file_from_filebucket elsif self.class.standalone? yield source_or_content.content elsif source_or_content.local? chunk_file_from_disk(source_or_content) { |chunk| yield chunk } else chunk_file_from_source(source_or_content) { |chunk| yield chunk } end end private def chunk_file_from_disk(source_or_content) File.open(source_or_content.full_path, "r") do |src| while chunk = src.read(8192) yield chunk end end end def chunk_file_from_source(source_or_content) request = Puppet::Indirector::Request.new(:file_content, :find, source_or_content.full_path.sub(/^\//,'')) connection = Puppet::Network::HttpPool.http_instance(source_or_content.server, source_or_content.port) connection.request_get(indirection2uri(request), add_accept_encoding({"Accept" => "raw"})) do |response| case response.code when "404"; nil when /^2/; uncompress(response) { |uncompressor| response.read_body { |chunk| yield uncompressor.uncompress(chunk) } } else # Raise the http error if we didn't get a 'success' of some kind. message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}" raise Net::HTTPError.new(message, response) end end end def read_file_from_filebucket raise "Could not get filebucket from file" unless dipper = resource.bucket sum = should.sub(/\{\w+\}/, '') dipper.getfile(sum) rescue => detail fail "Could not retrieve content for #{should} from filebucket: #{detail}" end end end diff --git a/lib/puppet/type/file/owner.rb b/lib/puppet/type/file/owner.rb index d473da20e..483cc7fce 100755 --- a/lib/puppet/type/file/owner.rb +++ b/lib/puppet/type/file/owner.rb @@ -1,52 +1,52 @@ module Puppet Puppet::Type.type(:file).newproperty(:owner) do desc "To whom the file should belong. Argument can be user name or user ID." @event = :file_changed def insync?(current) - provider.insync?(current, @should) + provider.is_owner_insync?(current, @should) end # We want to print names, not numbers def is_to_s(currentvalue) provider.id2name(currentvalue) || currentvalue end def should_to_s(newvalue = @should) case newvalue when Symbol newvalue.to_s when Integer provider.id2name(newvalue) || newvalue when String newvalue else raise Puppet::DevError, "Invalid uid type #{newvalue.class}(#{newvalue})" end end def retrieve if self.should @should = @should.collect do |val| unless val.is_a?(Integer) if tmp = provider.validuser?(val) val = tmp else raise "Could not find user #{val}" end else val end end end provider.retrieve(@resource) end def sync provider.sync(resource[:path], resource[:links], @should) end end end diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb index 7d03de2b0..bc464e1c3 100755 --- a/lib/puppet/type/file/source.rb +++ b/lib/puppet/type/file/source.rb @@ -1,197 +1,196 @@ require 'puppet/file_serving/content' require 'puppet/file_serving/metadata' module Puppet # Copy files from a local or remote source. This state *only* does any work # when the remote file is an actual file; in that case, this state copies # the file down. If the remote file is a dir or a link or whatever, then # this state, during retrieval, modifies the appropriate other states # so that things get taken care of appropriately. Puppet::Type.type(:file).newparam(:source) do include Puppet::Util::Diff attr_accessor :source, :local desc "Copy a file over the current file. Uses `checksum` to determine when a file should be copied. Valid values are either fully qualified paths to files, or URIs. Currently supported URI types are *puppet* and *file*. This is one of the primary mechanisms for getting content into applications that Puppet does not directly support and is very useful for those configuration files that don't change much across sytems. For instance: class sendmail { file { \"/etc/mail/sendmail.cf\": source => \"puppet://server/modules/module_name/sendmail.cf\" } } You can also leave out the server name, in which case `puppet agent` will fill in the name of its configuration server and `puppet apply` will use the local filesystem. This makes it easy to use the same configuration in both local and centralized forms. Currently, only the `puppet` scheme is supported for source URL's. Puppet will connect to the file server running on `server` to retrieve the contents of the file. If the `server` part is empty, the behavior of the command-line interpreter (`puppet apply`) and the client demon (`puppet agent`) differs slightly: `apply` will look such a file up on the module path on the local host, whereas `agent` will connect to the puppet server that it received the manifest from. See the [fileserver configuration documentation](http://projects.puppetlabs.com/projects/puppet/wiki/File_Serving_Configuration) for information on how to configure and use file services within Puppet. If you specify multiple file sources for a file, then the first source that exists will be used. This allows you to specify what amount to search paths for files: file { \"/path/to/my/file\": source => [ \"/modules/nfs/files/file.$host\", \"/modules/nfs/files/file.$operatingsystem\", \"/modules/nfs/files/file\" ] } This will use the first found file as the source. You cannot currently copy links using this mechanism; set `links` to `follow` if any remote sources are links. " validate do |sources| sources = [sources] unless sources.is_a?(Array) sources.each do |source| begin uri = URI.parse(URI.escape(source)) rescue => detail self.fail "Could not understand source #{source}: #{detail}" end self.fail "Cannot use URLs of type '#{uri.scheme}' as source for fileserving" unless uri.scheme.nil? or %w{file puppet}.include?(uri.scheme) end end munge do |sources| sources = [sources] unless sources.is_a?(Array) sources.collect { |source| source.sub(/\/$/, '') } end def change_to_s(currentvalue, newvalue) # newvalue = "{md5}#{@metadata.checksum}" if @resource.property(:ensure).retrieve == :absent return "creating from source #{metadata.source} with contents #{metadata.checksum}" else return "replacing from source #{metadata.source} with contents #{metadata.checksum}" end end def checksum metadata && metadata.checksum end # Look up (if necessary) and return remote content. cached_attr(:content) do raise Puppet::DevError, "No source for content was stored with the metadata" unless metadata.source unless tmp = Puppet::FileServing::Content.find(metadata.source) fail "Could not find any content at %s" % metadata.source end tmp.content end # Copy the values from the source to the resource. Yay. def copy_source_values devfail "Somehow got asked to copy source values without any metadata" unless metadata # Take each of the stats and set them as states on the local file # if a value has not already been provided. [:owner, :mode, :group, :checksum].each do |metadata_method| param_name = (metadata_method == :checksum) ? :content : metadata_method next if metadata_method == :owner and !Puppet.features.root? next if metadata_method == :checksum and metadata.ftype == "directory" if resource[param_name].nil? or resource[param_name] == :absent resource[param_name] = metadata.send(metadata_method) end end if resource[:ensure] == :absent # We know all we need to elsif metadata.ftype != "link" resource[:ensure] = metadata.ftype elsif @resource[:links] == :follow resource[:ensure] = :present else resource[:ensure] = "link" resource[:target] = metadata.destination end end def pinparams [:mode, :type, :owner, :group, :content] end def found? ! (metadata.nil? or metadata.ftype.nil?) end # Provide, and retrieve if necessary, the metadata for this file. Fail # if we can't find data about this host, and fail if there are any # problems in our query. cached_attr(:metadata) do return nil unless value result = nil value.each do |source| begin if data = Puppet::FileServing::Metadata.find(source) result = data result.source = source break end rescue => detail fail detail, "Could not retrieve file metadata for #{source}: #{detail}" end end fail "Could not retrieve information from source(s) #{value.join(", ")}" unless result result end # Make sure we're also checking the checksum def value=(value) super checks = (pinparams + [:ensure]) checks.delete(:checksum) resource[:audit] = checks - resource[:checksum] = :md5 unless resource.property(:checksum) end def local? found? and uri and (uri.scheme || "file") == "file" end def full_path URI.unescape(uri.path) if found? and uri end def server (uri and uri.host) or Puppet.settings[:server] end def port (uri and uri.port) or Puppet.settings[:masterport] end private def uri @uri ||= URI.parse(URI.escape(metadata.source)) end end end diff --git a/lib/puppet/type/file/target.rb b/lib/puppet/type/file/target.rb index 9e7229dda..b9fe9213b 100644 --- a/lib/puppet/type/file/target.rb +++ b/lib/puppet/type/file/target.rb @@ -1,74 +1,74 @@ module Puppet Puppet::Type.type(:file).newproperty(:target) do desc "The target for creating a link. Currently, symlinks are the only type supported." newvalue(:notlink) do # We do nothing if the value is absent return :nochange end # Anything else, basically newvalue(/./) do @resource[:ensure] = :link if ! @resource.should(:ensure) # Only call mklink if ensure didn't call us in the first place. currentensure = @resource.property(:ensure).retrieve - mklink if @resource.property(:ensure).insync?(currentensure) + mklink if @resource.property(:ensure).safe_insync?(currentensure) end # Create our link. def mklink raise Puppet::Error, "Cannot symlink on Microsoft Windows" if Puppet.features.microsoft_windows? target = self.should # Clean up any existing objects. The argument is just for logging, # it doesn't determine what's removed. @resource.remove_existing(target) raise Puppet::Error, "Could not remove existing file" if FileTest.exists?(@resource[:path]) Dir.chdir(File.dirname(@resource[:path])) do Puppet::Util::SUIDManager.asuser(@resource.asuser) do mode = @resource.should(:mode) if mode Puppet::Util.withumask(000) do File.symlink(target, @resource[:path]) end else File.symlink(target, @resource[:path]) end end @resource.send(:property_fix) :link_created end end def insync?(currentvalue) if [:nochange, :notlink].include?(self.should) or @resource.recurse? return true elsif ! @resource.replace? and File.exists?(@resource[:path]) return true else return super(currentvalue) end end def retrieve if stat = @resource.stat if stat.ftype == "link" return File.readlink(@resource[:path]) else return :notlink end else return :absent end end end end diff --git a/lib/puppet/type/host.rb b/lib/puppet/type/host.rb index 8ab750459..2666e50ae 100755 --- a/lib/puppet/type/host.rb +++ b/lib/puppet/type/host.rb @@ -1,98 +1,98 @@ module Puppet newtype(:host) do ensurable newproperty(:ip) do desc "The host's IP address, IPv4 or IPv6." validate do |value| unless value =~ /((([0-9a-fA-F]+:){7}[0-9a-fA-F]+)|(([0-9a-fA-F]+:)*[0-9a-fA-F]+)?::(([0-9a-fA-F]+:)*[0-9a-fA-F]+)?)|((25[0-5]|2[0-4][\d]|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})/ raise Puppet::Error, "Invalid IP address" end end end newproperty(:host_aliases) do desc "Any aliases the host might have. Multiple values must be specified as an array." def insync?(is) is == @should end def is_to_s(currentvalue = @is) currentvalue = [currentvalue] unless currentvalue.is_a? Array currentvalue.join(" ") end def retrieve is = super case is when String is = is.split(/\s*,\s*/) when Symbol is = [is] when Array # nothing else raise Puppet::DevError, "Invalid @is type #{is.class}" end is end # We actually want to return the whole array here, not just the first # value. def should if defined?(@should) if @should == [:absent] return :absent else return @should end else return nil end end def should_to_s(newvalue = @should) newvalue.join(" ") end validate do |value| raise Puppet::Error, "Host aliases cannot include whitespace" if value =~ /\s/ end end newproperty(:target) do desc "The file in which to store service information. Only used by - those providers that write to disk." + those providers that write to disk. On most systems this defaults to `/etc/hosts`." defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) @resource.class.defaultprovider.default_target else nil end } end newparam(:name) do desc "The host name." isnamevar validate do |value| # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = value.split('.').each do |hostpart| unless hostpart =~ /^([\d\w]+|[\d\w][\d\w\-]+[\d\w])$/ raise Puppet::Error, "Invalid host name" end end end end @doc = "Installs and manages host entries. For most systems, these entries will just be in `/etc/hosts`, but some systems (notably OS X) will have different solutions." end end diff --git a/lib/puppet/type/mount.rb b/lib/puppet/type/mount.rb index d048c90f1..da9a70bdf 100755 --- a/lib/puppet/type/mount.rb +++ b/lib/puppet/type/mount.rb @@ -1,224 +1,224 @@ module Puppet # We want the mount to refresh when it changes. newtype(:mount, :self_refresh => true) do @doc = "Manages mounted filesystems, including putting mount information into the mount table. The actual behavior depends on the value of the 'ensure' parameter. Note that if a `mount` receives an event from another resource, it will try to remount the filesystems if `ensure` is set to `mounted`." feature :refreshable, "The provider can remount the filesystem.", :methods => [:remount] # Use the normal parent class, because we actually want to # call code when sync is called. newproperty(:ensure) do desc "Control what to do with this mount. Set this attribute to `umounted` to make sure the filesystem is in the filesystem table but not mounted (if the filesystem is currently mounted, it will be unmounted). Set it to `absent` to unmount (if necessary) and remove the filesystem from the fstab. Set to `mounted` to add it to the fstab and mount it. Set to `present` to add to fstab but not change mount/unmount status" newvalue(:defined) do provider.create return :mount_created end aliasvalue :present, :defined newvalue(:unmounted) do if provider.mounted? syncothers provider.unmount return :mount_unmounted else provider.create return :mount_created end end newvalue(:absent, :event => :mount_deleted) do provider.unmount if provider.mounted? provider.destroy end newvalue(:mounted, :event => :mount_mounted) do # Create the mount point if it does not already exist. current_value = self.retrieve provider.create if current_value.nil? or current_value == :absent syncothers # The fs can be already mounted if it was absent but mounted provider.mount unless provider.mounted? end def insync?(is) if should == :defined and is != :absent true else super end end def retrieve # We need to special case :mounted; if we're absent, we still # want curval = super() if curval == :absent return :absent elsif provider.mounted? return :mounted else return :unmounted end end def syncothers # We have to flush any changes to disk. currentvalues = @resource.retrieve_resource # Determine if there are any out-of-sync properties. oos = @resource.send(:properties).find_all do |prop| unless currentvalues.include?(prop) raise Puppet::DevError, "Parent has property %s but it doesn't appear in the current values", [prop.name] end if prop.name == :ensure false else - ! prop.insync?(currentvalues[prop]) + ! prop.safe_insync?(currentvalues[prop]) end end.each { |prop| prop.sync }.length @resource.flush if oos > 0 end end newproperty(:device) do desc "The device providing the mount. This can be whatever device is supporting by the mount, including network devices or devices specified by UUID rather than device path, depending on the operating system." end # Solaris specifies two devices, not just one. newproperty(:blockdevice) do desc "The device to fsck. This is property is only valid on Solaris, and in most cases will default to the correct value." # Default to the device but with "dsk" replaced with "rdsk". defaultto do if Facter["operatingsystem"].value == "Solaris" device = @resource.value(:device) if device =~ %r{/dsk/} device.sub(%r{/dsk/}, "/rdsk/") else nil end else nil end end end newproperty(:fstype) do desc "The mount type. Valid values depend on the operating system. This is a required option." end newproperty(:options) do desc "Mount options for the mounts, as they would appear in the fstab." end newproperty(:pass) do desc "The pass in which the mount is checked." defaultto { 0 if @resource.managed? } end newproperty(:atboot) do desc "Whether to mount the mount at boot. Not all platforms support this." end newproperty(:dump) do desc "Whether to dump the mount. Not all platform support this. Valid values are `1` or `0`. or `2` on FreeBSD, Default is `0`." if Facter["operatingsystem"].value == "FreeBSD" newvalue(%r{(0|1|2)}) else newvalue(%r{(0|1)}) end newvalue(%r{(0|1)}) defaultto { 0 if @resource.managed? } end newproperty(:target) do desc "The file in which to store the mount table. Only used by those providers that write to disk." defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) @resource.class.defaultprovider.default_target else nil end } end newparam(:name) do desc "The mount path for the mount." isnamevar end newparam(:path) do desc "The deprecated name for the mount point. Please use `name` now." def value=(value) warning "'path' is deprecated for mounts. Please use 'name'." @resource[:name] = value super end end newparam(:remounts) do desc "Whether the mount can be remounted `mount -o remount`. If this is false, then the filesystem will be unmounted and remounted manually, which is prone to failure." newvalues(:true, :false) defaultto do case Facter.value(:operatingsystem) - when "FreeBSD", "Darwin" + when "FreeBSD", "Darwin", "AIX" false else true end end end def refresh # Only remount if we're supposed to be mounted. provider.remount if self.should(:fstype) != "swap" and provider.mounted? end def value(name) name = symbolize(name) ret = nil if property = @parameters[name] return property.value end end end end diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb index 51a866332..d73d90dff 100644 --- a/lib/puppet/type/package.rb +++ b/lib/puppet/type/package.rb @@ -1,321 +1,319 @@ # Define the different packaging systems. Each package system is implemented # in a module, which then gets used to individually extend each package object. # This allows packages to exist on the same machine using different packaging # systems. module Puppet newtype(:package) do @doc = "Manage packages. There is a basic dichotomy in package support right now: Some package types (e.g., yum and apt) can retrieve their own package files, while others (e.g., rpm and sun) cannot. For those package formats that cannot retrieve their own files, you can use the `source` parameter to point to the correct file. Puppet will automatically guess the packaging format that you are using based on the platform you are on, but you can override it using the `provider` parameter; each provider defines what it requires in order to function, and you must meet those requirements to use a given provider." feature :installable, "The provider can install packages.", :methods => [:install] feature :uninstallable, "The provider can uninstall packages.", :methods => [:uninstall] feature :upgradeable, "The provider can upgrade to the latest version of a package. This feature is used by specifying `latest` as the desired value for the package.", :methods => [:update, :latest] feature :purgeable, "The provider can purge packages. This generally means that all traces of the package are removed, including existing configuration files. This feature is thus destructive and should be used with the utmost care.", :methods => [:purge] feature :versionable, "The provider is capable of interrogating the package database for installed version(s), and can select which out of a set of available versions of a package to install if asked." feature :holdable, "The provider is capable of placing packages on hold such that they are not automatically upgraded as a result of other package dependencies unless explicit action is taken by a user or another package. Held is considered a superset of installed.", :methods => [:hold] ensurable do desc "What state the package should be in. *latest* only makes sense for those packaging formats that can retrieve new packages on their own and will throw an error on those that cannot. For those packaging systems that allow you to specify package versions, specify them here. Similarly, *purged* is only useful for packaging systems that support the notion of managing configuration files separately from 'normal' system files." attr_accessor :latest newvalue(:present, :event => :package_installed) do provider.install end newvalue(:absent, :event => :package_removed) do provider.uninstall end newvalue(:purged, :event => :package_purged, :required_features => :purgeable) do provider.purge end newvalue(:held, :event => :package_held, :required_features => :holdable) do provider.hold end # Alias the 'present' value. aliasvalue(:installed, :present) newvalue(:latest, :required_features => :upgradeable) do # Because yum always exits with a 0 exit code, there's a retrieve # in the "install" method. So, check the current state now, # to compare against later. current = self.retrieve begin provider.update rescue => detail self.fail "Could not update: #{detail}" end if current == :absent :package_installed else :package_changed end end newvalue(/./, :required_features => :versionable) do begin provider.install rescue => detail self.fail "Could not update: #{detail}" end if self.retrieve == :absent :package_installed else :package_changed end end defaultto :installed # Override the parent method, because we've got all kinds of # funky definitions of 'in sync'. def insync?(is) - @should ||= [] - @latest ||= nil @lateststamp ||= (Time.now.to_i - 1000) # Iterate across all of the should values, and see how they # turn out. @should.each { |should| case should when :present return true unless [:absent, :purged, :held].include?(is) when :latest # Short-circuit packages that are not present return false if is == :absent or is == :purged # Don't run 'latest' more than about every 5 minutes if @latest and ((Time.now.to_i - @lateststamp) / 60) < 5 #self.debug "Skipping latest check" else begin @latest = provider.latest @lateststamp = Time.now.to_i rescue => detail error = Puppet::Error.new("Could not get latest version: #{detail}") error.set_backtrace(detail.backtrace) raise error end end case is when @latest return true when :present # This will only happen on retarded packaging systems # that can't query versions. return true else self.debug "#{@resource.name} #{is.inspect} is installed, latest is #{@latest.inspect}" end when :absent return true if is == :absent or is == :purged when :purged return true if is == :purged when is return true end } false end # This retrieves the current state. LAK: I think this method is unused. def retrieve provider.properties[:ensure] end # Provide a bit more information when logging upgrades. def should_to_s(newvalue = @should) if @latest @latest.to_s else super(newvalue) end end end newparam(:name) do desc "The package name. This is the name that the packaging system uses internally, which is sometimes (especially on Solaris) a name that is basically useless to humans. If you want to abstract package installation, then you can use aliases to provide a common name to packages: # In the 'openssl' class $ssl = $operatingsystem ? { solaris => SMCossl, default => openssl } # It is not an error to set an alias to the same value as the # object name. package { $ssl: ensure => installed, alias => openssl } . etc. . $ssh = $operatingsystem ? { solaris => SMCossh, default => openssh } # Use the alias to specify a dependency, rather than # having another selector to figure it out again. package { $ssh: ensure => installed, alias => openssh, require => Package[openssl] } " isnamevar end newparam(:source) do desc "Where to find the actual package. This must be a local file (or on a network file system) or a URL that your specific packaging type understands; Puppet will not retrieve files for you." end newparam(:instance) do desc "A read-only parameter set by the package." end newparam(:status) do desc "A read-only parameter set by the package." end newparam(:type) do desc "Deprecated form of `provider`." munge do |value| warning "'type' is deprecated; use 'provider' instead" @resource[:provider] = value @resource[:provider] end end newparam(:adminfile) do desc "A file containing package defaults for installing packages. This is currently only used on Solaris. The value will be validated according to system rules, which in the case of Solaris means that it should either be a fully qualified path or it should be in `/var/sadm/install/admin`." end newparam(:responsefile) do desc "A file containing any necessary answers to questions asked by the package. This is currently used on Solaris and Debian. The value will be validated according to system rules, but it should generally be a fully qualified path." end newparam(:configfiles) do desc "Whether configfiles should be kept or replaced. Most packages types do not support this parameter." defaultto :keep newvalues(:keep, :replace) end newparam(:category) do desc "A read-only parameter set by the package." end newparam(:platform) do desc "A read-only parameter set by the package." end newparam(:root) do desc "A read-only parameter set by the package." end newparam(:vendor) do desc "A read-only parameter set by the package." end newparam(:description) do desc "A read-only parameter set by the package." end newparam(:allowcdrom) do desc "Tells apt to allow cdrom sources in the sources.list file. Normally apt will bail if you try this." newvalues(:true, :false) end newparam(:flavor) do desc "Newer versions of OpenBSD support 'flavors', which are further specifications for which type of package you want." end autorequire(:file) do autos = [] [:responsefile, :adminfile].each { |param| if val = self[param] autos << val end } if source = self[:source] if source =~ /^#{File::SEPARATOR}/ autos << source end end autos end # This only exists for testing. def clear if obj = @parameters[:ensure] obj.latest = nil end end # The 'query' method returns a hash of info if the package # exists and returns nil if it does not. def exists? @provider.get(:ensure) != :absent end end end diff --git a/lib/puppet/type/service.rb b/lib/puppet/type/service.rb index c00f02789..0d09c3d5d 100644 --- a/lib/puppet/type/service.rb +++ b/lib/puppet/type/service.rb @@ -1,185 +1,191 @@ # This is our main way of managing processes right now. # # a service is distinct from a process in that services # can only be managed through the interface of an init script # which is why they have a search path for initscripts and such module Puppet newtype(:service) do @doc = "Manage running services. Service support unfortunately varies widely by platform -- some platforms have very little if any concept of a running service, and some have a very codified and powerful concept. Puppet's service support will generally be able to make up for any inherent shortcomings (e.g., if there is no 'status' command, then Puppet will look in the process table for a command matching the service name), but the more information you can provide the better behaviour you will get. Or, you can just use a platform that has very good service support. Note that if a `service` receives an event from another resource, the service will get restarted. The actual command to restart the service depends on the platform. You can provide a special command for restarting with the `restart` attribute." feature :refreshable, "The provider can restart the service.", :methods => [:restart] feature :enableable, "The provider can enable and disable the service", :methods => [:disable, :enable, :enabled?] feature :controllable, "The provider uses a control variable." newproperty(:enable, :required_features => :enableable) do desc "Whether a service should be enabled to start at boot. This property behaves quite differently depending on the platform; wherever possible, it relies on local tools to enable or disable a given service." newvalue(:true, :event => :service_enabled) do provider.enable end newvalue(:false, :event => :service_disabled) do provider.disable end def retrieve provider.enabled? end end # Handle whether the service should actually be running right now. newproperty(:ensure) do desc "Whether a service should be running." newvalue(:stopped, :event => :service_stopped) do provider.stop end newvalue(:running, :event => :service_started) do provider.start end aliasvalue(:false, :stopped) aliasvalue(:true, :running) def retrieve provider.status end def sync event = super() if property = @resource.property(:enable) val = property.retrieve - property.sync unless property.insync?(val) + property.sync unless property.safe_insync?(val) end event end end newparam(:binary) do desc "The path to the daemon. This is only used for systems that do not support init scripts. This binary will be used to start the service if no `start` parameter is provided." end newparam(:hasstatus) do desc "Declare the the service's init script has a functional status command. Based on testing, it was found that a large number of init scripts on different platforms do not support any kind of status command; thus, you must specify manually whether the service you are running has such a command (or you can specify a specific command using the `status` parameter). If you do not specify anything, then the service name will be looked for in the process table." newvalues(:true, :false) end newparam(:name) do desc "The name of the service to run. This name is used to find the service in whatever service subsystem it is in." isnamevar end newparam(:path) do desc "The search path for finding init scripts. Multiple values should be separated by colons or provided as an array." munge do |value| value = [value] unless value.is_a?(Array) # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] # It affects stand-alone blocks, too. paths = value.flatten.collect { |p| x = p.split(":") }.flatten end defaultto { provider.class.defpath if provider.class.respond_to?(:defpath) } end newparam(:pattern) do desc "The pattern to search for in the process table. This is used for stopping services on platforms that do not support init scripts, and is also used for determining service status on those service whose init scripts do not include a status command. If this is left unspecified and is needed to check the status of a service, then the service name will be used instead. The pattern can be a simple string or any legal Ruby pattern." defaultto { @resource[:binary] || @resource[:name] } end newparam(:restart) do desc "Specify a *restart* command manually. If left unspecified, the service will be stopped and then started." end newparam(:start) do desc "Specify a *start* command manually. Most service subsystems support a `start` command, so this will not need to be specified." end newparam(:status) do - desc "Specify a *status* command manually. If left - unspecified, the status method will be determined - automatically, usually by looking for the service in the - process table." + desc "Specify a *status* command manually. This command must + return 0 if the service is running and a nonzero value otherwise. + Ideally, these return codes should conform to + [the LSB's specification for init script status actions](http://refspecs.freestandards.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/iniscrptact.html), + but puppet only considers the difference between 0 and nonzero + to be relevant. + + If left unspecified, the status method will be determined + automatically, usually by looking for the service in the process + table." end newparam(:stop) do desc "Specify a *stop* command manually." end newparam(:control) do desc "The control variable used to manage services (originally for HP-UX). Defaults to the upcased service name plus `START` replacing dots with underscores, for those providers that support the `controllable` feature." defaultto { resource.name.gsub(".","_").upcase + "_START" if resource.provider.controllable? } end newparam :hasrestart do desc "Specify that an init script has a `restart` option. Otherwise, the init script's `stop` and `start` methods are used." newvalues(:true, :false) end newparam(:manifest) do desc "Specify a command to config a service, or a path to a manifest to do so." end # Basically just a synonym for restarting. Used to respond # to events. def refresh # Only restart if we're actually running if (@parameters[:ensure] || newattr(:ensure)).retrieve == :running provider.restart else debug "Skipping restart; service is not running" end end end end diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index 761d5d71b..e7389a0d1 100755 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -1,438 +1,436 @@ require 'etc' require 'facter' require 'puppet/property/list' require 'puppet/property/ordered_list' require 'puppet/property/keyvalue' module Puppet newtype(:user) do @doc = "Manage users. This type is mostly built to manage system users, so it is lacking some features useful for managing normal users. This resource type uses the prescribed native tools for creating groups and generally uses POSIX APIs for retrieving information about them. It does not directly modify `/etc/passwd` or anything." feature :allows_duplicates, "The provider supports duplicate users with the same UID." feature :manages_homedir, "The provider can create and remove home directories." feature :manages_passwords, "The provider can modify user passwords, by accepting a password hash." feature :manages_password_age, "The provider can set age requirements and restrictions for passwords." feature :manages_solaris_rbac, "The provider can manage roles and normal users" feature :manages_expiry, "The provider can manage the expiry date for a user." newproperty(:ensure, :parent => Puppet::Property::Ensure) do newvalue(:present, :event => :user_created) do provider.create end newvalue(:absent, :event => :user_removed) do provider.delete end newvalue(:role, :event => :role_created, :required_features => :manages_solaris_rbac) do provider.create_role end desc "The basic state that the object should be in." # If they're talking about the thing at all, they generally want to # say it should exist. defaultto do if @resource.managed? :present else nil end end def retrieve if provider.exists? if provider.respond_to?(:is_role?) and provider.is_role? return :role else return :present end else return :absent end end end newproperty(:home) do desc "The home directory of the user. The directory must be created separately and is not currently checked for existence." end newproperty(:uid) do desc "The user ID. Must be specified numerically. For new users being created, if no user ID is specified then one will be chosen automatically, which will likely result in the same user having different IDs on different systems, which is not recommended. This is especially noteworthy if you use Puppet to manage the same user on both Darwin and other platforms, since Puppet does the ID generation for you on Darwin, but the tools do so on other platforms." munge do |value| case value when String if value =~ /^[-0-9]+$/ value = Integer(value) end end return value end end newproperty(:gid) do desc "The user's primary group. Can be specified numerically or by name." munge do |value| if value.is_a?(String) and value =~ /^[-0-9]+$/ Integer(value) else value end end def insync?(is) - return true unless self.should - # We know the 'is' is a number, so we need to convert the 'should' to a number, # too. @should.each do |value| return true if number = Puppet::Util.gid(value) and is == number end false end def sync found = false @should.each do |value| if number = Puppet::Util.gid(value) provider.gid = number found = true break end end fail "Could not find group(s) #{@should.join(",")}" unless found # Use the default event. end end newproperty(:comment) do desc "A description of the user. Generally is a user's full name." end newproperty(:shell) do desc "The user's login shell. The shell must exist and be executable." end newproperty(:password, :required_features => :manages_passwords) do desc "The user's password, in whatever encrypted format the local machine requires. Be sure to enclose any value that includes a dollar sign ($) in single quotes (\')." validate do |value| raise ArgumentError, "Passwords cannot include ':'" if value.is_a?(String) and value.include?(":") end def change_to_s(currentvalue, newvalue) if currentvalue == :absent return "created password" else return "changed password" end end end newproperty(:password_min_age, :required_features => :manages_password_age) do desc "The minimum amount of time in days a password must be used before it may be changed" munge do |value| case value when String Integer(value) else value end end validate do |value| - if value.to_s !~ /^\d+$/ + if value.to_s !~ /^-?\d+$/ raise ArgumentError, "Password minimum age must be provided as a number" end end end newproperty(:password_max_age, :required_features => :manages_password_age) do desc "The maximum amount of time in days a password may be used before it must be changed" munge do |value| case value when String Integer(value) else value end end validate do |value| - if value.to_s !~ /^\d+$/ + if value.to_s !~ /^-?\d+$/ raise ArgumentError, "Password maximum age must be provided as a number" end end end newproperty(:groups, :parent => Puppet::Property::List) do desc "The groups of which the user is a member. The primary group should not be listed. Multiple groups should be specified as an array." validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Group names must be provided, not numbers" end raise ArgumentError, "Group names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:name) do desc "User name. While limitations are determined for each operating system, it is generally a good idea to keep to the degenerate 8 characters, beginning with a letter." isnamevar end newparam(:membership) do desc "Whether specified groups should be treated as the only groups of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newparam(:allowdupe, :boolean => true) do desc "Whether to allow duplicate UIDs." newvalues(:true, :false) defaultto false end newparam(:managehome, :boolean => true) do desc "Whether to manage the home directory when managing the user." newvalues(:true, :false) defaultto false validate do |val| if val.to_s == "true" raise ArgumentError, "User provider #{provider.class.name} can not manage home directories" unless provider.class.manages_homedir? end end end newproperty(:expiry, :required_features => :manages_expiry) do desc "The expiry date for this user. Must be provided in a zero padded YYYY-MM-DD format - e.g 2010-02-19." validate do |value| if value !~ /^\d{4}-\d{2}-\d{2}$/ raise ArgumentError, "Expiry dates must be YYYY-MM-DD" end end end # Autorequire the group, if it's around autorequire(:group) do autos = [] if obj = @parameters[:gid] and groups = obj.shouldorig groups = groups.collect { |group| if group =~ /^\d+$/ Integer(group) else group end } groups.each { |group| case group when Integer if resource = catalog.resources.find { |r| r.is_a?(Puppet::Type.type(:group)) and r.should(:gid) == group } autos << resource end else autos << group end } end if obj = @parameters[:groups] and groups = obj.should autos += groups.split(",") end autos end # Provide an external hook. Yay breaking out of APIs. def exists? provider.exists? end def retrieve absent = false properties.inject({}) { |prophash, property| current_value = :absent if absent prophash[property] = :absent else current_value = property.retrieve prophash[property] = current_value end if property.name == :ensure and current_value == :absent absent = true end prophash } end newproperty(:roles, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The roles the user has. Multiple roles should be specified as an array." def membership :role_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Role names must be provided, not numbers" end raise ArgumentError, "Role names must be provided as an array, not a comma-separated list" if value.include?(",") end end #autorequire the roles that the user has autorequire(:user) do reqs = [] if roles_property = @parameters[:roles] and roles = roles_property.should reqs += roles.split(',') end reqs end newparam(:role_membership) do desc "Whether specified roles should be treated as the only roles of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:auths, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The auths the user has. Multiple auths should be specified as an array." def membership :auth_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Auth names must be provided, not numbers" end raise ArgumentError, "Auth names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:auth_membership) do desc "Whether specified auths should be treated as the only auths of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:profiles, :parent => Puppet::Property::OrderedList, :required_features => :manages_solaris_rbac) do desc "The profiles the user has. Multiple profiles should be specified as an array." def membership :profile_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Profile names must be provided, not numbers" end raise ArgumentError, "Profile names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:profile_membership) do desc "Whether specified roles should be treated as the only roles of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:keys, :parent => Puppet::Property::KeyValue, :required_features => :manages_solaris_rbac) do desc "Specify user attributes in an array of keyvalue pairs" def membership :key_membership end validate do |value| raise ArgumentError, "key value pairs must be seperated by an =" unless value.include?("=") end end newparam(:key_membership) do desc "Whether specified key value pairs should be treated as the only attributes of the user or whether they should merely be treated as the minimum list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:project, :required_features => :manages_solaris_rbac) do desc "The name of the project associated with a user" end end end diff --git a/lib/puppet/type/yumrepo.rb b/lib/puppet/type/yumrepo.rb index 160b2164d..9b4c79428 100644 --- a/lib/puppet/type/yumrepo.rb +++ b/lib/puppet/type/yumrepo.rb @@ -1,359 +1,359 @@ # Description of yum repositories require 'puppet/util/inifile' module Puppet # A property for one entry in a .ini-style file class IniProperty < Puppet::Property def insync?(is) # A should property of :absent is the same as nil - if is.nil? && (should.nil? || should == :absent) + if is.nil? && should == :absent return true end super(is) end def sync - if insync?(retrieve) + if safe_insync?(retrieve) result = nil else result = set(self.should) if should == :absent resource.section[inikey] = nil else resource.section[inikey] = should end end result end def retrieve resource.section[inikey] end def inikey name.to_s end # Set the key associated with this property to KEY, instead # of using the property's NAME def self.inikey(key) # Override the inikey instance method # Is there a way to do this without resorting to strings ? # Using a block fails because the block can't access # the variable 'key' in the outer scope self.class_eval("def inikey ; \"#{key.to_s}\" ; end") end end # Doc string for properties that can be made 'absent' ABSENT_DOC="Set this to 'absent' to remove it from the file completely" newtype(:yumrepo) do @doc = "The client-side description of a yum repository. Repository configurations are found by parsing `/etc/yum.conf` and the files indicated by the `reposdir` option in that file (see yum.conf(5) for details) Most parameters are identical to the ones documented in yum.conf(5) Continuation lines that yum supports for example for the baseurl are not supported. No attempt is made to access files included with the **include** directive" class << self attr_accessor :filetype # The writer is only used for testing, there should be no need # to change yumconf or inifile in any other context attr_accessor :yumconf attr_writer :inifile end self.filetype = Puppet::Util::FileType.filetype(:flat) @inifile = nil @yumconf = "/etc/yum.conf" # Where to put files for brand new sections @defaultrepodir = nil def self.instances l = [] check = validproperties clear inifile.each_section do |s| next if s.name == "main" obj = new(:name => s.name, :audit => check) current_values = obj.retrieve obj.eachproperty do |property| if current_values[property].nil? obj.delete(property.name) else property.should = current_values[property] end end obj.delete(:audit) l << obj end l end # Return the Puppet::Util::IniConfig::File for the whole yum config def self.inifile if @inifile.nil? @inifile = read main = @inifile['main'] raise Puppet::Error, "File #{yumconf} does not contain a main section" if main.nil? reposdir = main['reposdir'] reposdir ||= "/etc/yum.repos.d, /etc/yum/repos.d" reposdir.gsub!(/[\n,]/, " ") reposdir.split.each do |dir| Dir::glob("#{dir}/*.repo").each do |file| @inifile.read(file) if File.file?(file) end end reposdir.split.each do |dir| if File::directory?(dir) && File::writable?(dir) @defaultrepodir = dir break end end end @inifile end # Parse the yum config files. Only exposed for the tests # Non-test code should use self.inifile to get at the # underlying file def self.read result = Puppet::Util::IniConfig::File.new result.read(yumconf) main = result['main'] raise Puppet::Error, "File #{yumconf} does not contain a main section" if main.nil? reposdir = main['reposdir'] reposdir ||= "/etc/yum.repos.d, /etc/yum/repos.d" reposdir.gsub!(/[\n,]/, " ") reposdir.split.each do |dir| Dir::glob("#{dir}/*.repo").each do |file| result.read(file) if File.file?(file) end end if @defaultrepodir.nil? reposdir.split.each do |dir| if File::directory?(dir) && File::writable?(dir) @defaultrepodir = dir break end end end result end # Return the Puppet::Util::IniConfig::Section with name NAME # from the yum config def self.section(name) result = inifile[name] if result.nil? # Brand new section path = yumconf path = File::join(@defaultrepodir, "#{name}.repo") unless @defaultrepodir.nil? Puppet::info "create new repo #{name} in file #{path}" result = inifile.add_section(name, path) end result end # Store all modifications back to disk def self.store inifile.store unless Puppet[:noop] target_mode = 0644 # FIXME: should be configurable inifile.each_file do |file| current_mode = File.stat(file).mode & 0777 unless current_mode == target_mode Puppet::info "changing mode of #{file} from %03o to %03o" % [current_mode, target_mode] File.chmod(target_mode, file) end end end end # This is only used during testing. def self.clear @inifile = nil @yumconf = "/etc/yum.conf" @defaultrepodir = nil end # Return the Puppet::Util::IniConfig::Section for this yumrepo resource def section self.class.section(self[:name]) end # Store modifications to this yumrepo resource back to disk def flush self.class.store end newparam(:name) do desc "The name of the repository. This corresponds to the repositoryid parameter in yum.conf(5)." isnamevar end newproperty(:descr, :parent => Puppet::IniProperty) do desc "A human readable description of the repository. This corresponds to the name parameter in yum.conf(5). #{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(/.*/) { } inikey "name" end newproperty(:mirrorlist, :parent => Puppet::IniProperty) do desc "The URL that holds the list of mirrors for this repository. #{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } # Should really check that it's a valid URL newvalue(/.*/) { } end newproperty(:baseurl, :parent => Puppet::IniProperty) do desc "The URL for this repository.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } # Should really check that it's a valid URL newvalue(/.*/) { } end newproperty(:enabled, :parent => Puppet::IniProperty) do desc "Whether this repository is enabled or disabled. Possible values are '0', and '1'.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r{(0|1)}) { } end newproperty(:gpgcheck, :parent => Puppet::IniProperty) do desc "Whether to check the GPG signature on packages installed from this repository. Possible values are '0', and '1'. \n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r{(0|1)}) { } end newproperty(:gpgkey, :parent => Puppet::IniProperty) do desc "The URL for the GPG key with which packages from this repository are signed.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } # Should really check that it's a valid URL newvalue(/.*/) { } end newproperty(:include, :parent => Puppet::IniProperty) do desc "A URL from which to include the config.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } # Should really check that it's a valid URL newvalue(/.*/) { } end newproperty(:exclude, :parent => Puppet::IniProperty) do desc "List of shell globs. Matching packages will never be considered in updates or installs for this repo. #{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(/.*/) { } end newproperty(:includepkgs, :parent => Puppet::IniProperty) do desc "List of shell globs. If this is set, only packages matching one of the globs will be considered for update or install.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(/.*/) { } end newproperty(:enablegroups, :parent => Puppet::IniProperty) do desc "Determines whether yum will allow the use of package groups for this repository. Possible values are '0', and '1'.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r{(0|1)}) { } end newproperty(:failovermethod, :parent => Puppet::IniProperty) do desc "Either 'roundrobin' or 'priority'.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r{roundrobin|priority}) { } end newproperty(:keepalive, :parent => Puppet::IniProperty) do desc "Either '1' or '0'. This tells yum whether or not HTTP/1.1 keepalive should be used with this repository.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r{(0|1)}) { } end newproperty(:http_caching, :parent => Puppet::IniProperty) do desc "Either 'packages' or 'all' or 'none'.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r(packages|all|none)) { } end newproperty(:timeout, :parent => Puppet::IniProperty) do desc "Number of seconds to wait for a connection before timing out.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r{[0-9]+}) { } end newproperty(:metadata_expire, :parent => Puppet::IniProperty) do desc "Number of seconds after which the metadata will expire. #{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r{[0-9]+}) { } end newproperty(:protect, :parent => Puppet::IniProperty) do desc "Enable or disable protection for this repository. Requires that the protectbase plugin is installed and enabled. #{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r{(0|1)}) { } end newproperty(:priority, :parent => Puppet::IniProperty) do desc "Priority of this repository from 1-99. Requires that the priorities plugin is installed and enabled. #{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r{[1-9][0-9]?}) { } end newproperty(:cost, :parent => Puppet::IniProperty) do desc "Cost of this repository.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r{\d+}) { } end newproperty(:proxy, :parent => Puppet::IniProperty) do desc "URL to the proxy server for this repository.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } # Should really check that it's a valid URL newvalue(/.*/) { } end newproperty(:proxy_username, :parent => Puppet::IniProperty) do desc "Username for this proxy.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(/.*/) { } end newproperty(:proxy_password, :parent => Puppet::IniProperty) do desc "Password for this proxy.\n#{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(/.*/) { } end end end diff --git a/lib/puppet/type/zpool.rb b/lib/puppet/type/zpool.rb index 49cce552a..df06522e8 100755 --- a/lib/puppet/type/zpool.rb +++ b/lib/puppet/type/zpool.rb @@ -1,92 +1,88 @@ module Puppet class Property class VDev < Property def flatten_and_sort(array) array.collect { |a| a.split(' ') }.flatten.sort end def insync?(is) - return true unless self.should - return @should == [:absent] if is == :absent flatten_and_sort(is) == flatten_and_sort(@should) end end class MultiVDev < VDev def insync?(is) - return true unless self.should - return @should == [:absent] if is == :absent return false unless is.length == @should.length is.each_with_index { |list, i| return false unless flatten_and_sort(list) == flatten_and_sort(@should[i]) } #if we made it this far we are in sync true end end end newtype(:zpool) do @doc = "Manage zpools. Create and delete zpools. The provider WILL NOT SYNC, only report differences. Supports vdevs with mirrors, raidz, logs and spares." ensurable newproperty(:disk, :array_matching => :all, :parent => Puppet::Property::VDev) do desc "The disk(s) for this pool. Can be an array or space separated string" end newproperty(:mirror, :array_matching => :all, :parent => Puppet::Property::MultiVDev) do desc "List of all the devices to mirror for this pool. Each mirror should be a space separated string: mirror => [\"disk1 disk2\", \"disk3 disk4\"] " validate do |value| raise ArgumentError, "mirror names must be provided as string separated, not a comma-separated list" if value.include?(",") end end newproperty(:raidz, :array_matching => :all, :parent => Puppet::Property::MultiVDev) do desc "List of all the devices to raid for this pool. Should be an array of space separated strings: raidz => [\"disk1 disk2\", \"disk3 disk4\"] " validate do |value| raise ArgumentError, "raid names must be provided as string separated, not a comma-separated list" if value.include?(",") end end newproperty(:spare, :array_matching => :all, :parent => Puppet::Property::VDev) do desc "Spare disk(s) for this pool." end newproperty(:log, :array_matching => :all, :parent => Puppet::Property::VDev) do desc "Log disks for this pool. (doesn't support mirroring yet)" end newparam(:pool) do desc "The name for this pool." isnamevar end newparam(:raid_parity) do desc "Determines parity when using raidz property." end validate do has_should = [:disk, :mirror, :raidz].select { |prop| self.should(prop) } self.fail "You cannot specify #{has_should.join(" and ")} on this type (only one)" if has_should.length > 1 end end end diff --git a/lib/puppet/util/checksums.rb b/lib/puppet/util/checksums.rb index 6fdf14ecf..e129301e6 100644 --- a/lib/puppet/util/checksums.rb +++ b/lib/puppet/util/checksums.rb @@ -1,136 +1,148 @@ # A stand-alone module for calculating checksums # in a generic way. module Puppet::Util::Checksums class FakeChecksum def <<(*args) self end end # Is the provided string a checksum? def checksum?(string) string =~ /^\{(\w{3,5})\}\S+/ end # Strip the checksum type from an existing checksum def sumdata(checksum) checksum =~ /^\{(\w+)\}(.+)/ ? $2 : nil end # Strip the checksum type from an existing checksum def sumtype(checksum) checksum =~ /^\{(\w+)\}/ ? $1 : nil end # Calculate a checksum using Digest::MD5. def md5(content) require 'digest/md5' Digest::MD5.hexdigest(content) end # Calculate a checksum of the first 500 chars of the content using Digest::MD5. def md5lite(content) md5(content[0..511]) end # Calculate a checksum of a file's content using Digest::MD5. def md5_file(filename, lite = false) require 'digest/md5' digest = Digest::MD5.new checksum_file(digest, filename, lite) end # Calculate a checksum of the first 500 chars of a file's content using Digest::MD5. def md5lite_file(filename) md5_file(filename, true) end def md5_stream(&block) require 'digest/md5' digest = Digest::MD5.new yield digest digest.hexdigest end alias :md5lite_stream :md5_stream # Return the :mtime timestamp of a file. def mtime_file(filename) File.stat(filename).send(:mtime) end # by definition this doesn't exist # but we still need to execute the block given def mtime_stream noop_digest = FakeChecksum.new yield noop_digest nil end - alias :ctime_stream :mtime_stream + def mtime(content) + "" + end # Calculate a checksum using Digest::SHA1. def sha1(content) require 'digest/sha1' Digest::SHA1.hexdigest(content) end # Calculate a checksum of the first 500 chars of the content using Digest::SHA1. def sha1lite(content) sha1(content[0..511]) end # Calculate a checksum of a file's content using Digest::SHA1. def sha1_file(filename, lite = false) require 'digest/sha1' digest = Digest::SHA1.new checksum_file(digest, filename, lite) end # Calculate a checksum of the first 500 chars of a file's content using Digest::SHA1. def sha1lite_file(filename) sha1_file(filename, true) end def sha1_stream require 'digest/sha1' digest = Digest::SHA1.new yield digest digest.hexdigest end alias :sha1lite_stream :sha1_stream # Return the :ctime of a file. def ctime_file(filename) File.stat(filename).send(:ctime) end + alias :ctime_stream :mtime_stream + + def ctime(content) + "" + end + # Return a "no checksum" def none_file(filename) "" end def none_stream noop_digest = FakeChecksum.new yield noop_digest "" end + def none(content) + "" + end + private # Perform an incremental checksum on a file. def checksum_file(digest, filename, lite = false) buffer = lite ? 512 : 4096 File.open(filename, 'r') do |file| while content = file.read(buffer) digest << content break if lite end end digest.hexdigest end end diff --git a/lib/puppet/util/command_line/puppetca b/lib/puppet/util/command_line/puppetca index 9aa7e907c..317d99881 100755 --- a/lib/puppet/util/command_line/puppetca +++ b/lib/puppet/util/command_line/puppetca @@ -1,110 +1,110 @@ #!/usr/bin/env ruby # # = Synopsis # # Stand-alone certificate authority. Capable of generating certificates # but mostly meant for signing certificate requests from puppet clients. # # = Usage # # puppet cert [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose] # [-g|--generate] [-l|--list] [-s|--sign] [-r|--revoke] # [-p|--print] [-c|--clean] [--verify] [--digest DIGEST] # [--fingerprint] [host] # # = Description # # Because the puppetmasterd daemon defaults to not signing client certificate # requests, this script is available for signing outstanding requests. It # can be used to list outstanding requests and then either sign them individually # or sign all of them. # # = Options # # Note that any configuration parameter that's valid in the configuration file # is also a valid long argument. For example, 'ssldir' is a valid configuration # parameter, so you can specify '--ssldir ' as an argument. # # See the configuration file documentation at -# http://reductivelabs.com/projects/puppet/reference/configref.html for +# http://docs.puppetlabs.com/references/stable/configuration.html for # the full list of acceptable parameters. A commented list of all # configuration options can also be generated by running puppet cert with # '--genconfig'. # # all:: # Operate on all items. Currently only makes sense with '--sign', # '--clean', or '--list'. # # digest:: # Set the digest for fingerprinting (defaults to md5). Valid values depends # on your openssl and openssl ruby extension version, but should contain at # least md5, sha1, md2, sha256. # # clean:: # Remove all files related to a host from puppet cert's storage. This is # useful when rebuilding hosts, since new certificate signing requests # will only be honored if puppet cert does not have a copy of a signed -# certificate for that host. The certificate of the host remains valid. +# certificate for that host. The certificate of the host is also revoked. # If '--all' is specified then all host certificates, both signed and # unsigned, will be removed. # # debug:: # Enable full debugging. # # generate:: # Generate a certificate for a named client. A certificate/keypair will be # generated for each client named on the command line. # # help:: # Print this help message # # list:: # List outstanding certificate requests. If '--all' is specified, # signed certificates are also listed, prefixed by '+', and revoked # or invalid certificates are prefixed by '-' (the verification outcome # is printed in parenthesis). # # print:: # Print the full-text version of a host's certificate. # # fingerprint:: # Print the DIGEST (defaults to md5) fingerprint of a host's certificate. # # revoke:: # Revoke the certificate of a client. The certificate can be specified # either by its serial number, given as a decimal number or a hexadecimal # number prefixed by '0x', or by its hostname. The certificate is revoked # by adding it to the Certificate Revocation List given by the 'cacrl' # config parameter. Note that the puppetmasterd needs to be restarted # after revoking certificates. # # sign:: # Sign an outstanding certificate request. Unless '--all' is specified, # hosts must be listed after all flags. # # verbose:: # Enable verbosity. # # version:: # Print the puppet version number and exit. # # verify:: # Verify the named certificate against the local CA certificate. # # = Example # # $ puppet cert -l # culain.madstop.com # $ puppet cert -s culain.madstop.com # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2005 Puppet Labs, LLC # Licensed under the GNU Public License #Puppet::Application[:cert].run diff --git a/lib/puppet/util/command_line/puppetd b/lib/puppet/util/command_line/puppetd index cb8589c5f..71b28429b 100755 --- a/lib/puppet/util/command_line/puppetd +++ b/lib/puppet/util/command_line/puppetd @@ -1,186 +1,188 @@ #!/usr/bin/env ruby # == Synopsis # # Retrieve the client configuration from the puppet master and apply # it to the local host. # # Currently must be run out periodically, using cron or something similar. # # = Usage # # puppet agent [-D|--daemonize|--no-daemonize] [-d|--debug] # [--detailed-exitcodes] [--disable] [--enable] -# [-h|--help] [--fqdn ] [-l|--logdest syslog||console] +# [-h|--help] [--certname ] [-l|--logdest syslog||console] # [-o|--onetime] [--serve ] [-t|--test] [--noop] # [--digest ] [--fingerprint] [-V|--version] # [-v|--verbose] [-w|--waitforcert ] # # = Description # # This is the main puppet client. Its job is to retrieve the local machine's # configuration from a remote server and apply it. In order to successfully # communicate with the remote server, the client must have a certificate signed # by a certificate authority that the server trusts; the recommended method # for this, at the moment, is to run a certificate authority as part of the # puppet server (which is the default). The client will connect and request # a signed certificate, and will continue connecting until it receives one. # # Once the client has a signed certificate, it will retrieve its configuration # and apply it. # # = Usage Notes # # +puppet agent+ does its best to find a compromise between interactive use and # daemon use. Run with no arguments and no configuration, it will go into the # backgroun, attempt to get a signed certificate, and retrieve and apply its # configuration every 30 minutes. # # Some flags are meant specifically for interactive use -- in particular, # +test+, +tags+ or +fingerprint+ are useful. +test+ enables verbose logging, causes # the daemon to stay in the foreground, exits if the server's configuration is # invalid (this happens if, for instance, you've left a syntax error on the # server), and exits after running the configuration once (rather than hanging # around as a long-running process). # # +tags+ allows you to specify what portions of a configuration you want to apply. # Puppet elements are tagged with all of the class or definition names that # contain them, and you can use the +tags+ flag to specify one of these names, # causing only configuration elements contained within that class or definition # to be applied. This is very useful when you are testing new configurations -- # for instance, if you are just starting to manage +ntpd+, you would put all of # the new elements into an +ntpd+ class, and call puppet with +--tags ntpd+, # which would only apply that small portion of the configuration during your # testing, rather than applying the whole thing. # # +fingerprint+ is a one-time flag. In this mode +puppet agent+ will run once and # display on the console (and in the log) the current certificate (or certificate # request) fingerprint. Providing the +--digest+ option allows to use a different # digest algorithm to generate the fingerprint. The main use is to verify that # before signing a certificate request on the master, the certificate request the # master received is the same as the one the client sent (to prevent against # man-in-the-middle attacks when signing certificates). # # # = Options # # Note that any configuration parameter that's valid in the configuration file # is also a valid long argument. For example, 'server' is a valid configuration # parameter, so you can specify '--server ' as an argument. # # See the configuration file documentation at # http://docs.puppetlabs.com/references/stable/configuration.html for # the full list of acceptable parameters. A commented list of all # configuration options can also be generated by running puppet agent with # '--genconfig'. # # daemonize:: # Send the process into the background. This is the default. # # no-daemonize:: # Do not send the process into the background. # # debug:: # Enable full debugging. # # digest:: # Change the certificate fingerprinting digest algorithm. The default is MD5. # Valid values depends on the version of OpenSSL installed, but should always # at least contain MD5, MD2, SHA1 and SHA256. # # detailed-exitcodes:: # Provide transaction information via exit codes. If this is enabled, an # exit code of '2' means there were changes, and an exit code of '4' means # that there were failures during the transaction. This option only makes # sense in conjunction with --onetime. # # disable:: # Disable working on the local system. This puts a lock file in place, # causing +puppet agent+ not to work on the system until the lock file is removed. # This is useful if you are testing a configuration and do not want the central # configuration to override the local state until everything is tested and # committed. # # +puppet agent+ uses the same lock file while it is running, so no more than one # +puppet agent+ process is working at a time. # # +puppet agent+ exits after executing this. # # enable:: # Enable working on the local system. This removes any lock file, causing # +puppet agent+ to start managing the local system again (although it will continue # to use its normal scheduling, so it might not start for another half hour). # # +puppet agent+ exits after executing this. # -# fqdn:: -# Set the fully-qualified domain name of the client. This is only used for -# certificate purposes, but can be used to override the discovered hostname. -# If you need to use this flag, it is generally an indication of a setup problem. +# certname:: +# Set the certname (unique ID) of the client. The master reads this unique +# identifying string, which is usually set to the node's fully-qualified domain +# name, to determine which configurations the node will receive. Use this option +# to debug setup problems or implement unusual node identification schemes. # # help:: # Print this help message # # logdest:: # Where to send messages. Choose between syslog, the console, and a log file. # Defaults to sending messages to syslog, or the console if debugging or # verbosity is enabled. # # no-client:: # Do not create a config client. This will cause the daemon to run # without ever checking for its configuration automatically, and only # makes sense when used in conjunction with --listen. # # onetime:: # Run the configuration once. Runs a single (normally daemonized) Puppet run. # Useful for interactively running puppet agent when used in conjunction with # the --no-daemonize option. # # fingerprint:: # Display the current certificate or certificate signing request fingerprint # and then exit. Use the +--digest+ option to change the digest algorithm used. # # serve:: # Start another type of server. By default, +puppet agent+ will start # a service handler that allows authenticated and authorized remote nodes to # trigger the configuration to be pulled down and applied. You can specify # any handler here that does not require configuration, e.g., filebucket, ca, # or resource. The handlers are in +lib/puppet/network/handler+, and the names # must match exactly, both in the call to +serve+ and in +namespaceauth.conf+. # # test:: # Enable the most common options used for testing. These are +onetime+, -# +verbose+, +ignorecache, +no-daemonize+, and +no-usecacheonfailure+. +# +verbose+, +ignorecache, +no-daemonize+, +no-usecacheonfailure+, +# +detailed-exit-codes+, +no-splay+, and +show_diff+. # # noop:: # Use +noop+ mode where the daemon runs in a no-op or dry-run mode. This is useful # for seeing what changes Puppet will make without actually executing the changes. # # verbose:: # Turn on verbose reporting. # # version:: # Print the puppet version number and exit. # # waitforcert:: # This option only matters for daemons that do not yet have certificates # and it is enabled by default, with a value of 120 (seconds). This causes # +puppet agent+ to connect to the server every 2 minutes and ask it to sign a # certificate request. This is useful for the initial setup of a puppet # client. You can turn off waiting for certificates by specifying a time # of 0. # # = Example # # puppet agent --server puppet.domain.com # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2005, 2006 Puppet Labs, LLC # Licensed under the GNU Public License #Puppet::Application[:agent].run diff --git a/lib/puppet/util/command_line/puppetdoc b/lib/puppet/util/command_line/puppetdoc index 8f24ea5ef..45a9c6518 100755 --- a/lib/puppet/util/command_line/puppetdoc +++ b/lib/puppet/util/command_line/puppetdoc @@ -1,67 +1,67 @@ #!/usr/bin/env ruby # # = Synopsis # # Generate a reference for all Puppet types. Largely meant for internal Puppet # Labs use. # # = Usage # # puppet doc [-a|--all] [-h|--help] [-o|--outputdir ] [-m|--mode ] # [-r|--reference <[type]|configuration|..>] [--charset CHARSET] [manifest-file] # # = Description # # If mode is not 'rdoc', then this command generates a Markdown document describing all installed # Puppet types or all allowable arguments to puppet executables. It is largely # meant for internal use and is used to generate the reference document # available on the Puppet Labs web site. # # In 'rdoc' mode, this command generates an html RDoc hierarchy describing the manifests that # are in 'manifestdir' and 'modulepath' configuration directives. # The generated documentation directory is doc by default but can be changed with the 'outputdir' option. # # If the command is started with 'manifest-file' command-line arguments, puppet doc generate a single # manifest documentation that is output on stdout. # # = Options # # all:: # Output the docs for all of the reference types. In 'rdoc' modes, this also outputs documentation for all resources # # help:: # Print this help message # # outputdir:: # Specifies the directory where to output the rdoc documentation in 'rdoc' mode. # # mode:: -# Determine the output mode. Valid modes are 'text', 'trac', 'pdf' and 'rdoc'. The 'pdf' mode creates PDF formatted files in the /tmp directory. The default mode is 'text'. In 'rdoc' mode you must provide 'manifests-path' +# Determine the output mode. Valid modes are 'text', 'pdf' and 'rdoc'. The 'pdf' mode creates PDF formatted files in the /tmp directory. The default mode is 'text'. In 'rdoc' mode you must provide 'manifests-path' # # reference:: # Build a particular reference. Get a list of references by running +puppet doc --list+. # # charset:: # Used only in 'rdoc' mode. It sets the charset used in the html files produced. # # = Example # -# $ puppet doc -r type > /tmp/type_reference.rst +# $ puppet doc -r type > /tmp/type_reference.markdown # or # $ puppet doc --outputdir /tmp/rdoc --mode rdoc /path/to/manifests # or # $ puppet doc /etc/puppet/manifests/site.pp # or # $ puppet doc -m pdf -r configuration # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2005-2007 Puppet Labs, LLC # Licensed under the GNU Public License #Puppet::Application[:doc].run diff --git a/lib/puppet/util/command_line/puppetmasterd b/lib/puppet/util/command_line/puppetmasterd index baf8a7581..445169820 100755 --- a/lib/puppet/util/command_line/puppetmasterd +++ b/lib/puppet/util/command_line/puppetmasterd @@ -1,65 +1,74 @@ #!/usr/bin/env ruby # # = Synopsis # # The central puppet server. Functions as a certificate authority by default. # # = Usage # # puppet master [-D|--daemonize|--no-daemonize] [-d|--debug] [-h|--help] # [-l|--logdest |console|syslog] [-v|--verbose] [-V|--version] +# [--compile ] [--apply ] # # = Description # # This is the puppet central daemon. # # = Options # # Note that any configuration parameter that's valid in the configuration file # is also a valid long argument. For example, 'ssldir' is a valid configuration # parameter, so you can specify '--ssldir ' as an argument. # # See the configuration file documentation at # http://docs.puppetlabs.com/references/stable/configuration.html for # the full list of acceptable parameters. A commented list of all # configuration options can also be generated by running puppetmasterdd with # '--genconfig'. # # daemonize:: # Send the process into the background. This is the default. # # no-daemonize:: # Do not send the process into the background. # # debug:: # Enable full debugging. # # help:: # Print this help message. # # logdest:: # Where to send messages. Choose between syslog, the console, and a log file. # Defaults to sending messages to syslog, or the console # if debugging or verbosity is enabled. # # verbose:: # Enable verbosity. # # version:: # Print the puppet version number and exit. # +# compile:: +# Capability to compile a catalogue and output it in JSON from the Puppet master. Uses +# facts contained in the $vardir/yaml/ directory to compile the catalog. +# +# apply:: +# Capability to apply JSON catalog (such as one generated with --compile). You can either specify +# a JSON file or pipe in JSON from standard input. +# # = Example # # puppet master # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2005 Puppet Labs, LLC # Licensed under the GNU Public License #Puppet::Application[:master].run diff --git a/lib/puppet/util/reference.rb b/lib/puppet/util/reference.rb index 95efeb1c1..a4921ed2a 100644 --- a/lib/puppet/util/reference.rb +++ b/lib/puppet/util/reference.rb @@ -1,149 +1,140 @@ require 'puppet/util/instance_loader' require 'fileutils' # Manage Reference Documentation. class Puppet::Util::Reference include Puppet::Util include Puppet::Util::Docs extend Puppet::Util::InstanceLoader instance_load(:reference, 'puppet/reference') def self.footer "\n\n----------------\n\n*This page autogenerated on #{Time.now}*\n" end def self.modes %w{pdf text} end def self.newreference(name, options = {}, &block) ref = self.new(name, options, &block) instance_hash(:reference)[symbolize(name)] = ref ref end def self.page(*sections) depth = 4 # Use the minimum depth sections.each do |name| section = reference(name) or raise "Could not find section #{name}" depth = section.depth if section.depth < depth end end def self.pdf(text) puts "creating pdf" Puppet::Util.secure_open("/tmp/puppetdoc.txt", "w") do |f| f.puts text end rst2latex = which('rst2latex') || which('rst2latex.py') || raise("Could not find rst2latex") cmd = %{#{rst2latex} /tmp/puppetdoc.txt > /tmp/puppetdoc.tex} Puppet::Util.secure_open("/tmp/puppetdoc.tex","w") do |f| # If we get here without an error, /tmp/puppetdoc.tex isn't a tricky cracker's symlink end output = %x{#{cmd}} unless $CHILD_STATUS == 0 $stderr.puts "rst2latex failed" $stderr.puts output exit(1) end $stderr.puts output # Now convert to pdf Dir.chdir("/tmp") do %x{texi2pdf puppetdoc.tex >/dev/null 2>/dev/null} end end def self.references instance_loader(:reference).loadall loaded_instances(:reference).sort { |a,b| a.to_s <=> b.to_s } end HEADER_LEVELS = [nil, "#", "##", "###", "####", "#####"] attr_accessor :page, :depth, :header, :title, :dynamic attr_writer :doc def doc if defined?(@doc) return "#{@name} - #{@doc}" else return @title end end def dynamic? self.dynamic end def h(name, level) "#{HEADER_LEVELS[level]} #{name}\n\n" end def initialize(name, options = {}, &block) @name = name options.each do |option, value| send(option.to_s + "=", value) end meta_def(:generate, &block) # Now handle the defaults @title ||= "#{@name.to_s.capitalize} Reference" @page ||= @title.gsub(/\s+/, '') @depth ||= 2 @header ||= "" end # Indent every line in the chunk except those which begin with '..'. def indent(text, tab) text.gsub(/(^|\A)/, tab).gsub(/^ +\.\./, "..") end def option(name, value) ":#{name.to_s.capitalize}: #{value}\n" end def paramwrap(name, text, options = {}) options[:level] ||= 5 #str = "#{name} : " str = h(name, options[:level]) str += "- **namevar**\n\n" if options[:namevar] str += text #str += text.gsub(/\n/, "\n ") str += "\n\n" end - # Remove all trac links. - def strip_trac(text) - text.gsub(/`\w+\s+([^`]+)`:trac:/) { |m| $1 } - end - def text puts output end - def to_rest(withcontents = true) + def to_markdown(withcontents = true) # First the header text = h(@title, 1) text += "\n\n**This page is autogenerated; any changes will get overwritten** *(last generated on #{Time.now.to_s})*\n\n" text += @header text += generate text += self.class.footer if withcontents text end - - def to_text(withcontents = true) - strip_trac(to_rest(withcontents)) - end end diff --git a/lib/puppet/util/settings.rb b/lib/puppet/util/settings.rb index ca4ecda35..626ed20eb 100644 --- a/lib/puppet/util/settings.rb +++ b/lib/puppet/util/settings.rb @@ -1,932 +1,932 @@ require 'puppet' require 'sync' require 'getoptlong' require 'puppet/external/event-loop' require 'puppet/util/cacher' require 'puppet/util/loadedfile' # The class for handling configuration files. class Puppet::Util::Settings include Enumerable include Puppet::Util::Cacher require 'puppet/util/settings/setting' require 'puppet/util/settings/file_setting' require 'puppet/util/settings/boolean_setting' attr_accessor :file attr_reader :timer ReadOnly = [:run_mode, :name] # Retrieve a config value def [](param) value(param) end # Set a config value. This doesn't set the defaults, it sets the value itself. def []=(param, value) set_value(param, value, :memory) end # Generate the list of valid arguments, in a format that GetoptLong can # understand, and add them to the passed option list. def addargs(options) # Add all of the config parameters as valid options. self.each { |name, setting| setting.getopt_args.each { |args| options << args } } options end # Generate the list of valid arguments, in a format that OptionParser can # understand, and add them to the passed option list. def optparse_addargs(options) # Add all of the config parameters as valid options. self.each { |name, setting| options << setting.optparse_args } options end # Is our parameter a boolean parameter? def boolean?(param) param = param.to_sym !!(@config.include?(param) and @config[param].kind_of? BooleanSetting) end # Remove all set values, potentially skipping cli values. def clear(exceptcli = false) @sync.synchronize do unsafe_clear(exceptcli) end end # Remove all set values, potentially skipping cli values. def unsafe_clear(exceptcli = false) @values.each do |name, values| @values.delete(name) unless exceptcli and name == :cli end # Don't clear the 'used' in this case, since it's a config file reparse, # and we want to retain this info. @used = [] unless exceptcli @cache.clear end # This is mostly just used for testing. def clearused @cache.clear @used = [] end # Do variable interpolation on the value. def convert(value, environment = nil) return value unless value return value unless value.is_a? String newval = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value| varname = $2 || $1 if varname == "environment" and environment environment elsif pval = self.value(varname) pval else raise Puppet::DevError, "Could not find value for #{value}" end end newval end # Return a value's description. def description(name) if obj = @config[name.to_sym] obj.desc else nil end end def each @config.each { |name, object| yield name, object } end # Iterate over each section name. def eachsection yielded = [] @config.each do |name, object| section = object.section unless yielded.include? section yield section yielded << section end end end # Return an object by name. def setting(param) param = param.to_sym @config[param] end # Handle a command-line argument. def handlearg(opt, value = nil) @cache.clear value &&= munge_value(value) str = opt.sub(/^--/,'') bool = true newstr = str.sub(/^no-/, '') if newstr != str str = newstr bool = false end str = str.intern if @config[str].is_a?(Puppet::Util::Settings::BooleanSetting) if value == "" or value.nil? value = bool end end set_value(str, value, :cli) end def include?(name) name = name.intern if name.is_a? String @config.include?(name) end # check to see if a short name is already defined def shortinclude?(short) short = short.intern if name.is_a? String @shortnames.include?(short) end # Create a new collection of config settings. def initialize @config = {} @shortnames = {} @created = [] @searchpath = nil # Mutex-like thing to protect @values @sync = Sync.new # Keep track of set values. @values = Hash.new { |hash, key| hash[key] = {} } # And keep a per-environment cache @cache = Hash.new { |hash, key| hash[key] = {} } # The list of sections we've used. @used = [] end # NOTE: ACS ahh the util classes. . .sigh # as part of a fix for 1183, I pulled the logic for the following 5 methods out of the executables and puppet.rb # They probably deserve their own class, but I don't want to do that until I can refactor environments # its a little better than where they were # Prints the contents of a config file with the available config settings, or it # prints a single value of a config setting. def print_config_options env = value(:environment) val = value(:configprint) if val == "all" hash = {} each do |name, obj| val = value(name,env) val = val.inspect if val == "" hash[name] = val end hash.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, val| puts "#{name} = #{val}" end else val.split(/\s*,\s*/).sort.each do |v| if include?(v) #if there is only one value, just print it for back compatibility if v == val puts value(val,env) break end puts "#{v} = #{value(v,env)}" else puts "invalid parameter: #{v}" return false end end end true end def generate_config puts to_config true end def generate_manifest puts to_manifest true end def print_configs return print_config_options if value(:configprint) != "" return generate_config if value(:genconfig) generate_manifest if value(:genmanifest) end def print_configs? (value(:configprint) != "" || value(:genconfig) || value(:genmanifest)) && true end # Return a given object's file metadata. def metadata(param) if obj = @config[param.to_sym] and obj.is_a?(FileSetting) return [:owner, :group, :mode].inject({}) do |meta, p| if v = obj.send(p) meta[p] = v end meta end else nil end end # Make a directory with the appropriate user, group, and mode def mkdir(default) obj = get_config_file_default(default) Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do mode = obj.mode || 0750 Dir.mkdir(obj.value, mode) end end # Figure out the section name for the run_mode. def run_mode Puppet.run_mode.name end # Return all of the parameters associated with a given section. def params(section = nil) if section section = section.intern if section.is_a? String @config.find_all { |name, obj| obj.section == section }.collect { |name, obj| name } else @config.keys end end # Parse the configuration file. Just provides # thread safety. def parse raise "No :config setting defined; cannot parse unknown config file" unless self[:config] @sync.synchronize do unsafe_parse(self[:config]) end # Create a timer so that this file will get checked automatically # and reparsed if necessary. set_filetimeout_timer end # Unsafely parse the file -- this isn't thread-safe and causes plenty of problems if used directly. def unsafe_parse(file) return unless FileTest.exist?(file) begin data = parse_file(file) rescue => details puts details.backtrace if Puppet[:trace] Puppet.err "Could not parse #{file}: #{details}" return end unsafe_clear(true) metas = {} data.each do |area, values| metas[area] = values.delete(:_meta) values.each do |key,value| set_value(key, value, area, :dont_trigger_handles => true, :ignore_bad_settings => true ) end end # Determine our environment, if we have one. if @config[:environment] env = self.value(:environment).to_sym else env = "none" end # Call any hooks we should be calling. settings_with_hooks.each do |setting| each_source(env) do |source| if value = @values[source][setting.name] # We still have to use value to retrieve the value, since # we want the fully interpolated value, not $vardir/lib or whatever. # This results in extra work, but so few of the settings # will have associated hooks that it ends up being less work this # way overall. setting.handle(self.value(setting.name, env)) break end end end # We have to do it in the reverse of the search path, # because multiple sections could set the same value # and I'm too lazy to only set the metadata once. searchpath.reverse.each do |source| source = run_mode if source == :run_mode source = @name if (@name && source == :name) if meta = metas[source] set_metadata(meta) end end end # Create a new setting. The value is passed in because it's used to determine # what kind of setting we're creating, but the value itself might be either # a default or a value, so we can't actually assign it. def newsetting(hash) klass = nil hash[:section] = hash[:section].to_sym if hash[:section] if type = hash[:type] unless klass = {:setting => Setting, :file => FileSetting, :boolean => BooleanSetting}[type] raise ArgumentError, "Invalid setting type '#{type}'" end hash.delete(:type) else case hash[:default] when true, false, "true", "false" klass = BooleanSetting when /^\$\w+\//, /^\//, /^\w:\// klass = FileSetting when String, Integer, Float # nothing klass = Setting else raise ArgumentError, "Invalid value '#{hash[:default].inspect}' for #{hash[:name]}" end end hash[:settings] = self setting = klass.new(hash) setting end # This has to be private, because it doesn't add the settings to @config private :newsetting # Iterate across all of the objects in a given section. def persection(section) section = section.to_sym self.each { |name, obj| if obj.section == section yield obj end } end # Cache this in an easily clearable way, since we were # having trouble cleaning it up after tests. cached_attr(:file) do if path = self[:config] and FileTest.exist?(path) Puppet::Util::LoadedFile.new(path) end end # Reparse our config file, if necessary. def reparse if file and file.changed? Puppet.notice "Reparsing #{file.file}" parse reuse end end def reuse return unless defined?(@used) @sync.synchronize do # yay, thread-safe new = @used @used = [] self.use(*new) end end # The order in which to search for values. def searchpath(environment = nil) if environment [:cli, :memory, environment, :run_mode, :main, :mutable_defaults] else [:cli, :memory, :run_mode, :main, :mutable_defaults] end end # Get a list of objects per section def sectionlist sectionlist = [] self.each { |name, obj| section = obj.section || "puppet" sections[section] ||= [] sectionlist << section unless sectionlist.include?(section) sections[section] << obj } return sectionlist, sections end def service_user_available? return @service_user_available if defined?(@service_user_available) return @service_user_available = false unless user_name = self[:user] user = Puppet::Type.type(:user).new :name => self[:user], :audit => :ensure @service_user_available = user.exists? end def legacy_to_mode(type, param) if not defined?(@app_names) require 'puppet/util/command_line' command_line = Puppet::Util::CommandLine.new @app_names = Puppet::Util::CommandLine::LegacyName.inject({}) do |hash, pair| app, legacy = pair command_line.require_application app hash[legacy.to_sym] = Puppet::Application.find(app).run_mode.name hash end end if new_type = @app_names[type] Puppet.warning "You have configuration parameter $#{param} specified in [#{type}], which is a deprecated section. I'm assuming you meant [#{new_type}]" return new_type end type end def set_value(param, value, type, options = {}) param = param.to_sym unless setting = @config[param] if options[:ignore_bad_settings] return else raise ArgumentError, "Attempt to assign a value to unknown configuration parameter #{param.inspect}" end end value = setting.munge(value) if setting.respond_to?(:munge) setting.handle(value) if setting.respond_to?(:handle) and not options[:dont_trigger_handles] if ReadOnly.include? param and type != :mutable_defaults raise ArgumentError, "You're attempting to set configuration parameter $#{param}, which is read-only." end type = legacy_to_mode(type, param) @sync.synchronize do # yay, thread-safe @values[type][param] = value @cache.clear clearused # Clear the list of environments, because they cache, at least, the module path. # We *could* preferentially just clear them if the modulepath is changed, # but we don't really know if, say, the vardir is changed and the modulepath # is defined relative to it. We need the defined?(stuff) because of loading # order issues. Puppet::Node::Environment.clear if defined?(Puppet::Node) and defined?(Puppet::Node::Environment) end value end # Set a bunch of defaults in a given section. The sections are actually pretty # pointless, but they help break things up a bit, anyway. def setdefaults(section, defs) section = section.to_sym call = [] defs.each { |name, hash| if hash.is_a? Array unless hash.length == 2 raise ArgumentError, "Defaults specified as an array must contain only the default value and the decription" end tmp = hash hash = {} [:default, :desc].zip(tmp).each { |p,v| hash[p] = v } end name = name.to_sym hash[:name] = name hash[:section] = section raise ArgumentError, "Parameter #{name} is already defined" if @config.include?(name) tryconfig = newsetting(hash) if short = tryconfig.short if other = @shortnames[short] raise ArgumentError, "Parameter #{other.name} is already using short name '#{short}'" end @shortnames[short] = tryconfig end @config[name] = tryconfig # Collect the settings that need to have their hooks called immediately. # We have to collect them so that we can be sure we're fully initialized before # the hook is called. call << tryconfig if tryconfig.call_on_define } call.each { |setting| setting.handle(self.value(setting.name)) } end # Create a timer to check whether the file should be reparsed. def set_filetimeout_timer return unless timeout = self[:filetimeout] and timeout = Integer(timeout) and timeout > 0 timer = EventLoop::Timer.new(:interval => timeout, :tolerance => 1, :start? => true) { self.reparse } end # Convert the settings we manage into a catalog full of resources that model those settings. def to_catalog(*sections) sections = nil if sections.empty? catalog = Puppet::Resource::Catalog.new("Settings") @config.values.find_all { |value| value.is_a?(FileSetting) }.each do |file| next unless (sections.nil? or sections.include?(file.section)) next unless resource = file.to_resource next if catalog.resource(resource.ref) catalog.add_resource(resource) end add_user_resources(catalog, sections) catalog end # Convert our list of config settings into a configuration file. def to_config str = %{The configuration file for #{Puppet[:name]}. Note that this file is likely to have unused configuration parameters in it; any parameter that's valid anywhere in Puppet can be in any config file, even if it's not used. Every section can specify three special parameters: owner, group, and mode. These parameters affect the required permissions of any files specified after their specification. Puppet will sometimes use these parameters to check its own configured state, so they can be used to make Puppet a bit more self-managing. Generated on #{Time.now}. }.gsub(/^/, "# ") # Add a section heading that matches our name. if @config.include?(:run_mode) str += "[#{self[:run_mode]}]\n" end eachsection do |section| persection(section) do |obj| - str += obj.to_config + "\n" unless ReadOnly.include? obj.name + str += obj.to_config + "\n" unless ReadOnly.include? obj.name or obj.name == :genconfig end end return str end # Convert to a parseable manifest def to_manifest catalog = to_catalog catalog.resource_refs.collect do |ref| catalog.resource(ref).to_manifest end.join("\n\n") end # Create the necessary objects to use a section. This is idempotent; # you can 'use' a section as many times as you want. def use(*sections) sections = sections.collect { |s| s.to_sym } @sync.synchronize do # yay, thread-safe sections = sections.reject { |s| @used.include?(s) } return if sections.empty? begin catalog = to_catalog(*sections).to_ral rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not create resources for managing Puppet's files and directories in sections #{sections.inspect}: #{detail}" # We need some way to get rid of any resources created during the catalog creation # but not cleaned up. return end catalog.host_config = false catalog.apply do |transaction| if transaction.any_failed? report = transaction.report failures = report.logs.find_all { |log| log.level == :err } raise "Got #{failures.length} failure(s) while initializing: #{failures.collect { |l| l.to_s }.join("; ")}" end end sections.each { |s| @used << s } @used.uniq! end end def valid?(param) param = param.to_sym @config.has_key?(param) end def uninterpolated_value(param, environment = nil) param = param.to_sym environment &&= environment.to_sym # See if we can find it within our searchable list of values val = catch :foundval do each_source(environment) do |source| # Look for the value. We have to test the hash for whether # it exists, because the value might be false. @sync.synchronize do throw :foundval, @values[source][param] if @values[source].include?(param) end end throw :foundval, nil end # If we didn't get a value, use the default val = @config[param].default if val.nil? val end # Find the correct value using our search path. Optionally accept an environment # in which to search before the other configuration sections. def value(param, environment = nil) param = param.to_sym environment &&= environment.to_sym # Short circuit to nil for undefined parameters. return nil unless @config.include?(param) # Yay, recursion. #self.reparse unless [:config, :filetimeout].include?(param) # Check the cache first. It needs to be a per-environment # cache so that we don't spread values from one env # to another. if cached = @cache[environment||"none"][param] return cached end val = uninterpolated_value(param, environment) if param == :code # if we interpolate code, all hell breaks loose. return val end # Convert it if necessary val = convert(val, environment) # And cache it @cache[environment||"none"][param] = val val end # Open a file with the appropriate user, group, and mode def write(default, *args, &bloc) obj = get_config_file_default(default) writesub(default, value(obj.name), *args, &bloc) end # Open a non-default file under a default dir with the appropriate user, # group, and mode def writesub(default, file, *args, &bloc) obj = get_config_file_default(default) chown = nil if Puppet.features.root? chown = [obj.owner, obj.group] else chown = [nil, nil] end Puppet::Util::SUIDManager.asuser(*chown) do mode = obj.mode || 0640 args << "w" if args.empty? args << mode # Update the umask to make non-executable files Puppet::Util.withumask(File.umask ^ 0111) do File.open(file, *args) do |file| yield file end end end end def readwritelock(default, *args, &bloc) file = value(get_config_file_default(default).name) tmpfile = file + ".tmp" sync = Sync.new raise Puppet::DevError, "Cannot create #{file}; directory #{File.dirname(file)} does not exist" unless FileTest.directory?(File.dirname(tmpfile)) sync.synchronize(Sync::EX) do File.open(file, ::File::CREAT|::File::RDWR, 0600) do |rf| rf.lock_exclusive do if File.exist?(tmpfile) raise Puppet::Error, ".tmp file already exists for #{file}; Aborting locked write. Check the .tmp file and delete if appropriate" end # If there's a failure, remove our tmpfile begin writesub(default, tmpfile, *args, &bloc) rescue File.unlink(tmpfile) if FileTest.exist?(tmpfile) raise end begin File.rename(tmpfile, file) rescue => detail Puppet.err "Could not rename #{file} to #{tmpfile}: #{detail}" File.unlink(tmpfile) if FileTest.exist?(tmpfile) end end end end end private def get_config_file_default(default) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default #{default}" end raise ArgumentError, "Default #{default} is not a file" unless obj.is_a? FileSetting obj end # Create the transportable objects for users and groups. def add_user_resources(catalog, sections) return unless Puppet.features.root? return unless self[:mkusers] @config.each do |name, setting| next unless setting.respond_to?(:owner) next unless sections.nil? or sections.include?(setting.section) if user = setting.owner and user != "root" and catalog.resource(:user, user).nil? resource = Puppet::Resource.new(:user, user, :parameters => {:ensure => :present}) resource[:gid] = self[:group] if self[:group] catalog.add_resource resource end if group = setting.group and ! %w{root wheel}.include?(group) and catalog.resource(:group, group).nil? catalog.add_resource Puppet::Resource.new(:group, group, :parameters => {:ensure => :present}) end end end # Yield each search source in turn. def each_source(environment) searchpath(environment).each do |source| # Modify the source as necessary. source = self.run_mode if source == :run_mode yield source end end # Return all settings that have associated hooks; this is so # we can call them after parsing the configuration file. def settings_with_hooks @config.values.find_all { |setting| setting.respond_to?(:handle) } end # Extract extra setting information for files. def extract_fileinfo(string) result = {} value = string.sub(/\{\s*([^}]+)\s*\}/) do params = $1 params.split(/\s*,\s*/).each do |str| if str =~ /^\s*(\w+)\s*=\s*([\w\d]+)\s*$/ param, value = $1.intern, $2 result[param] = value raise ArgumentError, "Invalid file option '#{param}'" unless [:owner, :mode, :group].include?(param) if param == :mode and value !~ /^\d+$/ raise ArgumentError, "File modes must be numbers" end else raise ArgumentError, "Could not parse '#{string}'" end end '' end result[:value] = value.sub(/\s*$/, '') result end # Convert arguments into booleans, integers, or whatever. def munge_value(value) # Handle different data types correctly return case value when /^false$/i; false when /^true$/i; true when /^\d+$/i; Integer(value) when true; true when false; false else value.gsub(/^["']|["']$/,'').sub(/\s+$/, '') end end # This method just turns a file in to a hash of hashes. def parse_file(file) text = read_file(file) result = Hash.new { |names, name| names[name] = {} } count = 0 # Default to 'main' for the section. section = :main result[section][:_meta] = {} text.split(/\n/).each { |line| count += 1 case line when /^\s*\[(\w+)\]\s*$/ section = $1.intern # Section names # Add a meta section result[section][:_meta] ||= {} when /^\s*#/; next # Skip comments when /^\s*$/; next # Skip blanks when /^\s*(\w+)\s*=\s*(.*?)\s*$/ # settings var = $1.intern # We don't want to munge modes, because they're specified in octal, so we'll # just leave them as a String, since Puppet handles that case correctly. if var == :mode value = $2 else value = munge_value($2) end # Check to see if this is a file argument and it has extra options begin if value.is_a?(String) and options = extract_fileinfo(value) value = options[:value] options.delete(:value) result[section][:_meta][var] = options end result[section][var] = value rescue Puppet::Error => detail detail.file = file detail.line = line raise end else error = Puppet::Error.new("Could not match line #{line}") error.file = file error.line = line raise error end } result end # Read the file in. def read_file(file) begin return File.read(file) rescue Errno::ENOENT raise ArgumentError, "No such file #{file}" rescue Errno::EACCES raise ArgumentError, "Permission denied to file #{file}" end end # Set file metadata. def set_metadata(meta) meta.each do |var, values| values.each do |param, value| @config[var].send(param.to_s + "=", value) end end end end diff --git a/lib/puppet/util/zaml.rb b/lib/puppet/util/zaml.rb index 9fda5ae3b..14aa90e5f 100644 --- a/lib/puppet/util/zaml.rb +++ b/lib/puppet/util/zaml.rb @@ -1,321 +1,318 @@ # # ZAML -- A partial replacement for YAML, writen with speed and code clarity # in mind. ZAML fixes one YAML bug (loading Exceptions) and provides # a replacement for YAML.dump unimaginatively called ZAML.dump, # which is faster on all known cases and an order of magnitude faster # with complex structures. # # http://github.com/hallettj/zaml # # Authors: Markus Roberts, Jesse Hallett, Ian McIntosh, Igal Koshevoy, Simon Chiang # require 'yaml' class ZAML VERSION = "0.1.1" # # Class Methods # def self.dump(stuff, where='') z = new stuff.to_zaml(z) where << z.to_s end # # Instance Methods # def initialize @result = [] @indent = nil @structured_key_prefix = nil Label.counter_reset emit('--- ') end def nested(tail=' ') old_indent = @indent @indent = "#{@indent || "\n"}#{tail}" yield @indent = old_indent end class Label # # YAML only wants objects in the datastream once; if the same object # occurs more than once, we need to emit a label ("&idxxx") on the # first occurrence and then emit a back reference (*idxxx") on any # subsequent occurrence(s). # # To accomplish this we keeps a hash (by object id) of the labels of # the things we serialize as we begin to serialize them. The labels # initially serialize as an empty string (since most objects are only # going to be be encountered once), but can be changed to a valid # (by assigning it a number) the first time it is subsequently used, # if it ever is. Note that we need to do the label setup BEFORE we # start to serialize the object so that circular structures (in # which we will encounter a reference to the object as we serialize # it can be handled). # def self.counter_reset @@previously_emitted_object = {} @@next_free_label_number = 0 end - def initialize(obj,indent) - @indent = indent + def initialize(obj) @this_label_number = nil @@previously_emitted_object[obj.object_id] = self end def to_s - @this_label_number ? ('&id%03d%s' % [@this_label_number, @indent]) : '' + @this_label_number ? ('&id%03d ' % @this_label_number) : '' end def reference @this_label_number ||= (@@next_free_label_number += 1) @reference ||= '*id%03d' % @this_label_number end def self.for(obj) @@previously_emitted_object[obj.object_id] end end def new_label_for(obj) - Label.new(obj,(Hash === obj || Array === obj) ? "#{@indent || "\n"} " : ' ') + Label.new(obj) end def first_time_only(obj) if label = Label.for(obj) emit(label.reference) else if @structured_key_prefix and not obj.is_a? String emit(@structured_key_prefix) @structured_key_prefix = nil end emit(new_label_for(obj)) yield end end def emit(s) @result << s - @recent_nl = false unless s.kind_of?(Label) + @recent_nl = false end def nl(s='') emit(@indent || "\n") unless @recent_nl emit(s) @recent_nl = true end def to_s @result.join end def prefix_structured_keys(x) @structured_key_prefix = x yield nl unless @structured_key_prefix @structured_key_prefix = nil end end ################################################################ # # Behavior for custom classes # ################################################################ class Object def to_yaml_properties instance_variables.sort # Default YAML behavior end def yaml_property_munge(x) x end def zamlized_class_name(root) cls = self.class "!ruby/#{root.name.downcase}#{cls == root ? '' : ":#{cls.respond_to?(:name) ? cls.name : cls}"}" end def to_zaml(z) z.first_time_only(self) { z.emit(zamlized_class_name(Object)) z.nested { instance_variables = to_yaml_properties if instance_variables.empty? z.emit(" {}") else instance_variables.each { |v| z.nl v[1..-1].to_zaml(z) # Remove leading '@' z.emit(': ') yaml_property_munge(instance_variable_get(v)).to_zaml(z) } end } } end end ################################################################ # # Behavior for built-in classes # ################################################################ class NilClass def to_zaml(z) z.emit('') # NOTE: blank turns into nil in YAML.load end end class Symbol def to_zaml(z) z.emit(self.inspect) end end class TrueClass def to_zaml(z) z.emit('true') end end class FalseClass def to_zaml(z) z.emit('false') end end class Numeric def to_zaml(z) z.emit(self) end end class Regexp def to_zaml(z) z.first_time_only(self) { z.emit("#{zamlized_class_name(Regexp)} #{inspect}") } end end class Exception def to_zaml(z) z.emit(zamlized_class_name(Exception)) z.nested { z.nl("message: ") message.to_zaml(z) } end # # Monkey patch for buggy Exception restore in YAML # # This makes it work for now but is not very future-proof; if things # change we'll most likely want to remove this. To mitigate the risks # as much as possible, we test for the bug before appling the patch. # if respond_to? :yaml_new and yaml_new(self, :tag, "message" => "blurp").message != "blurp" def self.yaml_new( klass, tag, val ) o = YAML.object_maker( klass, {} ).exception(val.delete( 'message')) val.each_pair do |k,v| o.instance_variable_set("@#{k}", v) end o end end end class String ZAML_ESCAPES = %w{\x00 \x01 \x02 \x03 \x04 \x05 \x06 \a \x08 \t \n \v \f \r \x0e \x0f \x10 \x11 \x12 \x13 \x14 \x15 \x16 \x17 \x18 \x19 \x1a \e \x1c \x1d \x1e \x1f } def escaped_for_zaml gsub( /\x5C/, "\\\\\\" ). # Demi-kludge for Maglev/rubinius; the regexp should be /\\/ but parsetree chokes on that. gsub( /"/, "\\\"" ). gsub( /([\x00-\x1F])/ ) { |x| ZAML_ESCAPES[ x.unpack("C")[0] ] }. gsub( /([\x80-\xFF])/ ) { |x| "\\x#{x.unpack("C")[0].to_s(16)}" } end def to_zaml(z) - z.first_time_only(self) { - num = '[-+]?(0x)?\d+\.?\d*' - case - when self == '' - z.emit('""') - # when self =~ /[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\xFF]/ - # z.emit("!binary |\n") - # z.emit([self].pack("m*")) - when ( - (self =~ /\A(true|false|yes|no|on|null|off|#{num}(:#{num})*|!|=|~)$/i) or - (self =~ /\A\n* /) or - (self =~ /[\s:]$/) or - (self =~ /^[>|][-+\d]*\s/i) or - (self[-1..-1] =~ /\s/) or - (self =~ /[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\xFF]/) or - (self =~ /[,\[\]\{\}\r\t]|:\s|\s#/) or - (self =~ /\A([-:?!#&*'"]|<<|%.+:.)/) - ) - z.emit("\"#{escaped_for_zaml}\"") - when self =~ /\n/ - if self[-1..-1] == "\n" then z.emit('|+') else z.emit('|-') end - z.nested { split("\n",-1).each { |line| z.nl; z.emit(line.chomp("\n")) } } - else - z.emit(self) - end - } + num = '[-+]?(0x)?\d+\.?\d*' + case + when self == '' + z.emit('""') + # when self =~ /[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\xFF]/ + # z.emit("!binary |\n") + # z.emit([self].pack("m*")) + when ( + (self =~ /\A(true|false|yes|no|on|null|off|#{num}(:#{num})*|!|=|~)$/i) or + (self =~ /\A\n* /) or + (self =~ /[\s:]$/) or + (self =~ /^[>|][-+\d]*\s/i) or + (self[-1..-1] =~ /\s/) or + (self =~ /[\x00-\x08\x0B\x0C\x0E-\x1F\x80-\xFF]/) or + (self =~ /[,\[\]\{\}\r\t]|:\s|\s#/) or + (self =~ /\A([-:?!#&*'"]|<<|%.+:.)/) + ) + z.emit("\"#{escaped_for_zaml}\"") + when self =~ /\n/ + if self[-1..-1] == "\n" then z.emit('|+') else z.emit('|-') end + z.nested { split("\n",-1).each { |line| z.nl; z.emit(line.chomp("\n")) } } + else + z.emit(self) + end end end class Hash def to_zaml(z) z.first_time_only(self) { z.nested { if empty? z.emit('{}') else each_pair { |k, v| z.nl z.prefix_structured_keys('? ') { k.to_zaml(z) } z.emit(': ') v.to_zaml(z) } end } } end end class Array def to_zaml(z) z.first_time_only(self) { z.nested { if empty? z.emit('[]') else each { |v| z.nl('- '); v.to_zaml(z) } end } } end end class Time def to_zaml(z) # 2008-12-06 10:06:51.373758 -07:00 ms = ("%0.6f" % (usec * 1e-6)).sub(/^\d+\./,'') offset = "%+0.2i:%0.2i" % [utc_offset / 3600, (utc_offset / 60) % 60] z.emit(self.strftime("%Y-%m-%d %H:%M:%S.#{ms} #{offset}")) end end class Date def to_zaml(z) z.emit(strftime('%Y-%m-%d')) end end class Range def to_zaml(z) z.first_time_only(self) { z.emit(zamlized_class_name(Range)) z.nested { z.nl z.emit('begin: ') z.emit(first) z.nl z.emit('end: ') z.emit(last) z.nl z.emit('excl: ') z.emit(exclude_end?) } } end end diff --git a/spec/fixtures/unit/provider/mount/mount-output.aix.txt b/spec/fixtures/unit/provider/mount/mount-output.aix.txt new file mode 100644 index 000000000..54edb9c1c --- /dev/null +++ b/spec/fixtures/unit/provider/mount/mount-output.aix.txt @@ -0,0 +1,7 @@ +/dev/hd4 / jfs2 Nov 11 12:11 rw,log=/dev/hd8 +/dev/hd2 /usr jfs2 Nov 11 12:11 rw,log=/dev/hd8 +/dev/hd9var /var jfs2 Nov 11 12:11 rw,log=/dev/hd8 +/dev/hd3 /tmp jfs2 Nov 11 12:11 rw,log=/dev/hd8 +/dev/hd1 /home jfs2 Nov 11 12:11 rw,log=/dev/hd8 +/proc /proc procfs Nov 11 12:11 rw +/dev/hd10opt /opt jfs2 Nov 11 12:11 rw,log=/dev/hd8 diff --git a/spec/integration/reference/providers_spec.rb b/spec/integration/reference/providers_spec.rb index 8b95ca118..c2b1e17c5 100755 --- a/spec/integration/reference/providers_spec.rb +++ b/spec/integration/reference/providers_spec.rb @@ -1,17 +1,17 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/util/reference' reference = Puppet::Util::Reference.reference(:providers) describe reference do it "should exist" do reference.should_not be_nil end - it "should be able to be rendered as text" do - lambda { reference.to_text }.should_not raise_error + it "should be able to be rendered as markdown" do + lambda { reference.to_markdown }.should_not raise_error end end diff --git a/spec/unit/application/doc_spec.rb b/spec/unit/application/doc_spec.rb index 55da5e39a..ed723636b 100755 --- a/spec/unit/application/doc_spec.rb +++ b/spec/unit/application/doc_spec.rb @@ -1,349 +1,349 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/application/doc' require 'puppet/util/reference' require 'puppet/util/rdoc' describe Puppet::Application::Doc do before :each do @doc = Puppet::Application[:doc] @doc.stubs(:puts) @doc.preinit Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:level=) end it "should ask Puppet::Application to not parse Puppet configuration file" do @doc.should_parse_config?.should be_false end it "should declare a other command" do @doc.should respond_to(:other) end it "should declare a rdoc command" do @doc.should respond_to(:rdoc) end it "should declare a fallback for unknown options" do @doc.should respond_to(:handle_unknown) end it "should declare a preinit block" do @doc.should respond_to(:preinit) end describe "in preinit" do it "should set references to []" do @doc.preinit @doc.options[:references].should == [] end it "should init mode to text" do @doc.preinit @doc.options[:mode].should == :text end - it "should init format to to_rest" do + it "should init format to to_markdown" do @doc.preinit - @doc.options[:format].should == :to_rest + @doc.options[:format].should == :to_markdown end end describe "when handling options" do [:all, :outputdir, :verbose, :debug, :charset].each do |option| it "should declare handle_#{option} method" do @doc.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @doc.options.expects(:[]=).with(option, 'arg') @doc.send("handle_#{option}".to_sym, 'arg') end end it "should store the format if valid" do Puppet::Util::Reference.stubs(:method_defined?).with('to_format').returns(true) @doc.options.expects(:[]=).with(:format, 'to_format') @doc.handle_format('format') end it "should raise an error if the format is not valid" do Puppet::Util::Reference.stubs(:method_defined?).with('to_format').returns(false) lambda { @doc.handle_format('format') } end it "should store the mode if valid" do Puppet::Util::Reference.stubs(:modes).returns(stub('mode', :include? => true)) @doc.options.expects(:[]=).with(:mode, :mode) @doc.handle_mode('mode') end it "should store the mode if :rdoc" do Puppet::Util::Reference.modes.stubs(:include?).with('rdoc').returns(false) @doc.options.expects(:[]=).with(:mode, :rdoc) @doc.handle_mode('rdoc') end it "should raise an error if the mode is not valid" do Puppet::Util::Reference.modes.stubs(:include?).with('unknown').returns(false) lambda { @doc.handle_mode('unknown') } end it "should list all references on list and exit" do reference = stubs 'reference' ref = stubs 'ref' Puppet::Util::Reference.stubs(:references).returns([reference]) Puppet::Util::Reference.expects(:reference).with(reference).returns(ref) ref.expects(:doc) @doc.expects(:exit) @doc.handle_list(nil) end it "should add reference to references list with --reference" do @doc.options[:references] = [:ref1] @doc.handle_reference('ref2') @doc.options[:references].should == [:ref1,:ref2] end end describe "during setup" do before :each do Puppet::Log.stubs(:newdestination) @doc.command_line.stubs(:args).returns([]) end it "should default to rdoc mode if there are command line arguments" do @doc.command_line.stubs(:args).returns(["1"]) @doc.stubs(:setup_rdoc) @doc.options.expects(:[]=).with(:mode,:rdoc) @doc.setup end it "should call setup_rdoc in rdoc mode" do @doc.options.stubs(:[]).with(:mode).returns(:rdoc) @doc.expects(:setup_rdoc) @doc.setup end it "should call setup_reference if not rdoc" do @doc.options.stubs(:[]).with(:mode).returns(:test) @doc.expects(:setup_reference) @doc.setup end describe "in non-rdoc mode" do it "should get all non-dynamic reference if --all" do @doc.options.stubs(:[]).with(:all).returns(true) @doc.options.stubs(:[]).with(:references).returns([]) static = stub 'static', :dynamic? => false dynamic = stub 'dynamic', :dynamic? => true Puppet::Util::Reference.stubs(:reference).with(:static).returns(static) Puppet::Util::Reference.stubs(:reference).with(:dynamic).returns(dynamic) Puppet::Util::Reference.stubs(:references).returns([:static,:dynamic]) @doc.options.stubs(:[]=).with(:references, [:static]) @doc.setup_reference end it "should default to :type if no references" do @doc.options.stubs(:[]).with(:all).returns(false) array = stub 'array', :empty? => true @doc.options.stubs(:[]).with(:references).returns(array) array.expects(:<<).with(:type) @doc.setup_reference end end describe "in rdoc mode" do before :each do @doc.options.stubs(:[]).returns(false) Puppet.stubs(:parse_config) Puppet::Util::Log.stubs(:level=) Puppet::Util::Log.stubs(:newdestination) end describe "when there are unknown args" do it "should expand --modulepath if any" do @doc.unknown_args = [ { :opt => "--modulepath", :arg => "path" } ] Puppet.settings.stubs(:handlearg) File.expects(:expand_path).with("path") @doc.setup_rdoc end it "should expand --manifestdir if any" do @doc.unknown_args = [ { :opt => "--manifestdir", :arg => "path" } ] Puppet.settings.stubs(:handlearg) File.expects(:expand_path).with("path") @doc.setup_rdoc end it "should give them to Puppet.settings" do @doc.unknown_args = [ { :opt => :option, :arg => :argument } ] Puppet.settings.expects(:handlearg).with(:option,:argument) @doc.setup_rdoc end end it "should operate in master run_mode" do @doc.class.run_mode.name.should == :master @doc.setup_rdoc end it "should parse puppet configuration" do Puppet.expects(:parse_config) @doc.setup_rdoc end it "should set log level to debug if --debug" do @doc.options.stubs(:[]).with(:debug).returns(true) Puppet::Util::Log.expects(:level=).with(:debug) @doc.setup_rdoc end it "should set log level to info if --verbose" do @doc.options.stubs(:[]).with(:verbose).returns(true) Puppet::Util::Log.expects(:level=).with(:info) @doc.setup_rdoc end it "should set log destination to console if --verbose" do @doc.options.stubs(:[]).with(:verbose).returns(true) Puppet::Util::Log.expects(:newdestination).with(:console) @doc.setup_rdoc end it "should set log destination to console if --debug" do @doc.options.stubs(:[]).with(:debug).returns(true) Puppet::Util::Log.expects(:newdestination).with(:console) @doc.setup_rdoc end end end describe "when running" do describe "in rdoc mode" do before :each do @doc.manifest = false Puppet.stubs(:info) Puppet.stubs(:[]).with(:trace).returns(false) @env = stub 'env' Puppet::Node::Environment.stubs(:new).returns(@env) @env.stubs(:modulepath).returns(['modules']) @env.stubs(:[]).with(:manifest).returns('manifests/site.pp') Puppet.stubs(:[]).with(:modulepath).returns('modules') Puppet.stubs(:[]).with(:manifestdir).returns('manifests') @doc.options.stubs(:[]).with(:all).returns(false) @doc.options.stubs(:[]).with(:outputdir).returns('doc') @doc.options.stubs(:[]).with(:charset).returns(nil) Puppet.settings.stubs(:[]=).with(:document_all, false) Puppet.settings.stubs(:setdefaults) Puppet::Util::RDoc.stubs(:rdoc) @doc.stubs(:exit) File.stubs(:expand_path).with('modules').returns('modules') File.stubs(:expand_path).with('manifests').returns('manifests') @doc.command_line.stubs(:args).returns([]) end it "should set document_all on --all" do @doc.options.expects(:[]).with(:all).returns(true) Puppet.settings.expects(:[]=).with(:document_all, true) @doc.rdoc end it "should call Puppet::Util::RDoc.rdoc in full mode" do Puppet::Util::RDoc.expects(:rdoc).with('doc', ['modules','manifests'], nil) @doc.rdoc end it "should call Puppet::Util::RDoc.rdoc with a charset if --charset has been provided" do @doc.options.expects(:[]).with(:charset).returns("utf-8") Puppet::Util::RDoc.expects(:rdoc).with('doc', ['modules','manifests'], "utf-8") @doc.rdoc end it "should call Puppet::Util::RDoc.rdoc in full mode with outputdir set to doc if no --outputdir" do @doc.options.expects(:[]).with(:outputdir).returns(false) Puppet::Util::RDoc.expects(:rdoc).with('doc', ['modules','manifests'], nil) @doc.rdoc end it "should call Puppet::Util::RDoc.manifestdoc in manifest mode" do @doc.manifest = true Puppet::Util::RDoc.expects(:manifestdoc) @doc.rdoc end it "should get modulepath and manifestdir values from the environment" do @env.expects(:modulepath).returns(['envmodules1','envmodules2']) @env.expects(:[]).with(:manifest).returns('envmanifests/site.pp') Puppet::Util::RDoc.expects(:rdoc).with('doc', ['envmodules1','envmodules2','envmanifests'], nil) @doc.rdoc end end describe "in the other modes" do it "should get reference in given format" do reference = stub 'reference' @doc.options.stubs(:[]).with(:mode).returns(:none) @doc.options.stubs(:[]).with(:references).returns([:ref]) require 'puppet/util/reference' Puppet::Util::Reference.expects(:reference).with(:ref).returns(reference) @doc.options.stubs(:[]).with(:format).returns(:format) @doc.stubs(:exit) reference.expects(:send).with { |format,contents| format == :format }.returns('doc') @doc.other end end end end diff --git a/spec/unit/application_spec.rb b/spec/unit/application_spec.rb index f68a7e209..c0f97336c 100755 --- a/spec/unit/application_spec.rb +++ b/spec/unit/application_spec.rb @@ -1,509 +1,528 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/application' require 'puppet' require 'getoptlong' describe Puppet::Application do before do @app = Class.new(Puppet::Application).new @appclass = @app.class # avoid actually trying to parse any settings Puppet.settings.stubs(:parse) end + describe "finding" do + before do + @klass = Puppet::Application + @klass.stubs(:puts) + end + + it "should find classes in the namespace" do + @klass.find("Agent").should == @klass::Agent + end + + it "should not find classes outside the namespace" do + lambda { @klass.find("String") }.should raise_error(SystemExit) + end + + it "should exit if it can't find a class" do + lambda { @klass.find("ThisShallNeverEverEverExistAsdf") }.should raise_error(SystemExit) + end + end + describe ".run_mode" do it "should default to user" do @appclass.run_mode.name.should == :user end it "should set and get a value" do @appclass.run_mode :agent @appclass.run_mode.name.should == :agent end end it "should have a run entry-point" do @app.should respond_to(:run) end it "should have a read accessor to options" do @app.should respond_to(:options) end it "should include a default setup method" do @app.should respond_to(:setup) end it "should include a default preinit method" do @app.should respond_to(:preinit) end it "should include a default run_command method" do @app.should respond_to(:run_command) end it "should invoke main as the default" do @app.expects( :main ) @app.run_command end describe 'when invoking clear!' do before :each do Puppet::Application.run_status = :stop_requested Puppet::Application.clear! end it 'should have nil run_status' do Puppet::Application.run_status.should be_nil end it 'should return false for restart_requested?' do Puppet::Application.restart_requested?.should be_false end it 'should return false for stop_requested?' do Puppet::Application.stop_requested?.should be_false end it 'should return false for interrupted?' do Puppet::Application.interrupted?.should be_false end it 'should return true for clear?' do Puppet::Application.clear?.should be_true end end describe 'after invoking stop!' do before :each do Puppet::Application.run_status = nil Puppet::Application.stop! end after :each do Puppet::Application.run_status = nil end it 'should have run_status of :stop_requested' do Puppet::Application.run_status.should == :stop_requested end it 'should return true for stop_requested?' do Puppet::Application.stop_requested?.should be_true end it 'should return false for restart_requested?' do Puppet::Application.restart_requested?.should be_false end it 'should return true for interrupted?' do Puppet::Application.interrupted?.should be_true end it 'should return false for clear?' do Puppet::Application.clear?.should be_false end end describe 'when invoking restart!' do before :each do Puppet::Application.run_status = nil Puppet::Application.restart! end after :each do Puppet::Application.run_status = nil end it 'should have run_status of :restart_requested' do Puppet::Application.run_status.should == :restart_requested end it 'should return true for restart_requested?' do Puppet::Application.restart_requested?.should be_true end it 'should return false for stop_requested?' do Puppet::Application.stop_requested?.should be_false end it 'should return true for interrupted?' do Puppet::Application.interrupted?.should be_true end it 'should return false for clear?' do Puppet::Application.clear?.should be_false end end describe 'when performing a controlled_run' do it 'should not execute block if not :clear?' do Puppet::Application.run_status = :stop_requested target = mock 'target' target.expects(:some_method).never Puppet::Application.controlled_run do target.some_method end end it 'should execute block if :clear?' do Puppet::Application.run_status = nil target = mock 'target' target.expects(:some_method).once Puppet::Application.controlled_run do target.some_method end end describe 'on POSIX systems', :if => Puppet.features.posix? do it 'should signal process with HUP after block if restart requested during block execution' do Puppet::Application.run_status = nil target = mock 'target' target.expects(:some_method).once old_handler = trap('HUP') { target.some_method } begin Puppet::Application.controlled_run do Puppet::Application.run_status = :restart_requested end ensure trap('HUP', old_handler) end end end after :each do Puppet::Application.run_status = nil end end describe "when parsing command-line options" do before :each do @app.command_line.stubs(:args).returns([]) Puppet.settings.stubs(:optparse_addargs).returns([]) end it "should pass the banner to the option parser" do option_parser = stub "option parser" option_parser.stubs(:on) option_parser.stubs(:parse!) @app.class.instance_eval do banner "banner" end OptionParser.expects(:new).with("banner").returns(option_parser) @app.parse_options end it "should get options from Puppet.settings.optparse_addargs" do Puppet.settings.expects(:optparse_addargs).returns([]) @app.parse_options end it "should add Puppet.settings options to OptionParser" do Puppet.settings.stubs(:optparse_addargs).returns( [["--option","-o", "Funny Option"]]) Puppet.settings.expects(:handlearg).with("--option", 'true') @app.command_line.stubs(:args).returns(["--option"]) @app.parse_options end it "should ask OptionParser to parse the command-line argument" do @app.command_line.stubs(:args).returns(%w{ fake args }) OptionParser.any_instance.expects(:parse!).with(%w{ fake args }) @app.parse_options end describe "when using --help", :if => Puppet.features.usage? do it "should call RDoc::usage and exit" do @app.expects(:exit) RDoc.expects(:usage).returns(true) @app.handle_help(nil) end end describe "when using --version" do it "should declare a version option" do @app.should respond_to(:handle_version) end it "should exit after printing the version" do @app.stubs(:puts) lambda { @app.handle_version(nil) }.should raise_error(SystemExit) end end describe "when dealing with an argument not declared directly by the application" do it "should pass it to handle_unknown if this method exists" do Puppet.settings.stubs(:optparse_addargs).returns([["--not-handled", :REQUIRED]]) @app.expects(:handle_unknown).with("--not-handled", "value").returns(true) @app.command_line.stubs(:args).returns(["--not-handled", "value"]) @app.parse_options end it "should pass it to Puppet.settings if handle_unknown says so" do Puppet.settings.stubs(:optparse_addargs).returns([["--topuppet", :REQUIRED]]) @app.stubs(:handle_unknown).with("--topuppet", "value").returns(false) Puppet.settings.expects(:handlearg).with("--topuppet", "value") @app.command_line.stubs(:args).returns(["--topuppet", "value"]) @app.parse_options end it "should pass it to Puppet.settings if there is no handle_unknown method" do Puppet.settings.stubs(:optparse_addargs).returns([["--topuppet", :REQUIRED]]) @app.stubs(:respond_to?).returns(false) Puppet.settings.expects(:handlearg).with("--topuppet", "value") @app.command_line.stubs(:args).returns(["--topuppet", "value"]) @app.parse_options end it "should transform boolean false value to string for Puppet.settings" do Puppet.settings.expects(:handlearg).with("--option", "false") @app.handlearg("--option", false) end it "should transform boolean true value to string for Puppet.settings" do Puppet.settings.expects(:handlearg).with("--option", "true") @app.handlearg("--option", true) end it "should transform boolean option to normal form for Puppet.settings" do Puppet.settings.expects(:handlearg).with("--option", "true") @app.handlearg("--[no-]option", true) end it "should transform boolean option to no- form for Puppet.settings" do Puppet.settings.expects(:handlearg).with("--no-option", "false") @app.handlearg("--[no-]option", false) end end it "should exit if OptionParser raises an error" do $stderr.stubs(:puts) OptionParser.any_instance.stubs(:parse!).raises(OptionParser::ParseError.new("blah blah")) @app.expects(:exit) lambda { @app.parse_options }.should_not raise_error end end describe "when calling default setup" do before :each do @app.stubs(:should_parse_config?).returns(false) @app.options.stubs(:[]) end [ :debug, :verbose ].each do |level| it "should honor option #{level}" do @app.options.stubs(:[]).with(level).returns(true) Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.expects(:level=).with(level == :verbose ? :info : :debug) @app.setup end end it "should honor setdest option" do @app.options.stubs(:[]).with(:setdest).returns(false) Puppet::Util::Log.expects(:newdestination).with(:syslog) @app.setup end end describe "when running" do before :each do @app.stubs(:preinit) @app.stubs(:setup) @app.stubs(:parse_options) end it "should call preinit" do @app.stubs(:run_command) @app.expects(:preinit) @app.run end it "should call parse_options" do @app.stubs(:run_command) @app.expects(:parse_options) @app.run end it "should call run_command" do @app.expects(:run_command) @app.run end it "should parse Puppet configuration if should_parse_config is called" do @app.stubs(:run_command) @app.class.should_parse_config Puppet.settings.expects(:parse) @app.run end it "should not parse_option if should_not_parse_config is called" do @app.stubs(:run_command) @app.class.should_not_parse_config Puppet.settings.expects(:parse).never @app.run end it "should parse Puppet configuration if needed" do @app.stubs(:run_command) @app.stubs(:should_parse_config?).returns(true) Puppet.settings.expects(:parse) @app.run end it "should call run_command" do @app.expects(:run_command) @app.run end it "should call main as the default command" do @app.expects(:main) @app.run end it "should warn and exit if no command can be called" do $stderr.expects(:puts) @app.expects(:exit).with(1) @app.run end it "should raise an error if dispatch returns no command" do @app.stubs(:get_command).returns(nil) $stderr.expects(:puts) @app.expects(:exit).with(1) @app.run end it "should raise an error if dispatch returns an invalid command" do @app.stubs(:get_command).returns(:this_function_doesnt_exist) $stderr.expects(:puts) @app.expects(:exit).with(1) @app.run end end describe "when metaprogramming" do describe "when calling option" do it "should create a new method named after the option" do @app.class.option("--test1","-t") do end @app.should respond_to(:handle_test1) end it "should transpose in option name any '-' into '_'" do @app.class.option("--test-dashes-again","-t") do end @app.should respond_to(:handle_test_dashes_again) end it "should create a new method called handle_test2 with option(\"--[no-]test2\")" do @app.class.option("--[no-]test2","-t") do end @app.should respond_to(:handle_test2) end describe "when a block is passed" do it "should create a new method with it" do @app.class.option("--[no-]test2","-t") do raise "I can't believe it, it works!" end lambda { @app.handle_test2 }.should raise_error end it "should declare the option to OptionParser" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.expects(:on).with { |*arg| arg[0] == "--[no-]test3" } @app.class.option("--[no-]test3","-t") do end @app.parse_options end it "should pass a block that calls our defined method" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.stubs(:on).with('--test4','-t').yields(nil) @app.expects(:send).with(:handle_test4, nil) @app.class.option("--test4","-t") do end @app.parse_options end end describe "when no block is given" do it "should declare the option to OptionParser" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.expects(:on).with("--test4","-t") @app.class.option("--test4","-t") @app.parse_options end it "should give to OptionParser a block that adds the the value to the options array" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.stubs(:on).with("--test4","-t").yields(nil) @app.options.expects(:[]=).with(:test4,nil) @app.class.option("--test4","-t") @app.parse_options end end end end end diff --git a/spec/unit/parser/compiler_spec.rb b/spec/unit/parser/compiler_spec.rb index 95f3853e2..687f2ecb9 100755 --- a/spec/unit/parser/compiler_spec.rb +++ b/spec/unit/parser/compiler_spec.rb @@ -1,765 +1,850 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' class CompilerTestResource attr_accessor :builtin, :virtual, :evaluated, :type, :title def initialize(type, title) @type = type @title = title end def [](attr) return nil if attr == :stage :main end def ref "#{type.to_s.capitalize}[#{title}]" end def evaluated? @evaluated end def builtin_type? @builtin end def virtual? @virtual end def evaluate end end describe Puppet::Parser::Compiler do def resource(type, title) Puppet::Parser::Resource.new(type, title, :scope => @scope) end before :each do @node = Puppet::Node.new "testnode" @known_resource_types = Puppet::Resource::TypeCollection.new "development" @compiler = Puppet::Parser::Compiler.new(@node) @scope = Puppet::Parser::Scope.new(:compiler => @compiler, :source => stub('source')) @scope_resource = Puppet::Parser::Resource.new(:file, "/my/file", :scope => @scope) @scope.resource = @scope_resource @compiler.environment.stubs(:known_resource_types).returns @known_resource_types end it "should have a class method that compiles, converts, and returns a catalog" do compiler = stub 'compiler' Puppet::Parser::Compiler.expects(:new).with(@node).returns compiler catalog = stub 'catalog' compiler.expects(:compile).returns catalog converted_catalog = stub 'converted_catalog' catalog.expects(:to_resource).returns converted_catalog Puppet::Parser::Compiler.compile(@node).should equal(converted_catalog) end it "should fail intelligently when a class-level compile fails" do Puppet::Parser::Compiler.expects(:new).raises ArgumentError lambda { Puppet::Parser::Compiler.compile(@node) }.should raise_error(Puppet::Error) end it "should use the node's environment as its environment" do @compiler.environment.should equal(@node.environment) end it "should include the resource type collection helper" do Puppet::Parser::Compiler.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper) end it "should be able to return a class list containing all added classes" do @compiler.add_class "" @compiler.add_class "one" @compiler.add_class "two" @compiler.classlist.sort.should == %w{one two}.sort end describe "when initializing" do it "should set its node attribute" do @compiler.node.should equal(@node) end it "should detect when ast nodes are absent" do @compiler.ast_nodes?.should be_false end it "should detect when ast nodes are present" do @known_resource_types.expects(:nodes?).returns true @compiler.ast_nodes?.should be_true end it "should copy the known_resource_types version to the catalog" do @compiler.catalog.version.should == @known_resource_types.version end it "should copy any node classes into the class list" do node = Puppet::Node.new("mynode") node.classes = %w{foo bar} compiler = Puppet::Parser::Compiler.new(node) - compiler.classlist.should include("foo") - compiler.classlist.should include("bar") + compiler.classlist.should =~ ['foo', 'bar'] + end + + it "should transform node class hashes into a class list" do + node = Puppet::Node.new("mynode") + node.classes = {'foo'=>{'one'=>'1'}, 'bar'=>{'two'=>'2'}} + compiler = Puppet::Parser::Compiler.new(node) + + compiler.classlist.should =~ ['foo', 'bar'] end it "should add a 'main' stage to the catalog" do @compiler.catalog.resource(:stage, :main).should be_instance_of(Puppet::Parser::Resource) end end describe "when managing scopes" do it "should create a top scope" do @compiler.topscope.should be_instance_of(Puppet::Parser::Scope) end it "should be able to create new scopes" do @compiler.newscope(@compiler.topscope).should be_instance_of(Puppet::Parser::Scope) end it "should correctly set the level of newly created scopes" do @compiler.newscope(@compiler.topscope, :level => 5).level.should == 5 end it "should set the parent scope of the new scope to be the passed-in parent" do scope = mock 'scope' newscope = @compiler.newscope(scope) newscope.parent.should equal(scope) end it "should set the parent scope of the new scope to its topscope if the parent passed in is nil" do scope = mock 'scope' newscope = @compiler.newscope(nil) newscope.parent.should equal(@compiler.topscope) end end describe "when compiling" do def compile_methods [:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated, :finish, :store, :extract, :evaluate_relationships] end # Stub all of the main compile methods except the ones we're specifically interested in. def compile_stub(*except) (compile_methods - except).each { |m| @compiler.stubs(m) } end it "should set node parameters as variables in the top scope" do params = {"a" => "b", "c" => "d"} @node.stubs(:parameters).returns(params) compile_stub(:set_node_parameters) @compiler.compile @compiler.topscope.lookupvar("a").should == "b" @compiler.topscope.lookupvar("c").should == "d" end it "should set the client and server versions on the catalog" do params = {"clientversion" => "2", "serverversion" => "3"} @node.stubs(:parameters).returns(params) compile_stub(:set_node_parameters) @compiler.compile @compiler.catalog.client_version.should == "2" @compiler.catalog.server_version.should == "3" end it "should evaluate any existing classes named in the node" do classes = %w{one two three four} main = stub 'main' one = stub 'one', :name => "one" three = stub 'three', :name => "three" @node.stubs(:name).returns("whatever") @node.stubs(:classes).returns(classes) @compiler.expects(:evaluate_classes).with(classes, @compiler.topscope) @compiler.class.publicize_methods(:evaluate_node_classes) { @compiler.evaluate_node_classes } end + it "should evaluate any parameterized classes named in the node" do + classes = {'foo'=>{'1'=>'one'}, 'bar'=>{'2'=>'two'}} + @node.stubs(:classes).returns(classes) + @compiler.expects(:evaluate_classes).with(classes, @compiler.topscope) + @compiler.compile + end + + it "should evaluate the main class if it exists" do compile_stub(:evaluate_main) main_class = @known_resource_types.add Puppet::Resource::Type.new(:hostclass, "") main_class.expects(:evaluate_code).with { |r| r.is_a?(Puppet::Parser::Resource) } @compiler.topscope.expects(:source=).with(main_class) @compiler.compile end it "should create a new, empty 'main' if no main class exists" do compile_stub(:evaluate_main) @compiler.compile @known_resource_types.find_hostclass([""], "").should be_instance_of(Puppet::Resource::Type) end it "should add an edge between the main stage and main class" do @compiler.compile (stage = @compiler.catalog.resource(:stage, "main")).should be_instance_of(Puppet::Parser::Resource) (klass = @compiler.catalog.resource(:class, "")).should be_instance_of(Puppet::Parser::Resource) @compiler.catalog.edge?(stage, klass).should be_true end it "should evaluate any node classes" do @node.stubs(:classes).returns(%w{one two three four}) @compiler.expects(:evaluate_classes).with(%w{one two three four}, @compiler.topscope) @compiler.send(:evaluate_node_classes) end it "should evaluate all added collections" do colls = [] # And when the collections fail to evaluate. colls << mock("coll1-false") colls << mock("coll2-false") colls.each { |c| c.expects(:evaluate).returns(false) } @compiler.add_collection(colls[0]) @compiler.add_collection(colls[1]) compile_stub(:evaluate_generators) @compiler.compile end it "should ignore builtin resources" do resource = resource(:file, "testing") @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end it "should evaluate unevaluated resources" do resource = CompilerTestResource.new(:file, "testing") @compiler.add_resource(@scope, resource) # We have to now mark the resource as evaluated resource.expects(:evaluate).with { |*whatever| resource.evaluated = true } @compiler.compile end it "should not evaluate already-evaluated resources" do resource = resource(:file, "testing") resource.stubs(:evaluated?).returns true @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end it "should evaluate unevaluated resources created by evaluating other resources" do resource = CompilerTestResource.new(:file, "testing") @compiler.add_resource(@scope, resource) resource2 = CompilerTestResource.new(:file, "other") # We have to now mark the resource as evaluated resource.expects(:evaluate).with { |*whatever| resource.evaluated = true; @compiler.add_resource(@scope, resource2) } resource2.expects(:evaluate).with { |*whatever| resource2.evaluated = true } @compiler.compile end describe "when finishing" do before do @compiler.send(:evaluate_main) @catalog = @compiler.catalog end def add_resource(name, parent = nil) resource = Puppet::Parser::Resource.new "file", name, :scope => @scope @compiler.add_resource(@scope, resource) @catalog.add_edge(parent, resource) if parent resource end it "should call finish() on all resources" do # Add a resource that does respond to :finish resource = Puppet::Parser::Resource.new "file", "finish", :scope => @scope resource.expects(:finish) @compiler.add_resource(@scope, resource) # And one that does not dnf_resource = stub_everything "dnf", :ref => "File[dnf]", :type => "file" @compiler.add_resource(@scope, dnf_resource) @compiler.send(:finish) end it "should call finish() in add_resource order" do resources = sequence('resources') resource1 = add_resource("finish1") resource1.expects(:finish).in_sequence(resources) resource2 = add_resource("finish2") resource2.expects(:finish).in_sequence(resources) @compiler.send(:finish) end it "should add each container's metaparams to its contained resources" do main = @catalog.resource(:class, :main) main[:noop] = true resource1 = add_resource("meh", main) @compiler.send(:finish) resource1[:noop].should be_true end it "should add metaparams recursively" do main = @catalog.resource(:class, :main) main[:noop] = true resource1 = add_resource("meh", main) resource2 = add_resource("foo", resource1) @compiler.send(:finish) resource2[:noop].should be_true end it "should prefer metaparams from immediate parents" do main = @catalog.resource(:class, :main) main[:noop] = true resource1 = add_resource("meh", main) resource2 = add_resource("foo", resource1) resource1[:noop] = false @compiler.send(:finish) resource2[:noop].should be_false end it "should merge tags downward" do main = @catalog.resource(:class, :main) main.tag("one") resource1 = add_resource("meh", main) resource1.tag "two" resource2 = add_resource("foo", resource1) @compiler.send(:finish) resource2.tags.should be_include("one") resource2.tags.should be_include("two") end it "should work if only middle resources have metaparams set" do main = @catalog.resource(:class, :main) resource1 = add_resource("meh", main) resource1[:noop] = true resource2 = add_resource("foo", resource1) @compiler.send(:finish) resource2[:noop].should be_true end end it "should return added resources in add order" do resource1 = resource(:file, "yay") @compiler.add_resource(@scope, resource1) resource2 = resource(:file, "youpi") @compiler.add_resource(@scope, resource2) @compiler.resources.should == [resource1, resource2] end it "should add resources that do not conflict with existing resources" do resource = resource(:file, "yay") @compiler.add_resource(@scope, resource) @compiler.catalog.should be_vertex(resource) end it "should fail to add resources that conflict with existing resources" do path = Puppet.features.posix? ? "/foo" : "C:/foo" file1 = Puppet::Type.type(:file).new :path => path file2 = Puppet::Type.type(:file).new :path => path @compiler.add_resource(@scope, file1) lambda { @compiler.add_resource(@scope, file2) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError) end it "should add an edge from the scope resource to the added resource" do resource = resource(:file, "yay") @compiler.add_resource(@scope, resource) @compiler.catalog.should be_edge(@scope.resource, resource) end it "should add an edge to any specified stage for class resources" do other_stage = resource(:stage, "other") @compiler.add_resource(@scope, other_stage) resource = resource(:class, "foo") resource[:stage] = 'other' @compiler.add_resource(@scope, resource) @compiler.catalog.edge?(other_stage, resource).should be_true end it "should fail if a non-class resource attempts to set a stage" do other_stage = resource(:stage, "other") @compiler.add_resource(@scope, other_stage) resource = resource(:file, "foo") resource[:stage] = 'other' lambda { @compiler.add_resource(@scope, resource) }.should raise_error(ArgumentError) end it "should fail if an unknown stage is specified" do resource = resource(:class, "foo") resource[:stage] = 'other' lambda { @compiler.add_resource(@scope, resource) }.should raise_error(ArgumentError) end it "should add edges from the class resources to the parent's stage if no stage is specified" do main = @compiler.catalog.resource(:stage, :main) foo_stage = resource(:stage, :foo_stage) @compiler.add_resource(@scope, foo_stage) resource = resource(:class, "foo") @scope.stubs(:resource).returns(:stage => :foo_stage) @compiler.add_resource(@scope, resource) @compiler.catalog.should be_edge(foo_stage, resource) end it "should add edges from top-level class resources to the main stage if no stage is specified" do main = @compiler.catalog.resource(:stage, :main) resource = resource(:class, "foo") @compiler.add_resource(@scope, resource) @compiler.catalog.should be_edge(main, resource) end it "should not add non-class resources that don't specify a stage to the 'main' stage" do main = @compiler.catalog.resource(:stage, :main) resource = resource(:file, "foo") @compiler.add_resource(@scope, resource) @compiler.catalog.should_not be_edge(main, resource) end it "should not add any parent-edges to stages" do stage = resource(:stage, "other") @compiler.add_resource(@scope, stage) @scope.resource = resource(:class, "foo") @compiler.catalog.edge?(@scope.resource, stage).should be_false end it "should not attempt to add stages to other stages" do other_stage = resource(:stage, "other") second_stage = resource(:stage, "second") @compiler.add_resource(@scope, other_stage) @compiler.add_resource(@scope, second_stage) second_stage[:stage] = "other" @compiler.catalog.edge?(other_stage, second_stage).should be_false end it "should have a method for looking up resources" do resource = resource(:yay, "foo") @compiler.add_resource(@scope, resource) @compiler.findresource("Yay[foo]").should equal(resource) end it "should be able to look resources up by type and title" do resource = resource(:yay, "foo") @compiler.add_resource(@scope, resource) @compiler.findresource("Yay", "foo").should equal(resource) end it "should not evaluate virtual defined resources" do resource = resource(:file, "testing") resource.virtual = true @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end end describe "when evaluating collections" do it "should evaluate each collection" do 2.times { |i| coll = mock 'coll%s' % i @compiler.add_collection(coll) # This is the hard part -- we have to emulate the fact that # collections delete themselves if they are done evaluating. coll.expects(:evaluate).with do @compiler.delete_collection(coll) end } @compiler.class.publicize_methods(:evaluate_collections) { @compiler.evaluate_collections } end it "should not fail when there are unevaluated resource collections that do not refer to specific resources" do coll = stub 'coll', :evaluate => false coll.expects(:resources).returns(nil) @compiler.add_collection(coll) lambda { @compiler.compile }.should_not raise_error end it "should fail when there are unevaluated resource collections that refer to a specific resource" do coll = stub 'coll', :evaluate => false coll.expects(:resources).returns(:something) @compiler.add_collection(coll) - lambda { @compiler.compile }.should raise_error(Puppet::ParseError) + lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Failed to realize virtual resources something' end it "should fail when there are unevaluated resource collections that refer to multiple specific resources" do coll = stub 'coll', :evaluate => false coll.expects(:resources).returns([:one, :two]) @compiler.add_collection(coll) - lambda { @compiler.compile }.should raise_error(Puppet::ParseError) + lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Failed to realize virtual resources one, two' end end describe "when evaluating relationships" do it "should evaluate each relationship with its catalog" do dep = stub 'dep' dep.expects(:evaluate).with(@compiler.catalog) @compiler.add_relationship dep @compiler.evaluate_relationships end end describe "when told to evaluate missing classes" do it "should fail if there's no source listed for the scope" do scope = stub 'scope', :source => nil proc { @compiler.evaluate_classes(%w{one two}, scope) }.should raise_error(Puppet::DevError) end it "should tag the catalog with the name of each not-found class" do @compiler.catalog.expects(:tag).with("notfound") @scope.expects(:find_hostclass).with("notfound").returns(nil) @compiler.evaluate_classes(%w{notfound}, @scope) end + # I wish it would fail + it "should log when it can't find class" do + klasses = {'foo'=>nil} + @node.classes = klasses + @compiler.topscope.stubs(:find_hostclass).with('foo').returns(nil) + Puppet.expects(:info).with('Could not find class foo for testnode') + @compiler.compile + end end describe "when evaluating found classes" do before do @class = stub 'class', :name => "my::class" @scope.stubs(:find_hostclass).with("myclass").returns(@class) @resource = stub 'resource', :ref => "Class[myclass]", :type => "file" end it "should evaluate each class" do @compiler.catalog.stubs(:tag) @class.expects(:ensure_in_catalog).with(@scope) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope) end + it "should ensure each node class hash is in catalog and have appropriate parameters" do + klasses = {'foo'=>{'1'=>'one'}, 'bar::foo'=>{'2'=>'two'}, 'bar'=>{'1'=> [1,2,3], '2'=>{'foo'=>'bar'}}} + @node.classes = klasses + ast_obj = Puppet::Parser::AST::String.new(:value => 'foo') + klasses.each do |name, params| + klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'1' => ast_obj, '2' => ast_obj}) + @compiler.topscope.known_resource_types.add klass + end + catalog = @compiler.compile + catalog.classes.should =~ ['foo', 'bar::foo', 'settings', 'bar'] + + r1 = catalog.resources.detect {|r| r.title == 'Foo' } + r1.to_hash.should == {:'1' => 'one', :'2' => 'foo'} + r1.tags. should =~ ['class', 'foo'] + + r2 = catalog.resources.detect {|r| r.title == 'Bar::Foo' } + r2.to_hash.should == {:'1' => 'foo', :'2' => 'two'} + r2.tags.should =~ ['bar::foo', 'class', 'bar', 'foo'] + + r2 = catalog.resources.detect {|r| r.title == 'Bar' } + r2.to_hash.should == {:'1' => [1,2,3], :'2' => {'foo'=>'bar'}} + r2.tags.should =~ ['class', 'bar'] + end + + it "should ensure each node class is in catalog and has appropriate tags" do + klasses = ['bar::foo'] + @node.classes = klasses + ast_obj = Puppet::Parser::AST::String.new(:value => 'foo') + klasses.each do |name| + klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'1' => ast_obj, '2' => ast_obj}) + @compiler.topscope.known_resource_types.add klass + end + catalog = @compiler.compile + + r2 = catalog.resources.detect {|r| r.title == 'Bar::Foo' } + r2.tags.should =~ ['bar::foo', 'class', 'bar', 'foo'] + end + + it "should fail if required parameters are missing" do + klass = {'foo'=>{'1'=>'one'}} + @node.classes = klass + klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {'1' => nil, '2' => nil}) + @compiler.topscope.known_resource_types.add klass + lambda { @compiler.compile }.should raise_error Puppet::ParseError, "Must pass 2 to Class[Foo]" + end + + it "should fail if invalid parameters are passed" do + klass = {'foo'=>{'3'=>'one'}} + @node.classes = klass + klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {'1' => nil, '2' => nil}) + @compiler.topscope.known_resource_types.add klass + lambda { @compiler.compile }.should raise_error Puppet::ParseError, "Invalid parameter 3" + end + + it "should ensure class is in catalog without params" do + @node.classes = klasses = {'foo'=>nil} + foo = Puppet::Resource::Type.new(:hostclass, 'foo') + @compiler.topscope.known_resource_types.add foo + catalog = @compiler.compile + catalog.classes.should include 'foo' + end + it "should not evaluate the resources created for found classes unless asked" do @compiler.catalog.stubs(:tag) @resource.expects(:evaluate).never @class.expects(:ensure_in_catalog).returns(@resource) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope) end it "should immediately evaluate the resources created for found classes when asked" do @compiler.catalog.stubs(:tag) @resource.expects(:evaluate) @class.expects(:ensure_in_catalog).returns(@resource) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope, false) end it "should skip classes that have already been evaluated" do @compiler.catalog.stubs(:tag) @scope.stubs(:class_scope).with(@class).returns("something") @compiler.expects(:add_resource).never @resource.expects(:evaluate).never Puppet::Parser::Resource.expects(:new).never @compiler.evaluate_classes(%w{myclass}, @scope, false) end it "should skip classes previously evaluated with different capitalization" do @compiler.catalog.stubs(:tag) @scope.stubs(:find_hostclass).with("MyClass").returns(@class) @scope.stubs(:class_scope).with(@class).returns("something") @compiler.expects(:add_resource).never @resource.expects(:evaluate).never Puppet::Parser::Resource.expects(:new).never @compiler.evaluate_classes(%w{MyClass}, @scope, false) end it "should return the list of found classes" do @compiler.catalog.stubs(:tag) @compiler.stubs(:add_resource) @scope.stubs(:find_hostclass).with("notfound").returns(nil) @scope.stubs(:class_scope).with(@class) Puppet::Parser::Resource.stubs(:new).returns(@resource) @class.stubs :ensure_in_catalog @compiler.evaluate_classes(%w{myclass notfound}, @scope).should == %w{myclass} end end describe "when evaluating AST nodes with no AST nodes present" do it "should do nothing" do @compiler.expects(:ast_nodes?).returns(false) @compiler.known_resource_types.expects(:nodes).never Puppet::Parser::Resource.expects(:new).never @compiler.send(:evaluate_ast_node) end end describe "when evaluating AST nodes with AST nodes present" do before do @compiler.known_resource_types.stubs(:nodes?).returns true # Set some names for our test @node.stubs(:names).returns(%w{a b c}) @compiler.known_resource_types.stubs(:node).with("a").returns(nil) @compiler.known_resource_types.stubs(:node).with("b").returns(nil) @compiler.known_resource_types.stubs(:node).with("c").returns(nil) # It should check this last, of course. @compiler.known_resource_types.stubs(:node).with("default").returns(nil) end it "should fail if the named node cannot be found" do proc { @compiler.send(:evaluate_ast_node) }.should raise_error(Puppet::ParseError) end it "should evaluate the first node class matching the node name" do node_class = stub 'node', :name => "c", :evaluate_code => nil @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node" node_class.expects(:ensure_in_catalog).returns(node_resource) @compiler.compile end it "should match the default node if no matching node can be found" do node_class = stub 'node', :name => "default", :evaluate_code => nil @compiler.known_resource_types.stubs(:node).with("default").returns(node_class) node_resource = stub 'node resource', :ref => "Node[default]", :evaluate => nil, :type => "node" node_class.expects(:ensure_in_catalog).returns(node_resource) @compiler.compile end it "should evaluate the node resource immediately rather than using lazy evaluation" do node_class = stub 'node', :name => "c" @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]", :type => "node" node_class.expects(:ensure_in_catalog).returns(node_resource) node_resource.expects(:evaluate) @compiler.send(:evaluate_ast_node) end it "should set the node's scope as the top scope" do node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node" node_class = stub 'node', :name => "c", :ensure_in_catalog => node_resource @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) # The #evaluate method normally does this. scope = stub 'scope', :source => "mysource" @compiler.topscope.expects(:class_scope).with(node_class).returns(scope) node_resource.stubs(:evaluate) @compiler.stubs :create_settings_scope @compiler.compile @compiler.topscope.should equal(scope) end end describe "when managing resource overrides" do before do @override = stub 'override', :ref => "File[/foo]", :type => "my" @resource = resource(:file, "/foo") end it "should be able to store overrides" do lambda { @compiler.add_override(@override) }.should_not raise_error end it "should apply overrides to the appropriate resources" do @compiler.add_resource(@scope, @resource) @resource.expects(:merge).with(@override) @compiler.add_override(@override) @compiler.compile end it "should accept overrides before the related resource has been created" do @resource.expects(:merge).with(@override) # First store the override @compiler.add_override(@override) # Then the resource @compiler.add_resource(@scope, @resource) # And compile, so they get resolved @compiler.compile end it "should fail if the compile is finished and resource overrides have not been applied" do @compiler.add_override(@override) - lambda { @compiler.compile }.should raise_error(Puppet::ParseError) + lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Could not find resource(s) File[/foo] for overriding' end end end diff --git a/spec/unit/property/keyvalue_spec.rb b/spec/unit/property/keyvalue_spec.rb index 7666def56..a44d891d7 100644 --- a/spec/unit/property/keyvalue_spec.rb +++ b/spec/unit/property/keyvalue_spec.rb @@ -1,168 +1,168 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/property/keyvalue' klass = Puppet::Property::KeyValue describe klass do it "should be a subclass of Property" do klass.superclass.must == Puppet::Property end describe "as an instance" do before do # Wow that's a messy interface to the resource. klass.initvars @resource = stub 'resource', :[]= => nil, :property => nil @property = klass.new(:resource => @resource) end it "should have a , as default delimiter" do @property.delimiter.should == ";" end it "should have a = as default separator" do @property.separator.should == "=" end it "should have a :membership as default membership" do @property.membership.should == :key_value_membership end it "should return the same value passed into should_to_s" do @property.should_to_s({:foo => "baz", :bar => "boo"}) == "foo=baz;bar=boo" end it "should return the passed in array values joined with the delimiter from is_to_s" do @property.is_to_s({"foo" => "baz" , "bar" => "boo"}).should == "foo=baz;bar=boo" end describe "when calling inclusive?" do it "should use the membership method to look up on the @resource" do @property.expects(:membership).returns(:key_value_membership) @resource.expects(:[]).with(:key_value_membership) @property.inclusive? end it "should return true when @resource[membership] == inclusive" do @property.stubs(:membership).returns(:key_value_membership) @resource.stubs(:[]).with(:key_value_membership).returns(:inclusive) @property.inclusive?.must == true end it "should return false when @resource[membership] != inclusive" do @property.stubs(:membership).returns(:key_value_membership) @resource.stubs(:[]).with(:key_value_membership).returns(:minimum) @property.inclusive?.must == false end end describe "when calling process_current_hash" do it "should return {} if hash is :absent" do @property.process_current_hash(:absent).must == {} end it "should set every key to nil if inclusive?" do @property.stubs(:inclusive?).returns(true) @property.process_current_hash({:foo => "bar", :do => "re"}).must == { :foo => nil, :do => nil } end it "should return the hash if !inclusive?" do @property.stubs(:inclusive?).returns(false) @property.process_current_hash({:foo => "bar", :do => "re"}).must == {:foo => "bar", :do => "re"} end end describe "when calling should" do it "should return nil if @should is nil" do @property.should.must == nil end it "should call process_current_hash" do @property.should = ["foo=baz", "bar=boo"] @property.stubs(:retrieve).returns({:do => "re", :mi => "fa" }) @property.expects(:process_current_hash).returns({}) @property.should end it "should return the hashed values of @should and the nilled values of retrieve if inclusive" do @property.should = ["foo=baz", "bar=boo"] @property.expects(:retrieve).returns({:do => "re", :mi => "fa" }) @property.expects(:inclusive?).returns(true) @property.should.must == { :foo => "baz", :bar => "boo", :do => nil, :mi => nil } end it "should return the hashed @should + the unique values of retrieve if !inclusive" do @property.should = ["foo=baz", "bar=boo"] @property.expects(:retrieve).returns({:foo => "diff", :do => "re", :mi => "fa"}) @property.expects(:inclusive?).returns(false) @property.should.must == { :foo => "baz", :bar => "boo", :do => "re", :mi => "fa" } end end describe "when calling retrieve" do before do @provider = mock("provider") @property.stubs(:provider).returns(@provider) end it "should send 'name' to the provider" do @provider.expects(:send).with(:keys) @property.expects(:name).returns(:keys) @property.retrieve end it "should return a hash with the provider returned info" do @provider.stubs(:send).with(:keys).returns({"do" => "re", "mi" => "fa" }) @property.stubs(:name).returns(:keys) @property.retrieve == {"do" => "re", "mi" => "fa" } end it "should return :absent when the provider returns :absent" do @provider.stubs(:send).with(:keys).returns(:absent) @property.stubs(:name).returns(:keys) @property.retrieve == :absent end end describe "when calling hashify" do it "should return the array hashified" do @property.hashify(["foo=baz", "bar=boo"]).must == { :foo => "baz", :bar => "boo" } end end - describe "when calling insync?" do + describe "when calling safe_insync?" do before do @provider = mock("provider") @property.stubs(:provider).returns(@provider) @property.stubs(:name).returns(:prop_name) end it "should return true unless @should is defined and not nil" do - @property.insync?("foo") == true + @property.safe_insync?("foo") == true end it "should return true if the passed in values is nil" do @property.should = "foo" - @property.insync?(nil) == true + @property.safe_insync?(nil) == true end it "should return true if hashified should value == (retrieved) value passed in" do @provider.stubs(:prop_name).returns({ :foo => "baz", :bar => "boo" }) @property.should = ["foo=baz", "bar=boo"] @property.expects(:inclusive?).returns(true) - @property.insync?({ :foo => "baz", :bar => "boo" }).must == true + @property.safe_insync?({ :foo => "baz", :bar => "boo" }).must == true end it "should return false if prepared value != should value" do @provider.stubs(:prop_name).returns({ "foo" => "bee", "bar" => "boo" }) @property.should = ["foo=baz", "bar=boo"] @property.expects(:inclusive?).returns(true) - @property.insync?({ "foo" => "bee", "bar" => "boo" }).must == false + @property.safe_insync?({ "foo" => "bee", "bar" => "boo" }).must == false end end end end diff --git a/spec/unit/property/list_spec.rb b/spec/unit/property/list_spec.rb index 3e8cc5402..c6c5db10e 100644 --- a/spec/unit/property/list_spec.rb +++ b/spec/unit/property/list_spec.rb @@ -1,166 +1,166 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/property/list' list_class = Puppet::Property::List describe list_class do it "should be a subclass of Property" do list_class.superclass.must == Puppet::Property end describe "as an instance" do before do # Wow that's a messy interface to the resource. list_class.initvars @resource = stub 'resource', :[]= => nil, :property => nil @property = list_class.new(:resource => @resource) end it "should have a , as default delimiter" do @property.delimiter.should == "," end it "should have a :membership as default membership" do @property.membership.should == :membership end it "should return the same value passed into should_to_s" do @property.should_to_s("foo") == "foo" end it "should return the passed in array values joined with the delimiter from is_to_s" do @property.is_to_s(["foo","bar"]).should == "foo,bar" end it "should be able to correctly convert ':absent' to a string" do @property.is_to_s(:absent).should == "absent" end describe "when adding should to current" do it "should add the arrays when current is an array" do @property.add_should_with_current(["foo"], ["bar"]).should == ["foo", "bar"] end it "should return should if current is not a array" do @property.add_should_with_current(["foo"], :absent).should == ["foo"] end it "should return only the uniq elements" do @property.add_should_with_current(["foo", "bar"], ["foo", "baz"]).should == ["foo", "bar", "baz"] end end describe "when calling inclusive?" do it "should use the membership method to look up on the @resource" do @property.expects(:membership).returns(:membership) @resource.expects(:[]).with(:membership) @property.inclusive? end it "should return true when @resource[membership] == inclusive" do @property.stubs(:membership).returns(:membership) @resource.stubs(:[]).with(:membership).returns(:inclusive) @property.inclusive?.must == true end it "should return false when @resource[membership] != inclusive" do @property.stubs(:membership).returns(:membership) @resource.stubs(:[]).with(:membership).returns(:minimum) @property.inclusive?.must == false end end describe "when calling should" do it "should return nil if @should is nil" do @property.should.must == nil end it "should return the sorted values of @should as a string if inclusive" do @property.should = ["foo", "bar"] @property.expects(:inclusive?).returns(true) @property.should.must == "bar,foo" end it "should return the uniq sorted values of @should + retrieve as a string if !inclusive" do @property.should = ["foo", "bar"] @property.expects(:inclusive?).returns(false) @property.expects(:retrieve).returns(["foo","baz"]) @property.should.must == "bar,baz,foo" end end describe "when calling retrieve" do before do @provider = mock("provider") @property.stubs(:provider).returns(@provider) end it "should send 'name' to the provider" do @provider.expects(:send).with(:group) @property.expects(:name).returns(:group) @property.retrieve end it "should return an array with the provider returned info" do @provider.stubs(:send).with(:group).returns("foo,bar,baz") @property.stubs(:name).returns(:group) @property.retrieve == ["foo", "bar", "baz"] end it "should return :absent when the provider returns :absent" do @provider.stubs(:send).with(:group).returns(:absent) @property.stubs(:name).returns(:group) @property.retrieve == :absent end end - describe "when calling insync?" do + describe "when calling safe_insync?" do it "should return true unless @should is defined and not nil" do - @property.must be_insync("foo") + @property.must be_safe_insync("foo") end it "should return true unless the passed in values is not nil" do @property.should = "foo" - @property.must be_insync(nil) + @property.must be_safe_insync(nil) end it "should call prepare_is_for_comparison with value passed in and should" do @property.should = "foo" @property.expects(:prepare_is_for_comparison).with("bar") @property.expects(:should) - @property.insync?("bar") + @property.safe_insync?("bar") end it "should return true if 'is' value is array of comma delimited should values" do @property.should = "bar,foo" @property.expects(:inclusive?).returns(true) - @property.must be_insync(["bar","foo"]) + @property.must be_safe_insync(["bar","foo"]) end it "should return true if 'is' value is :absent and should value is empty string" do @property.should = "" @property.expects(:inclusive?).returns(true) - @property.must be_insync([]) + @property.must be_safe_insync([]) end it "should return false if prepared value != should value" do @property.should = "bar,baz,foo" @property.expects(:inclusive?).returns(true) - @property.must_not be_insync(["bar","foo"]) + @property.must_not be_safe_insync(["bar","foo"]) end end describe "when calling dearrayify" do it "should sort and join the array with 'delimiter'" do array = mock "array" array.expects(:sort).returns(array) array.expects(:join).with(@property.delimiter) @property.dearrayify(array) end end end end diff --git a/spec/unit/provider/mount_spec.rb b/spec/unit/provider/mount_spec.rb index b034214ee..f567a4a40 100755 --- a/spec/unit/provider/mount_spec.rb +++ b/spec/unit/provider/mount_spec.rb @@ -1,137 +1,145 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/provider/mount' describe Puppet::Provider::Mount do before :each do @mounter = Object.new @mounter.extend(Puppet::Provider::Mount) @name = "/" @resource = stub 'resource' @resource.stubs(:[]).with(:name).returns(@name) @mounter.stubs(:resource).returns(@resource) end describe Puppet::Provider::Mount, " when mounting" do it "should use the 'mountcmd' method to mount" do @mounter.stubs(:options).returns(nil) @mounter.expects(:mountcmd) @mounter.mount end it "should flush before mounting if a flush method exists" do @mounter.meta_def(:flush) { } @mounter.expects(:flush) @mounter.stubs(:mountcmd) @mounter.stubs(:options).returns(nil) @mounter.mount end it "should add the options following '-o' if they exist and are not set to :absent" do @mounter.stubs(:options).returns("ro") @mounter.expects(:mountcmd).with { |*ary| ary[0] == "-o" and ary[1] == "ro" } @mounter.mount end it "should specify the filesystem name to the mount command" do @mounter.stubs(:options).returns(nil) @mounter.expects(:mountcmd).with { |*ary| ary[-1] == @name } @mounter.mount end end describe Puppet::Provider::Mount, " when remounting" do it "should use '-o remount' if the resource specifies it supports remounting" do @mounter.stubs(:info) @resource.stubs(:[]).with(:remounts).returns(:true) @mounter.expects(:mountcmd).with("-o", "remount", @name) @mounter.remount end it "should unmount and mount if the resource does not specify it supports remounting" do @mounter.stubs(:info) @resource.stubs(:[]).with(:remounts).returns(false) @mounter.expects(:unmount) @mounter.expects(:mount) @mounter.remount end it "should log that it is remounting" do @resource.stubs(:[]).with(:remounts).returns(:true) @mounter.stubs(:mountcmd) @mounter.expects(:info).with("Remounting") @mounter.remount end end describe Puppet::Provider::Mount, " when unmounting" do it "should call the :umount command with the resource name" do @mounter.expects(:umount).with(@name) @mounter.unmount end end describe Puppet::Provider::Mount, " when determining if it is mounted" do it "should parse the results of running the mount command with no arguments" do Facter.stubs(:value).returns("whatever") @mounter.expects(:mountcmd).returns("") @mounter.mounted? end it "should match ' on /private/var/automount' if the operating system is Darwin" do Facter.stubs(:value).with("operatingsystem").returns("Darwin") @mounter.expects(:mountcmd).returns("/dev/whatever on /private/var/automount/\ndevfs on /dev") @mounter.should be_mounted end it "should match ' on ' if the operating system is Darwin" do Facter.stubs(:value).with("operatingsystem").returns("Darwin") @mounter.expects(:mountcmd).returns("/dev/disk03 on / (local, journaled)\ndevfs on /dev") @mounter.should be_mounted end it "should match '^ on' if the operating system is Solaris" do Facter.stubs(:value).with("operatingsystem").returns("Solaris") @mounter.expects(:mountcmd).returns("/ on /dev/dsk/whatever\n/var on /dev/dsk/other") @mounter.should be_mounted end it "should match '^ on' if the operating system is HP-UX" do Facter.stubs(:value).with("operatingsystem").returns("HP-UX") @mounter.expects(:mountcmd).returns("/ on /dev/dsk/whatever\n/var on /dev/dsk/other") @mounter.should be_mounted end + it "should match mounted devices if the operating system is AIX" do + Facter.stubs(:value).with("operatingsystem").returns("AIX") + mount_data = File.read(File.join(File.dirname(__FILE__), '..', '..', 'fixtures', 'unit', 'provider', 'mount', 'mount-output.aix.txt')) + @mounter.expects(:mountcmd).returns(mount_data) + + @mounter.should be_mounted + end + it "should match ' on ' if the operating system is not Darwin, Solaris, or HP-UX" do Facter.stubs(:value).with("operatingsystem").returns("Debian") @mounter.expects(:mountcmd).returns("/dev/dsk/whatever on / and stuff\n/dev/other/disk on /var and stuff") @mounter.should be_mounted end it "should not be considered mounted if it did not match the mount output" do Facter.stubs(:value).with("operatingsystem").returns("Debian") @mounter.expects(:mountcmd).returns("/dev/dsk/whatever on /something/else and stuff\n/dev/other/disk on /var and stuff") @mounter.should_not be_mounted end end end diff --git a/spec/unit/provider/nameservice/directoryservice_spec.rb b/spec/unit/provider/nameservice/directoryservice_spec.rb new file mode 100755 index 000000000..661899db9 --- /dev/null +++ b/spec/unit/provider/nameservice/directoryservice_spec.rb @@ -0,0 +1,38 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +# We use this as a reasonable way to obtain all the support infrastructure. +[:user, :group].each do |type_for_this_round| + provider_class = Puppet::Type.type(type_for_this_round).provider(:directoryservice) + + describe provider_class do + before do + @resource = stub("resource") + @provider = provider_class.new(@resource) + end + + it "[#6009] should handle nested arrays of members" do + current = ["foo", "bar", "baz"] + desired = ["foo", ["quux"], "qorp"] + group = 'example' + + @resource.stubs(:[]).with(:name).returns(group) + @resource.stubs(:[]).with(:auth_membership).returns(true) + @provider.instance_variable_set(:@property_value_cache_hash, + { :members => current }) + + %w{bar baz}.each do |del| + @provider.expects(:execute).once. + with([:dseditgroup, '-o', 'edit', '-n', '.', '-d', del, group]) + end + + %w{quux qorp}.each do |add| + @provider.expects(:execute).once. + with([:dseditgroup, '-o', 'edit', '-n', '.', '-a', add, group]) + end + + expect { @provider.set(:members, desired) }.should_not raise_error + end + end +end diff --git a/spec/unit/provider/package/freebsd_spec.rb b/spec/unit/provider/package/freebsd_spec.rb new file mode 100755 index 000000000..0d38a16cf --- /dev/null +++ b/spec/unit/provider/package/freebsd_spec.rb @@ -0,0 +1,55 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +provider_class = Puppet::Type.type(:package).provider(:freebsd) + +describe provider_class do + before :each do + # Create a mock resource + @resource = stub 'resource' + + # A catch all; no parameters set + @resource.stubs(:[]).returns(nil) + + # But set name and source + @resource.stubs(:[]).with(:name).returns "mypackage" + @resource.stubs(:[]).with(:ensure).returns :installed + + @provider = provider_class.new + @provider.resource = @resource + end + + it "should have an install method" do + @provider = provider_class.new + @provider.should respond_to(:install) + end + + describe "when installing" do + before :each do + @resource.stubs(:should).with(:ensure).returns(:installed) + end + + it "should install a package from a path to a directory" do + # For better or worse, trailing '/' is needed. --daniel 2011-01-26 + path = '/path/to/directory/' + @resource.stubs(:[]).with(:source).returns(path) + Puppet::Util::Execution.expects(:withenv).once.with({:PKG_PATH => path}).yields + @provider.expects(:pkgadd).once.with("mypackage") + + expect { @provider.install }.should_not raise_error + end + + %w{http https ftp}.each do |protocol| + it "should install a package via #{protocol}" do + # For better or worse, trailing '/' is needed. --daniel 2011-01-26 + path = "#{protocol}://localhost/" + @resource.stubs(:[]).with(:source).returns(path) + Puppet::Util::Execution.expects(:withenv).once.with({:PACKAGESITE => path}).yields + @provider.expects(:pkgadd).once.with('-r', "mypackage") + + expect { @provider.install }.should_not raise_error + end + end + end +end diff --git a/spec/unit/resource/type_spec.rb b/spec/unit/resource/type_spec.rb index 7b240bb82..87b4ab420 100755 --- a/spec/unit/resource/type_spec.rb +++ b/spec/unit/resource/type_spec.rb @@ -1,740 +1,786 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/resource/type' describe Puppet::Resource::Type do 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| 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_pson(json) end def double_convert Puppet::Resource::Type.from_pson(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 include any arguments" do @type.set_arguments("one" => nil, "two" => "foo") double_convert.arguments.should == {"one" => nil, "two" => "foo"} end it "should include any extra attributes" do @type.file = "/my/file" @type.line = 50 double_convert.file.should == "/my/file" double_convert.line.should == 50 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 a 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 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 it "should return the name converted to a string when the name is not a regex" do pending "Need to define LoadedCode behaviour first" name = Puppet::Parser::AST::HostName.new(:value => "foo") Puppet::Resource::Type.new(:node, name).name.should == "foo" end it "should return the name converted to a string when the name is a regex" do pending "Need to define LoadedCode behaviour first" name = Puppet::Parser::AST::HostName.new(:value => /regex/) Puppet::Resource::Type.new(:node, name).name.should == /regex/.to_s end it "should mark any created scopes as a node scope" do pending "Need to define LoadedCode behaviour first" name = Puppet::Parser::AST::HostName.new(:value => /regex/) Puppet::Resource::Type.new(:node, name).name.should == /regex/.to_s 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 creating a subscope" do before do @scope = stub 'scope', :newscope => nil @resource = stub 'resource' @type = Puppet::Resource::Type.new(:hostclass, "foo") end it "should return a new scope created with the provided scope as the parent" do @scope.expects(:newscope).returns "foo" @type.subscope(@scope, @resource).should == "foo" end it "should set the source as itself" do @scope.expects(:newscope).with { |args| args[:source] == @type } @type.subscope(@scope, @resource) end it "should set the scope's namespace to its namespace" do @type.expects(:namespace).returns "yayness" @scope.expects(:newscope).with { |args| args[:namespace] == "yayness" } @type.subscope(@scope, @resource) end it "should set the scope's resource to the provided resource" do @scope.expects(:newscope).with { |args| args[:resource] == @resource } @type.subscope(@scope, @resource) end end describe "when setting its parameters in the scope" do before do @scope = Puppet::Parser::Scope.new(:compiler => stub("compiler", :environment => Puppet::Node::Environment.new), :source => stub("source")) @resource = Puppet::Parser::Resource.new(:foo, "bar", :scope => @scope) @type = Puppet::Resource::Type.new(:hostclass, "foo") end + ['module_name', 'name', 'title'].each do |variable| + it "should allow #{variable} to be evaluated as param default" do + @type.module_name = "bar" + var = Puppet::Parser::AST::Variable.new({'value' => variable}) + @type.set_arguments :foo => var + @type.set_resource_parameters(@resource, @scope) + @scope.lookupvar('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 "resource should 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.lookupvar('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.lookupvar("foo").should == "bar" @scope.lookupvar("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.lookupvar("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 => stub("value", :safeevaluate => "something") @type.set_resource_parameters(@resource, @scope) @scope.lookupvar("foo").should == "something" end it "should set all default values as parameters in the resource" do @type.set_arguments :foo => stub("value", :safeevaluate => "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 @resource.expects(:to_hash).returns({}) 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.lookupvar("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.lookupvar("name").should == "bar" end it "should set its module name in the scope if available" do @type.module_name = "mymod" @type.set_resource_parameters(@resource, @scope) @scope.lookupvar("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.lookupvar("caller_module_name").should == "mycaller" end end describe "when describing and managing parent classes" do before do @code = Puppet::Resource::TypeCollection.new("env") @parent = Puppet::Resource::Type.new(:hostclass, "bar") @code.add @parent @child = Puppet::Resource::Type.new(:hostclass, "foo", :parent => "bar") @code.add @child @env = stub "environment", :known_resource_types => @code @scope = stub "scope", :environment => @env, :namespaces => [""] 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") @code.add parent child = Puppet::Resource::Type.new(:node, "foo", :parent => "bar") @code.add child child.parent_type(@scope).should equal(parent) end it "should cache a reference to the parent type" do @code.stubs(:hostclass).with("foo::bar").returns nil @code.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") @code.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") @code.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 => @compiler @resource = Puppet::Parser::Resource.new(:foo, "yay", :scope => @scope) # This is so the internal resource lookup works, yo. @compiler.catalog.add_resource @resource @known_resource_types = stub 'known_resource_types' @resource.stubs(:known_resource_types).returns @known_resource_types @type = Puppet::Resource::Type.new(:hostclass, "foo") 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 add node names to the classes list" do @type = Puppet::Resource::Type.new(:node, "foo") @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 @type.expects(:subscope).with(@scope, @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 @type.stubs(:subscope).returns stub_everything("subscope", :compiler => @compiler) code.expects(:safeevaluate).with @type.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.stubs(:node).with("parent").returns(@parent_type) @type.resource_type_collection.stubs(:node).with("Parent").returns(@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 => @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 turn its code into an ASTArray if necessary" do dest = Puppet::Resource::Type.new(:hostclass, "bar", :code => code("foo")) source = Puppet::Resource::Type.new(:hostclass, "foo", :code => code("bar")) dest.merge(source) dest.code.should be_instance_of(Puppet::Parser::AST::ASTArray) 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::ASTArray.new :children => [code("dest")] dest = Puppet::Resource::Type.new(:hostclass, "bar", :code => dcode) scode = Puppet::Parser::AST::ASTArray.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/resource_spec.rb b/spec/unit/resource_spec.rb index 877b6b6b0..ff31b2492 100755 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -1,778 +1,786 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/resource' describe Puppet::Resource do before do @basepath = Puppet.features.posix? ? "/somepath" : "C:/somepath" end [:catalog, :file, :line].each do |attr| it "should have an #{attr} attribute" do resource = Puppet::Resource.new("file", "/my/file") resource.should respond_to(attr) resource.should respond_to(attr.to_s + "=") end end it "should have a :title attribute" do Puppet::Resource.new(:user, "foo").title.should == "foo" end it "should require the type and title" do lambda { Puppet::Resource.new }.should raise_error(ArgumentError) end it "should canonize types to capitalized strings" do Puppet::Resource.new(:user, "foo").type.should == "User" end it "should canonize qualified types so all strings are capitalized" do Puppet::Resource.new("foo::bar", "foo").type.should == "Foo::Bar" end it "should tag itself with its type" do Puppet::Resource.new("file", "/f").should be_tagged("file") end it "should tag itself with its title if the title is a valid tag" do Puppet::Resource.new("user", "bar").should be_tagged("bar") end it "should not tag itself with its title if the title is a not valid tag" do Puppet::Resource.new("file", "/bar").should_not be_tagged("/bar") end it "should allow setting of attributes" do Puppet::Resource.new("file", "/bar", :file => "/foo").file.should == "/foo" Puppet::Resource.new("file", "/bar", :exported => true).should be_exported end it "should set its type to 'Class' and its title to the passed title if the passed type is :component and the title has no square brackets in it" do ref = Puppet::Resource.new(:component, "foo") ref.type.should == "Class" ref.title.should == "Foo" end it "should interpret the title as a reference and assign appropriately if the type is :component and the title contains square brackets" do ref = Puppet::Resource.new(:component, "foo::bar[yay]") ref.type.should == "Foo::Bar" ref.title.should == "yay" end it "should set the type to 'Class' if it is nil and the title contains no square brackets" do ref = Puppet::Resource.new(nil, "yay") ref.type.should == "Class" ref.title.should == "Yay" end it "should interpret the title as a reference and assign appropriately if the type is nil and the title contains square brackets" do ref = Puppet::Resource.new(nil, "foo::bar[yay]") ref.type.should == "Foo::Bar" ref.title.should == "yay" end it "should interpret the title as a reference and assign appropriately if the type is nil and the title contains nested square brackets" do ref = Puppet::Resource.new(nil, "foo::bar[baz[yay]]") ref.type.should == "Foo::Bar" ref.title.should =="baz[yay]" end it "should interpret the type as a reference and assign appropriately if the title is nil and the type contains square brackets" do ref = Puppet::Resource.new("foo::bar[baz]") ref.type.should == "Foo::Bar" ref.title.should =="baz" end it "should be able to extract its information from a Puppet::Type instance" do ral = Puppet::Type.type(:file).new :path => @basepath+"/foo" ref = Puppet::Resource.new(ral) ref.type.should == "File" ref.title.should == @basepath+"/foo" end it "should fail if the title is nil and the type is not a valid resource reference string" do lambda { Puppet::Resource.new("foo") }.should raise_error(ArgumentError) end + it 'should fail if strict is set and type does not exist' do + lambda { Puppet::Resource.new('foo', 'title', {:strict=>true}) }.should raise_error(ArgumentError, 'Invalid resource type foo') + end + + it 'should fail if strict is set and class does not exist' do + lambda { Puppet::Resource.new('Class', 'foo', {:strict=>true}) }.should raise_error(ArgumentError, 'Could not find declared class foo') + end + it "should fail if the title is a hash and the type is not a valid resource reference string" do lambda { Puppet::Resource.new({:type => "foo", :title => "bar"}) }.should raise_error(ArgumentError, 'Puppet::Resource.new does not take a hash as the first argument. Did you mean ("foo", "bar") ?' ) end it "should be able to produce a backward-compatible reference array" do Puppet::Resource.new("foobar", "/f").to_trans_ref.should == %w{Foobar /f} end it "should be taggable" do Puppet::Resource.ancestors.should be_include(Puppet::Util::Tagging) end it "should have an 'exported' attribute" do resource = Puppet::Resource.new("file", "/f") resource.exported = true resource.exported.should == true resource.should be_exported end it "should support an environment attribute" do Puppet::Resource.new("file", "/my/file", :environment => :foo).environment.name.should == :foo end describe "and munging its type and title" do describe "when modeling a builtin resource" do it "should be able to find the resource type" do Puppet::Resource.new("file", "/my/file").resource_type.should equal(Puppet::Type.type(:file)) end it "should set its type to the capitalized type name" do Puppet::Resource.new("file", "/my/file").type.should == "File" end end describe "when modeling a defined resource" do describe "that exists" do before do @type = Puppet::Resource::Type.new(:definition, "foo::bar") Puppet::Node::Environment.new.known_resource_types.add @type end it "should set its type to the capitalized type name" do Puppet::Resource.new("foo::bar", "/my/file").type.should == "Foo::Bar" end it "should be able to find the resource type" do Puppet::Resource.new("foo::bar", "/my/file").resource_type.should equal(@type) end it "should set its title to the provided title" do Puppet::Resource.new("foo::bar", "/my/file").title.should == "/my/file" end end describe "that does not exist" do it "should set its resource type to the capitalized resource type name" do Puppet::Resource.new("foo::bar", "/my/file").type.should == "Foo::Bar" end end end describe "when modeling a node" do # Life's easier with nodes, because they can't be qualified. it "should set its type to 'Node' and its title to the provided title" do node = Puppet::Resource.new("node", "foo") node.type.should == "Node" node.title.should == "foo" end end describe "when modeling a class" do it "should set its type to 'Class'" do Puppet::Resource.new("class", "foo").type.should == "Class" end describe "that exists" do before do @type = Puppet::Resource::Type.new(:hostclass, "foo::bar") Puppet::Node::Environment.new.known_resource_types.add @type end it "should set its title to the capitalized, fully qualified resource type" do Puppet::Resource.new("class", "foo::bar").title.should == "Foo::Bar" end it "should be able to find the resource type" do Puppet::Resource.new("class", "foo::bar").resource_type.should equal(@type) end end describe "that does not exist" do it "should set its type to 'Class' and its title to the capitalized provided name" do klass = Puppet::Resource.new("class", "foo::bar") klass.type.should == "Class" klass.title.should == "Foo::Bar" end end describe "and its name is set to the empty string" do it "should set its title to :main" do Puppet::Resource.new("class", "").title.should == :main end describe "and a class exists whose name is the empty string" do # this was a bit tough to track down it "should set its title to :main" do @type = Puppet::Resource::Type.new(:hostclass, "") Puppet::Node::Environment.new.known_resource_types.add @type Puppet::Resource.new("class", "").title.should == :main end end end describe "and its name is set to :main" do it "should set its title to :main" do Puppet::Resource.new("class", :main).title.should == :main end describe "and a class exists whose name is the empty string" do # this was a bit tough to track down it "should set its title to :main" do @type = Puppet::Resource::Type.new(:hostclass, "") Puppet::Node::Environment.new.known_resource_types.add @type Puppet::Resource.new("class", :main).title.should == :main end end end end end it "should return nil when looking up resource types that don't exist" do Puppet::Resource.new("foobar", "bar").resource_type.should be_nil end it "should not fail when an invalid parameter is used and strict mode is disabled" do type = Puppet::Resource::Type.new(:definition, "foobar") Puppet::Node::Environment.new.known_resource_types.add type resource = Puppet::Resource.new("foobar", "/my/file") resource[:yay] = true end it "should be considered equivalent to another resource if their type and title match and no parameters are set" do Puppet::Resource.new("file", "/f").should == Puppet::Resource.new("file", "/f") end it "should be considered equivalent to another resource if their type, title, and parameters are equal" do Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"}).should == Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"}) end it "should not be considered equivalent to another resource if their type and title match but parameters are different" do Puppet::Resource.new("file", "/f", :parameters => {:fee => "baz"}).should_not == Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"}) end it "should not be considered equivalent to a non-resource" do Puppet::Resource.new("file", "/f").should_not == "foo" end it "should not be considered equivalent to another resource if their types do not match" do Puppet::Resource.new("file", "/f").should_not == Puppet::Resource.new("exec", "/f") end it "should not be considered equivalent to another resource if their titles do not match" do Puppet::Resource.new("file", "/foo").should_not == Puppet::Resource.new("file", "/f") end describe "when referring to a resource with name canonicalization" do it "should canonicalize its own name" do res = Puppet::Resource.new("file", "/path/") res.uniqueness_key.should == ["/path"] res.ref.should == "File[/path/]" end end describe "when running in strict mode" do it "should be strict" do Puppet::Resource.new("file", "/path", :strict => true).should be_strict end it "should fail if invalid parameters are used" do lambda { Puppet::Resource.new("file", "/path", :strict => true, :parameters => {:nosuchparam => "bar"}) }.should raise_error end it "should fail if the resource type cannot be resolved" do lambda { Puppet::Resource.new("nosuchtype", "/path", :strict => true) }.should raise_error end end describe "when managing parameters" do before do @resource = Puppet::Resource.new("file", "/my/file") end it "should correctly detect when provided parameters are not valid for builtin types" do Puppet::Resource.new("file", "/my/file").should_not be_valid_parameter("foobar") end it "should correctly detect when provided parameters are valid for builtin types" do Puppet::Resource.new("file", "/my/file").should be_valid_parameter("mode") end it "should correctly detect when provided parameters are not valid for defined resource types" do type = Puppet::Resource::Type.new(:definition, "foobar") Puppet::Node::Environment.new.known_resource_types.add type Puppet::Resource.new("foobar", "/my/file").should_not be_valid_parameter("myparam") end it "should correctly detect when provided parameters are valid for defined resource types" do type = Puppet::Resource::Type.new(:definition, "foobar", :arguments => {"myparam" => nil}) Puppet::Node::Environment.new.known_resource_types.add type Puppet::Resource.new("foobar", "/my/file").should be_valid_parameter("myparam") end it "should allow setting and retrieving of parameters" do @resource[:foo] = "bar" @resource[:foo].should == "bar" end it "should allow setting of parameters at initialization" do Puppet::Resource.new("file", "/my/file", :parameters => {:foo => "bar"})[:foo].should == "bar" end it "should canonicalize retrieved parameter names to treat symbols and strings equivalently" do @resource[:foo] = "bar" @resource["foo"].should == "bar" end it "should canonicalize set parameter names to treat symbols and strings equivalently" do @resource["foo"] = "bar" @resource[:foo].should == "bar" end it "should set the namevar when asked to set the name" do resource = Puppet::Resource.new("user", "bob") Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar] resource[:name] = "bob" resource[:myvar].should == "bob" end it "should return the namevar when asked to return the name" do resource = Puppet::Resource.new("user", "bob") Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar] resource[:myvar] = "test" resource[:name].should == "test" end it "should be able to set the name for non-builtin types" do resource = Puppet::Resource.new(:foo, "bar") resource[:name] = "eh" lambda { resource[:name] = "eh" }.should_not raise_error end it "should be able to return the name for non-builtin types" do resource = Puppet::Resource.new(:foo, "bar") resource[:name] = "eh" resource[:name].should == "eh" end it "should be able to iterate over parameters" do @resource[:foo] = "bar" @resource[:fee] = "bare" params = {} @resource.each do |key, value| params[key] = value end params.should == {:foo => "bar", :fee => "bare"} end it "should include Enumerable" do @resource.class.ancestors.should be_include(Enumerable) end it "should have a method for testing whether a parameter is included" do @resource[:foo] = "bar" @resource.should be_has_key(:foo) @resource.should_not be_has_key(:eh) end it "should have a method for providing the list of parameters" do @resource[:foo] = "bar" @resource[:bar] = "foo" keys = @resource.keys keys.should be_include(:foo) keys.should be_include(:bar) end it "should have a method for providing the number of parameters" do @resource[:foo] = "bar" @resource.length.should == 1 end it "should have a method for deleting parameters" do @resource[:foo] = "bar" @resource.delete(:foo) @resource[:foo].should be_nil end it "should have a method for testing whether the parameter list is empty" do @resource.should be_empty @resource[:foo] = "bar" @resource.should_not be_empty end it "should be able to produce a hash of all existing parameters" do @resource[:foo] = "bar" @resource[:fee] = "yay" hash = @resource.to_hash hash[:foo].should == "bar" hash[:fee].should == "yay" end it "should not provide direct access to the internal parameters hash when producing a hash" do hash = @resource.to_hash hash[:foo] = "bar" @resource[:foo].should be_nil end it "should use the title as the namevar to the hash if no namevar is present" do resource = Puppet::Resource.new("user", "bob") Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar] resource.to_hash[:myvar].should == "bob" end it "should set :name to the title if :name is not present for non-builtin types" do krt = Puppet::Resource::TypeCollection.new("myenv") krt.add Puppet::Resource::Type.new(:definition, :foo) resource = Puppet::Resource.new :foo, "bar" resource.stubs(:known_resource_types).returns krt resource.to_hash[:name].should == "bar" end end describe "when serializing" do before do @resource = Puppet::Resource.new("file", "/my/file") @resource["one"] = "test" @resource["two"] = "other" end it "should be able to be dumped to yaml" do proc { YAML.dump(@resource) }.should_not raise_error end it "should produce an equivalent yaml object" do text = YAML.dump(@resource) newresource = YAML.load(text) newresource.title.should == @resource.title newresource.type.should == @resource.type %w{one two}.each do |param| newresource[param].should == @resource[param] end end end describe "when converting to a RAL resource" do it "should use the resource type's :new method to create the resource if the resource is of a builtin type" do resource = Puppet::Resource.new("file", @basepath+"/my/file") result = resource.to_ral result.should be_instance_of(Puppet::Type.type(:file)) result[:path].should == @basepath+"/my/file" end it "should convert to a component instance if the resource type is not of a builtin type" do resource = Puppet::Resource.new("foobar", "somename") result = resource.to_ral result.should be_instance_of(Puppet::Type.type(:component)) result.title.should == "Foobar[somename]" end end it "should be able to convert itself to Puppet code" do Puppet::Resource.new("one::two", "/my/file").should respond_to(:to_manifest) end describe "when converting to puppet code" do before do @resource = Puppet::Resource.new("one::two", "/my/file", :parameters => {:noop => true, :foo => %w{one two}}) end it "should print the type and title" do @resource.to_manifest.should be_include("one::two { '/my/file':\n") end it "should print each parameter, with the value single-quoted" do @resource.to_manifest.should be_include(" noop => 'true'") end it "should print array values appropriately" do @resource.to_manifest.should be_include(" foo => ['one','two']") end end it "should be able to convert itself to a TransObject instance" do Puppet::Resource.new("one::two", "/my/file").should respond_to(:to_trans) end describe "when converting to a TransObject" do describe "and the resource is not an instance of a builtin type" do before do @resource = Puppet::Resource.new("foo", "bar") end it "should return a simple TransBucket if it is not an instance of a builtin type" do bucket = @resource.to_trans bucket.should be_instance_of(Puppet::TransBucket) bucket.type.should == @resource.type bucket.name.should == @resource.title end it "should return a simple TransBucket if it is a stage" do @resource = Puppet::Resource.new("stage", "bar") bucket = @resource.to_trans bucket.should be_instance_of(Puppet::TransBucket) bucket.type.should == @resource.type bucket.name.should == @resource.title end it "should copy over the resource's file" do @resource.file = "/foo/bar" @resource.to_trans.file.should == "/foo/bar" end it "should copy over the resource's line" do @resource.line = 50 @resource.to_trans.line.should == 50 end end describe "and the resource is an instance of a builtin type" do before do @resource = Puppet::Resource.new("file", "bar") end it "should return a TransObject if it is an instance of a builtin resource type" do trans = @resource.to_trans trans.should be_instance_of(Puppet::TransObject) trans.type.should == "file" trans.name.should == @resource.title end it "should copy over the resource's file" do @resource.file = "/foo/bar" @resource.to_trans.file.should == "/foo/bar" end it "should copy over the resource's line" do @resource.line = 50 @resource.to_trans.line.should == 50 end # Only TransObjects support tags, annoyingly it "should copy over the resource's tags" do @resource.tag "foo" @resource.to_trans.tags.should == @resource.tags end it "should copy the resource's parameters into the transobject and convert the parameter name to a string" do @resource[:foo] = "bar" @resource.to_trans["foo"].should == "bar" end it "should be able to copy arrays of values" do @resource[:foo] = %w{yay fee} @resource.to_trans["foo"].should == %w{yay fee} end it "should reduce single-value arrays to just a value" do @resource[:foo] = %w{yay} @resource.to_trans["foo"].should == "yay" end it "should convert resource references into the backward-compatible form" do @resource[:foo] = Puppet::Resource.new(:file, "/f") @resource.to_trans["foo"].should == %w{File /f} end it "should convert resource references into the backward-compatible form even when within arrays" do @resource[:foo] = ["a", Puppet::Resource.new(:file, "/f")] @resource.to_trans["foo"].should == ["a", %w{File /f}] end end end describe "when converting to pson", :if => Puppet.features.pson? do def pson_output_should @resource.class.expects(:pson_create).with { |hash| yield hash } end it "should include the pson util module" do Puppet::Resource.singleton_class.ancestors.should be_include(Puppet::Util::Pson) end # LAK:NOTE For all of these tests, we convert back to the resource so we can # trap the actual data structure then. it "should set its type to the provided type" do Puppet::Resource.from_pson(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).type.should == "File" end it "should set its title to the provided title" do Puppet::Resource.from_pson(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).title.should == "/foo" end it "should include all tags from the resource" do resource = Puppet::Resource.new("File", "/foo") resource.tag("yay") Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).tags.should == resource.tags end it "should include the file if one is set" do resource = Puppet::Resource.new("File", "/foo") resource.file = "/my/file" Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).file.should == "/my/file" end it "should include the line if one is set" do resource = Puppet::Resource.new("File", "/foo") resource.line = 50 Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).line.should == 50 end it "should include the 'exported' value if one is set" do resource = Puppet::Resource.new("File", "/foo") resource.exported = true Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).exported.should be_true end it "should set 'exported' to false if no value is set" do resource = Puppet::Resource.new("File", "/foo") Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).exported.should be_false end it "should set all of its parameters as the 'parameters' entry" do resource = Puppet::Resource.new("File", "/foo") resource[:foo] = %w{bar eh} resource[:fee] = %w{baz} result = Puppet::Resource.from_pson(PSON.parse(resource.to_pson)) result["foo"].should == %w{bar eh} result["fee"].should == %w{baz} end it "should serialize relationships as reference strings" do resource = Puppet::Resource.new("File", "/foo") resource[:requires] = Puppet::Resource.new("File", "/bar") result = Puppet::Resource.from_pson(PSON.parse(resource.to_pson)) result[:requires].should == "File[/bar]" end it "should serialize multiple relationships as arrays of reference strings" do resource = Puppet::Resource.new("File", "/foo") resource[:requires] = [Puppet::Resource.new("File", "/bar"), Puppet::Resource.new("File", "/baz")] result = Puppet::Resource.from_pson(PSON.parse(resource.to_pson)) result[:requires].should == [ "File[/bar]", "File[/baz]" ] end end describe "when converting from pson", :if => Puppet.features.pson? do def pson_result_should Puppet::Resource.expects(:new).with { |hash| yield hash } end before do @data = { 'type' => "file", 'title' => @basepath+"/yay", } end it "should set its type to the provided type" do Puppet::Resource.from_pson(@data).type.should == "File" end it "should set its title to the provided title" do Puppet::Resource.from_pson(@data).title.should == @basepath+"/yay" end it "should tag the resource with any provided tags" do @data['tags'] = %w{foo bar} resource = Puppet::Resource.from_pson(@data) resource.tags.should be_include("foo") resource.tags.should be_include("bar") end it "should set its file to the provided file" do @data['file'] = "/foo/bar" Puppet::Resource.from_pson(@data).file.should == "/foo/bar" end it "should set its line to the provided line" do @data['line'] = 50 Puppet::Resource.from_pson(@data).line.should == 50 end it "should 'exported' to true if set in the pson data" do @data['exported'] = true Puppet::Resource.from_pson(@data).exported.should be_true end it "should 'exported' to false if not set in the pson data" do Puppet::Resource.from_pson(@data).exported.should be_false end it "should fail if no title is provided" do @data.delete('title') lambda { Puppet::Resource.from_pson(@data) }.should raise_error(ArgumentError) end it "should fail if no type is provided" do @data.delete('type') lambda { Puppet::Resource.from_pson(@data) }.should raise_error(ArgumentError) end it "should set each of the provided parameters" do @data['parameters'] = {'foo' => %w{one two}, 'fee' => %w{three four}} resource = Puppet::Resource.from_pson(@data) resource['foo'].should == %w{one two} resource['fee'].should == %w{three four} end it "should convert single-value array parameters to normal values" do @data['parameters'] = {'foo' => %w{one}} resource = Puppet::Resource.from_pson(@data) resource['foo'].should == %w{one} end end describe "it should implement to_resource" do resource = Puppet::Resource.new("file", "/my/file") resource.to_resource.should == resource end describe "because it is an indirector model" do it "should include Puppet::Indirector" do Puppet::Resource.should be_is_a(Puppet::Indirector) end it "should have a default terminus" do Puppet::Resource.indirection.terminus_class.should == :ral end it "should have a name" do Puppet::Resource.new("file", "/my/file").name.should == "File//my/file" end end describe "when resolving resources with a catalog" do it "should resolve all resources using the catalog" do catalog = mock 'catalog' resource = Puppet::Resource.new("foo::bar", "yay") resource.catalog = catalog catalog.expects(:resource).with("Foo::Bar[yay]").returns(:myresource) resource.resolve.should == :myresource end end describe "when generating the uniqueness key" do it "should include all of the key_attributes in alphabetical order by attribute name" do Puppet::Type.type(:file).stubs(:key_attributes).returns [:myvar, :owner, :path] Puppet::Type.type(:file).stubs(:title_patterns).returns( [ [ /(.*)/, [ [:path, lambda{|x| x} ] ] ] ] ) res = Puppet::Resource.new("file", "/my/file", :parameters => {:owner => 'root', :content => 'hello'}) res.uniqueness_key.should == [ nil, 'root', '/my/file'] end end end diff --git a/spec/unit/type/file/content_spec.rb b/spec/unit/type/file/content_spec.rb index cde643fc8..9178c94bf 100755 --- a/spec/unit/type/file/content_spec.rb +++ b/spec/unit/type/file/content_spec.rb @@ -1,461 +1,514 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } content = Puppet::Type.type(:file).attrclass(:content) describe content do before do @resource = Puppet::Type.type(:file).new :path => "/foo/bar" content.stubs(:standalone?).returns(false) end it "should be a subclass of Property" do content.superclass.must == Puppet::Property end describe "when determining the checksum type" do it "should use the type specified in the source checksum if a source is set" do @resource[:source] = "/foo" @resource.parameter(:source).expects(:checksum).returns "{md5lite}eh" @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite end it "should use the type specified by the checksum parameter if no source is set" do @resource[:checksum] = :md5lite @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite end end describe "when determining the actual content to write" do it "should use the set content if available" do @content = content.new(:resource => @resource) @content.should = "ehness" @content.actual_content.should == "ehness" end it "should not use the content from the source if the source is set" do source = mock 'source' @resource.expects(:parameter).never.with(:source).returns source @content = content.new(:resource => @resource) @content.actual_content.should be_nil end end describe "when setting the desired content" do it "should make the actual content available via an attribute" do @content = content.new(:resource => @resource) @content.stubs(:checksum_type).returns "md5" @content.should = "this is some content" @content.actual_content.should == "this is some content" end it "should store the checksum as the desired content" do @content = content.new(:resource => @resource) digest = Digest::MD5.hexdigest("this is some content") @content.stubs(:checksum_type).returns "md5" @content.should = "this is some content" @content.should.must == "{md5}#{digest}" end it "should not checksum 'absent'" do @content = content.new(:resource => @resource) @content.should = :absent @content.should.must == :absent end it "should accept a checksum as the desired content" do @content = content.new(:resource => @resource) digest = Digest::MD5.hexdigest("this is some content") string = "{md5}#{digest}" @content.should = string @content.should.must == string end end describe "when retrieving the current content" do it "should return :absent if the file does not exist" do @content = content.new(:resource => @resource) @resource.expects(:stat).returns nil @content.retrieve.should == :absent end it "should not manage content on directories" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "directory" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end it "should not manage content on links" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "link" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end it "should always return the checksum as a string" do @content = content.new(:resource => @resource) @resource[:checksum] = :mtime stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat time = Time.now @resource.parameter(:checksum).expects(:mtime_file).with(@resource[:path]).returns time @content.retrieve.should == "{mtime}#{time}" end it "should return the checksum of the file if it exists and is a normal file" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat @resource.parameter(:checksum).expects(:md5_file).with(@resource[:path]).returns "mysum" @content.retrieve.should == "{md5}mysum" end end describe "when testing whether the content is in sync" do before do @resource[:ensure] = :file @content = content.new(:resource => @resource) end it "should return true if the resource shouldn't be a regular file" do @resource.expects(:should_be_file?).returns false - @content.must be_insync("whatever") + @content.should = "foo" + @content.must be_safe_insync("whatever") end it "should return false if the current content is :absent" do - @content.should_not be_insync(:absent) + @content.should = "foo" + @content.should_not be_safe_insync(:absent) end it "should return false if the file should be a file but is not present" do @resource.expects(:should_be_file?).returns true + @content.should = "foo" - @content.should_not be_insync(:absent) + @content.should_not be_safe_insync(:absent) end describe "and the file exists" do before do @resource.stubs(:stat).returns mock("stat") end it "should return false if the current contents are different from the desired content" do @content.should = "some content" - @content.should_not be_insync("other content") + @content.should_not be_safe_insync("other content") end it "should return true if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" - @content.must be_insync("{md5}" + Digest::MD5.hexdigest("some content")) + @content.must be_safe_insync("{md5}" + Digest::MD5.hexdigest("some content")) end describe "and Puppet[:show_diff] is set" do before do Puppet[:show_diff] = true end it "should display a diff if the current contents are different from the desired content" do @content.should = "some content" @content.expects(:diff).returns("my diff").once @content.expects(:print).with("my diff").once - @content.insync?("other content") + @content.safe_insync?("other content") end it "should not display a diff if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" @content.expects(:diff).never - @content.insync?("{md5}" + Digest::MD5.hexdigest("some content")) + @content.safe_insync?("{md5}" + Digest::MD5.hexdigest("some content")) end end end describe "and :replace is false" do before do @resource.stubs(:replace?).returns false end it "should be insync if the file exists and the content is different" do @resource.stubs(:stat).returns mock('stat') - @content.must be_insync("whatever") + @content.must be_safe_insync("whatever") end it "should be insync if the file exists and the content is right" do @resource.stubs(:stat).returns mock('stat') - @content.must be_insync("something") + @content.must be_safe_insync("something") end it "should not be insync if the file does not exist" do - @content.should_not be_insync(:absent) + @content.should = "foo" + @content.should_not be_safe_insync(:absent) end end end describe "when changing the content" do before do @content = content.new(:resource => @resource) @content.should = "some content" @resource.stubs(:[]).with(:path).returns "/boo" @resource.stubs(:stat).returns "eh" end it "should use the file's :write method to write the content" do @resource.expects(:write).with(:content) @content.sync end it "should return :file_changed if the file already existed" do @resource.expects(:stat).returns "something" @resource.stubs(:write) @content.sync.should == :file_changed end it "should return :file_created if the file did not exist" do @resource.expects(:stat).returns nil @resource.stubs(:write) @content.sync.should == :file_created end end describe "when writing" do before do @content = content.new(:resource => @resource) @fh = stub_everything end it "should attempt to read from the filebucket if no actual content nor source exists" do @content.should = "{md5}foo" @content.resource.bucket.class.any_instance.stubs(:getfile).returns "foo" @content.write(@fh) end describe "from actual content" do before(:each) do @content.stubs(:actual_content).returns("this is content") end it "should write to the given file handle" do @fh.expects(:print).with("this is content") @content.write(@fh) end it "should return the current checksum value" do @resource.parameter(:checksum).expects(:sum_stream).returns "checksum" @content.write(@fh).should == "checksum" end end describe "from a file bucket" do it "should fail if a file bucket cannot be retrieved" do @content.should = "{md5}foo" @content.resource.expects(:bucket).returns nil lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should fail if the file bucket cannot find any content" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").raises "foobar" lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should write the returned content to the file" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").returns "mycontent" @fh.expects(:print).with("mycontent") @content.write(@fh) end end describe "from local source" do before(:each) do @content.stubs(:actual_content).returns(nil) @source = stub_everything 'source', :local? => true, :full_path => "/path/to/source" @resource.stubs(:parameter).with(:source).returns @source @sum = stub_everything 'sum' @resource.stubs(:parameter).with(:checksum).returns(@sum) @digest = stub_everything 'digest' @sum.stubs(:sum_stream).yields(@digest) @file = stub_everything 'file' File.stubs(:open).yields(@file) @file.stubs(:read).with(8192).returns("chunk1").then.returns("chunk2").then.returns(nil) end it "should open the local file" do File.expects(:open).with("/path/to/source", "r") @content.write(@fh) end it "should read the local file by chunks" do @file.expects(:read).with(8192).returns("chunk1").then.returns(nil) @content.write(@fh) end it "should write each chunk to the file" do @fh.expects(:print).with("chunk1").then.with("chunk2") @content.write(@fh) end it "should pass each chunk to the current sum stream" do @digest.expects(:<<).with("chunk1").then.with("chunk2") @content.write(@fh) end it "should return the checksum computed" do @sum.stubs(:sum_stream).yields(@digest).returns("checksum") @content.write(@fh).should == "checksum" end end describe "from remote source" do before(:each) do @response = stub_everything 'mock response', :code => "404" @conn = stub_everything 'connection' @conn.stubs(:request_get).yields(@response) Puppet::Network::HttpPool.stubs(:http_instance).returns @conn @content.stubs(:actual_content).returns(nil) @source = stub_everything 'source', :local? => false, :full_path => "/path/to/source", :server => "server", :port => 1234 @resource.stubs(:parameter).with(:source).returns @source @sum = stub_everything 'sum' @resource.stubs(:parameter).with(:checksum).returns(@sum) @digest = stub_everything 'digest' @sum.stubs(:sum_stream).yields(@digest) end it "should open a network connection to source server and port" do Puppet::Network::HttpPool.expects(:http_instance).with("server", 1234).returns @conn @content.write(@fh) end it "should send the correct indirection uri" do @conn.expects(:request_get).with { |uri,headers| uri == "/production/file_content/path/to/source" }.yields(@response) @content.write(@fh) end it "should return nil if source is not found" do @response.expects(:code).returns("404") @content.write(@fh).should == nil end it "should not write anything if source is not found" do @response.expects(:code).returns("404") @fh.expects(:print).never @content.write(@fh).should == nil end it "should raise an HTTP error in case of server error" do @response.expects(:code).returns("500") lambda { @content.write(@fh) }.should raise_error end it "should write content by chunks" do @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") @fh.expects(:print).with("chunk1").then.with("chunk2") @content.write(@fh) end it "should pass each chunk to the current sum stream" do @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") @digest.expects(:<<).with("chunk1").then.with("chunk2") @content.write(@fh) end it "should return the checksum computed" do @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") @sum.expects(:sum_stream).yields(@digest).returns("checksum") @content.write(@fh).should == "checksum" end it "should get the current accept encoding header value" do @content.expects(:add_accept_encoding) @content.write(@fh) end it "should uncompress body on error" do @response.expects(:code).returns("500") @response.expects(:body).returns("compressed body") @content.expects(:uncompress_body).with(@response).returns("uncompressed") lambda { @content.write(@fh) }.should raise_error { |e| e.message =~ /uncompressed/ } end it "should uncompress chunk by chunk" do uncompressor = stub_everything 'uncompressor' @content.expects(:uncompress).with(@response).yields(uncompressor) @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") uncompressor.expects(:uncompress).with("chunk1").then.with("chunk2") @content.write(@fh) end it "should write uncompressed chunks to the file" do uncompressor = stub_everything 'uncompressor' @content.expects(:uncompress).with(@response).yields(uncompressor) @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") uncompressor.expects(:uncompress).with("chunk1").returns("uncompressed1") uncompressor.expects(:uncompress).with("chunk2").returns("uncompressed2") @fh.expects(:print).with("uncompressed1") @fh.expects(:print).with("uncompressed2") @content.write(@fh) end it "should pass each uncompressed chunk to the current sum stream" do uncompressor = stub_everything 'uncompressor' @content.expects(:uncompress).with(@response).yields(uncompressor) @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") uncompressor.expects(:uncompress).with("chunk1").returns("uncompressed1") uncompressor.expects(:uncompress).with("chunk2").returns("uncompressed2") @digest.expects(:<<).with("uncompressed1").then.with("uncompressed2") @content.write(@fh) end end describe "from a filebucket" do end + + # These are testing the implementation rather than the desired behaviour; while that bites, there are a whole + # pile of other methods in the File type that depend on intimate details of this implementation and vice-versa. + # If these blow up, you are gonna have to review the callers to make sure they don't explode! --daniel 2011-02-01 + describe "each_chunk_from should work" do + before do + @content = content.new(:resource => @resource) + end + + it "when content is a string" do + @content.each_chunk_from('i_am_a_string') { |chunk| chunk.should == 'i_am_a_string' } + end + + it "when no content, source, but ensure present" do + @resource[:ensure] = :present + @content.each_chunk_from(nil) { |chunk| chunk.should == '' } + end + + it "when no content, source, but ensure file" do + @resource[:ensure] = :file + @content.each_chunk_from(nil) { |chunk| chunk.should == '' } + end + + it "when no content or source" do + @content.expects(:read_file_from_filebucket).once.returns('im_a_filebucket') + @content.each_chunk_from(nil) { |chunk| chunk.should == 'im_a_filebucket' } + end + + it "when running as puppet apply" do + @content.class.expects(:standalone?).returns true + source_or_content = stubs('source_or_content') + source_or_content.expects(:content).once.returns :whoo + @content.each_chunk_from(source_or_content) { |chunk| chunk.should == :whoo } + end + + it "when running from source with a local file" do + source_or_content = stubs('source_or_content') + source_or_content.expects(:local?).returns true + @content.expects(:chunk_file_from_disk).with(source_or_content).once.yields 'woot' + @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } + end + + it "when running from source with a remote file" do + source_or_content = stubs('source_or_content') + source_or_content.expects(:local?).returns false + @content.expects(:chunk_file_from_source).with(source_or_content).once.yields 'woot' + @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } + end + end end end diff --git a/spec/unit/type/file/ensure_spec.rb b/spec/unit/type/file/ensure_spec.rb index ec53ed85a..dbb3a1053 100755 --- a/spec/unit/type/file/ensure_spec.rb +++ b/spec/unit/type/file/ensure_spec.rb @@ -1,84 +1,85 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } property = Puppet::Type.type(:file).attrclass(:ensure) describe property do before do # Wow that's a messy interface to the resource. @resource = stub 'resource', :[] => nil, :[]= => nil, :property => nil, :newattr => nil, :parameter => nil, :replace? => true @resource.stubs(:[]).returns "foo" @resource.stubs(:[]).with(:path).returns "/my/file" @ensure = property.new :resource => @resource end it "should be a subclass of Ensure" do property.superclass.must == Puppet::Property::Ensure end describe "when retrieving the current state" do it "should return :absent if the file does not exist" do @ensure = property.new(:resource => @resource) @resource.expects(:stat).returns nil @ensure.retrieve.should == :absent end it "should return the current file type if the file exists" do @ensure = property.new(:resource => @resource) stat = mock 'stat', :ftype => "directory" @resource.expects(:stat).returns stat @ensure.retrieve.should == :directory end end describe "when testing whether :ensure is in sync" do before do @ensure = property.new(:resource => @resource) @stat = stub 'stat', :ftype => "file" end it "should always be in sync if replace is 'false' unless the file is missing" do + @ensure.should = :file @resource.expects(:replace?).returns false - @ensure.insync?(:link).should be_true + @ensure.safe_insync?(:link).should be_true end it "should be in sync if :ensure is set to :absent and the file does not exist" do @ensure.should = :absent - @ensure.must be_insync(:absent) + @ensure.must be_safe_insync(:absent) end it "should not be in sync if :ensure is set to :absent and the file exists" do @ensure.should = :absent - @ensure.should_not be_insync(:file) + @ensure.should_not be_safe_insync(:file) end it "should be in sync if a normal file exists and :ensure is set to :present" do @ensure.should = :present - @ensure.must be_insync(:file) + @ensure.must be_safe_insync(:file) end it "should be in sync if a directory exists and :ensure is set to :present" do @ensure.should = :present - @ensure.must be_insync(:directory) + @ensure.must be_safe_insync(:directory) end it "should be in sync if a symlink exists and :ensure is set to :present" do @ensure.should = :present - @ensure.must be_insync(:link) + @ensure.must be_safe_insync(:link) end it "should not be in sync if :ensure is set to :file and a directory exists" do @ensure.should = :file - @ensure.should_not be_insync(:directory) + @ensure.should_not be_safe_insync(:directory) end end end diff --git a/spec/unit/type/file/group_spec.rb b/spec/unit/type/file/group_spec.rb index 2283b57fa..956cd57e7 100755 --- a/spec/unit/type/file/group_spec.rb +++ b/spec/unit/type/file/group_spec.rb @@ -1,123 +1,123 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } property = Puppet::Type.type(:file).attrclass(:group) describe property do before do @resource = stub 'resource', :line => "foo", :file => "bar" @resource.stubs(:[]).returns "foo" @resource.stubs(:[]).with(:path).returns "/my/file" @group = property.new :resource => @resource end it "should have a method for testing whether a group is valid" do @group.must respond_to(:validgroup?) end it "should return the found gid if a group is valid" do @group.expects(:gid).with("foo").returns 500 @group.validgroup?("foo").should == 500 end it "should return false if a group is not valid" do @group.expects(:gid).with("foo").returns nil @group.validgroup?("foo").should be_false end describe "when retrieving the current value" do it "should return :absent if the file cannot stat" do @resource.expects(:stat).returns nil @group.retrieve.should == :absent end it "should get the gid from the stat instance from the file" do stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat stat.expects(:gid).returns 500 @group.retrieve.should == 500 end it "should warn and return :silly if the found value is higher than the maximum uid value" do Puppet.settings.expects(:value).with(:maximum_uid).returns 500 stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat stat.expects(:gid).returns 1000 @group.expects(:warning) @group.retrieve.should == :silly end end describe "when determining if the file is in sync" do it "should directly compare the group values if the desired group is an integer" do @group.should = [10] - @group.must be_insync(10) + @group.must be_safe_insync(10) end it "should treat numeric strings as integers" do @group.should = ["10"] - @group.must be_insync(10) + @group.must be_safe_insync(10) end it "should convert the group name to an integer if the desired group is a string" do @group.expects(:gid).with("foo").returns 10 @group.should = %w{foo} - @group.must be_insync(10) + @group.must be_safe_insync(10) end it "should not validate that groups exist when a group is specified as an integer" do @group.expects(:gid).never @group.validgroup?(10) end it "should fail if it cannot convert a group name to an integer" do @group.expects(:gid).with("foo").returns nil @group.should = %w{foo} - lambda { @group.insync?(10) }.should raise_error(Puppet::Error) + lambda { @group.safe_insync?(10) }.should raise_error(Puppet::Error) end it "should return false if the groups are not equal" do @group.should = [10] - @group.should_not be_insync(20) + @group.should_not be_safe_insync(20) end end describe "when changing the group" do before do @group.should = %w{one} @group.stubs(:gid).returns 500 end it "should chown the file if :links is set to :follow" do @resource.expects(:[]).with(:links).returns :follow File.expects(:chown) @group.sync end it "should lchown the file if :links is set to :manage" do @resource.expects(:[]).with(:links).returns :manage File.expects(:lchown) @group.sync end it "should use the first valid group in its 'should' list" do @group.should = %w{one two three} @group.expects(:validgroup?).with("one").returns nil @group.expects(:validgroup?).with("two").returns 500 @group.expects(:validgroup?).with("three").never File.expects(:chown).with(nil, 500, "/my/file") @group.sync end end end diff --git a/spec/unit/type/file/owner_spec.rb b/spec/unit/type/file/owner_spec.rb index 8e136a187..bcb8e07d6 100755 --- a/spec/unit/type/file/owner_spec.rb +++ b/spec/unit/type/file/owner_spec.rb @@ -1,150 +1,150 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } property = Puppet::Type.type(:file).attrclass(:owner) describe property do before do # FIXME: many of these tests exercise the provider rather than `owner` # and should be moved into provider tests. ~JW @provider = Puppet::Type.type(:file).provider(:posix).new @provider.stubs(:uid).with("one").returns(1) @resource = stub 'resource', :line => "foo", :file => "bar" @resource.stubs(:[]).returns "foo" @resource.stubs(:[]).with(:path).returns "/my/file" @resource.stubs(:provider).returns @provider @owner = property.new :resource => @resource end it "should have a method for testing whether an owner is valid" do @provider.must respond_to(:validuser?) end it "should return the found uid if an owner is valid" do @provider.expects(:uid).with("foo").returns 500 @provider.validuser?("foo").should == 500 end it "should return false if an owner is not valid" do @provider.expects(:uid).with("foo").returns nil @provider.validuser?("foo").should be_false end describe "when retrieving the current value" do it "should return :absent if the file cannot stat" do @resource.expects(:stat).returns nil @owner.retrieve.should == :absent end it "should get the uid from the stat instance from the file" do stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat stat.expects(:uid).returns 500 @owner.retrieve.should == 500 end it "should warn and return :silly if the found value is higher than the maximum uid value" do Puppet.settings.expects(:value).with(:maximum_uid).returns 500 stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat stat.expects(:uid).returns 1000 @provider.expects(:warning) @owner.retrieve.should == :silly end end describe "when determining if the file is in sync" do describe "and not running as root" do it "should warn once and return true" do Puppet.features.expects(:root?).returns false @provider.expects(:warnonce) @owner.should = [10] - @owner.must be_insync(20) + @owner.must be_safe_insync(20) end end before do Puppet.features.stubs(:root?).returns true end it "should be in sync if 'should' is not provided" do - @owner.must be_insync(10) + @owner.must be_safe_insync(10) end it "should directly compare the owner values if the desired owner is an integer" do @owner.should = [10] - @owner.must be_insync(10) + @owner.must be_safe_insync(10) end it "should treat numeric strings as integers" do @owner.should = ["10"] - @owner.must be_insync(10) + @owner.must be_safe_insync(10) end it "should convert the owner name to an integer if the desired owner is a string" do @provider.expects(:uid).with("foo").returns 10 @owner.should = %w{foo} - @owner.must be_insync(10) + @owner.must be_safe_insync(10) end it "should not validate that users exist when a user is specified as an integer" do @provider.expects(:uid).never @provider.validuser?(10) end it "should fail if it cannot convert an owner name to an integer" do @provider.expects(:uid).with("foo").returns nil @owner.should = %w{foo} - lambda { @owner.insync?(10) }.should raise_error(Puppet::Error) + lambda { @owner.safe_insync?(10) }.should raise_error(Puppet::Error) end it "should return false if the owners are not equal" do @owner.should = [10] - @owner.should_not be_insync(20) + @owner.should_not be_safe_insync(20) end end describe "when changing the owner" do before do @owner.should = %w{one} @owner.stubs(:path).returns "path" @owner.stubs(:uid).returns 500 end it "should chown the file if :links is set to :follow" do @resource.expects(:[]).with(:links).returns :follow File.expects(:chown) @owner.sync end it "should lchown the file if :links is set to :manage" do @resource.expects(:[]).with(:links).returns :manage File.expects(:lchown) @owner.sync end it "should use the first valid owner in its 'should' list" do @owner.should = %w{one two three} @provider.expects(:validuser?).with("one").returns nil @provider.expects(:validuser?).with("two").returns 500 @provider.expects(:validuser?).with("three").never File.expects(:chown).with(500, nil, "/my/file") @owner.sync end end end diff --git a/spec/unit/type/file/selinux_spec.rb b/spec/unit/type/file/selinux_spec.rb index 1ca59e9e7..043471dec 100644 --- a/spec/unit/type/file/selinux_spec.rb +++ b/spec/unit/type/file/selinux_spec.rb @@ -1,83 +1,83 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } [:seluser, :selrole, :seltype, :selrange].each do |param| property = Puppet::Type.type(:file).attrclass(param) describe property do before do @resource = Puppet::Type.type(:file).new :path => "/my/file" @sel = property.new :resource => @resource end it "retrieve on #{param} should return :absent if the file isn't statable" do @resource.expects(:stat).returns nil @sel.retrieve.should == :absent end it "should retrieve nil for #{param} if there is no SELinux support" do stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat @sel.expects(:get_selinux_current_context).with("/my/file").returns nil @sel.retrieve.should be_nil end it "should retrieve #{param} if a SELinux context is found with a range" do stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat @sel.expects(:get_selinux_current_context).with("/my/file").returns "user_u:role_r:type_t:s0" expectedresult = case param when :seluser; "user_u" when :selrole; "role_r" when :seltype; "type_t" when :selrange; "s0" end @sel.retrieve.should == expectedresult end it "should retrieve #{param} if a SELinux context is found without a range" do stat = stub 'stat', :ftype => "foo" @resource.expects(:stat).returns stat @sel.expects(:get_selinux_current_context).with("/my/file").returns "user_u:role_r:type_t" expectedresult = case param when :seluser; "user_u" when :selrole; "role_r" when :seltype; "type_t" when :selrange; nil end @sel.retrieve.should == expectedresult end it "should handle no default gracefully" do @sel.expects(:get_selinux_default_context).with("/my/file").returns nil @sel.default.must be_nil end it "should be able to detect matchpathcon defaults" do @sel.stubs(:debug) @sel.expects(:get_selinux_default_context).with("/my/file").returns "user_u:role_r:type_t:s0" expectedresult = case param when :seluser; "user_u" when :selrole; "role_r" when :seltype; "type_t" when :selrange; "s0" end @sel.default.must == expectedresult end it "should be able to set a new context" do stat = stub 'stat', :ftype => "foo" @sel.should = %w{newone} @sel.expects(:set_selinux_context).with("/my/file", ["newone"], param) @sel.sync end - it "should do nothing for insync? if no SELinux support" do + it "should do nothing for safe_insync? if no SELinux support" do @sel.should = %{newcontext} @sel.expects(:selinux_support?).returns false - @sel.insync?("oldcontext").should == true + @sel.safe_insync?("oldcontext").should == true end end end diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb index 22921d85a..90f3daf09 100755 --- a/spec/unit/type/file_spec.rb +++ b/spec/unit/type/file_spec.rb @@ -1,1068 +1,1193 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:file) do before do Puppet.settings.stubs(:use) @real_posix = Puppet.features.posix? Puppet.features.stubs("posix?").returns(true) @path = Tempfile.new("puppetspec") pathname = @path.path @path.close!() @path = pathname @file = Puppet::Type::File.new(:name => @path) @catalog = Puppet::Resource::Catalog.new @file.catalog = @catalog end describe "when determining if recursion is enabled" do it "should default to recursion being disabled" do @file.should_not be_recurse end [true, "true", 10, "inf", "remote"].each do |value| it "should consider #{value} to enable recursion" do @file[:recurse] = value @file.must be_recurse end end [false, "false", 0].each do |value| it "should consider #{value} to disable recursion" do @file[:recurse] = value @file.should_not be_recurse end end end describe "#write" do it "should propagate failures encountered when renaming the temporary file" do File.stubs(:open) File.expects(:rename).raises ArgumentError file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") file.stubs(:validate_checksum?).returns(false) property = stub('content_property', :actual_content => "something", :length => "something".length) file.stubs(:property).with(:content).returns(property) lambda { file.write(:content) }.should raise_error(Puppet::Error) end it "should delegate writing to the content property" do filehandle = stub_everything 'fh' File.stubs(:open).yields(filehandle) File.stubs(:rename) property = stub('content_property', :actual_content => "something", :length => "something".length) file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") file.stubs(:validate_checksum?).returns(false) file.stubs(:property).with(:content).returns(property) property.expects(:write).with(filehandle) file.write(:content) end 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) lambda { @file.write :NOTUSED }.should 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) lambda { @file.write :NOTUSED }.should_not raise_error(Puppet::Error) end end end it "should have a method for determining if the file is present" do @file.must respond_to(:exist?) end 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 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 describe "when using POSIX filenames" do describe "on POSIX systems" do before do Puppet.features.stubs(:posix?).returns(true) Puppet.features.stubs(:microsoft_windows?).returns(false) end it "should autorequire its parent directory" do file = Puppet::Type::File.new(:path => "/foo/bar") dir = Puppet::Type::File.new(:path => "/foo") @catalog.add_resource file @catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire its parent dir if its parent dir is itself" do file = Puppet::Type::File.new(:path => "/") @catalog.add_resource file file.autorequire.should be_empty end it "should remove trailing slashes" do file = Puppet::Type::File.new(:path => "/foo/bar/baz/") file[:path].should == "/foo/bar/baz" end it "should remove double slashes" do file = Puppet::Type::File.new(:path => "/foo/bar//baz") file[:path].should == "/foo/bar/baz" end it "should remove trailing double slashes" do file = Puppet::Type::File.new(:path => "/foo/bar/baz//") file[:path].should == "/foo/bar/baz" end it "should leave a single slash alone" do file = Puppet::Type::File.new(:path => "/") file[:path].should == "/" end + + it "should accept a double-slash at the start of the path" do + expect { + file = Puppet::Type::File.new(:path => "//tmp/xxx") + # REVISIT: This should be wrong, later. See the next test. + # --daniel 2011-01-31 + file[:path].should == '/tmp/xxx' + }.should_not raise_error + end + + # REVISIT: This is pending, because I don't want to try and audit the + # entire codebase to make sure we get this right. POSIX treats two (and + # exactly two) '/' characters at the start of the path specially. + # + # See sections 3.2 and 4.11, which allow DomainOS to be all special like + # and still have the POSIX branding and all. --daniel 2011-01-31 + it "should preserve the double-slash at the start of the path" end describe "on Microsoft Windows systems" do before do Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) end it "should refuse to work" do lambda { Puppet::Type::File.new(:path => "/foo/bar") }.should raise_error(Puppet::Error) end end end describe "when using Microsoft Windows filenames", :if => Puppet.features.microsoft_windows? do describe "on Microsoft Windows systems" do before do Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) end it "should autorequire its parent directory" do file = Puppet::Type::File.new(:path => "X:/foo/bar") dir = Puppet::Type::File.new(:path => "X:/foo") @catalog.add_resource file @catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire its parent dir if its parent dir is itself" do file = Puppet::Type::File.new(:path => "X:/") @catalog.add_resource file file.autorequire.should be_empty end it "should remove trailing slashes" do file = Puppet::Type::File.new(:path => "X:/foo/bar/baz/") file[:path].should == "X:/foo/bar/baz" end it "should remove double slashes" do file = Puppet::Type::File.new(:path => "X:/foo/bar//baz") file[:path].should == "X:/foo/bar/baz" end it "should remove trailing double slashes" do file = Puppet::Type::File.new(: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 = Puppet::Type::File.new(:path => "X:/") file[:path].should == "X:/" end it "should add a slash to a drive letter" do file = Puppet::Type::File.new(:path => "X:") file[:path].should == "X:/" end end describe "on POSIX systems" do before do Puppet.features.stubs(:posix?).returns(true) Puppet.features.stubs(:microsoft_windows?).returns(false) end it "should refuse to work" do lambda { Puppet::Type::File.new(:path => "X:/foo/bar") }.should raise_error(Puppet::Error) end end end describe "when using UNC filenames" do describe "on Microsoft Windows systems", :if => Puppet.features.microsoft_windows? do before do Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) end it "should autorequire its parent directory" do file = Puppet::Type::File.new(:path => "//server/foo/bar") dir = Puppet::Type::File.new(:path => "//server/foo") @catalog.add_resource file @catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire its parent dir if its parent dir is itself" do file = Puppet::Type::File.new(:path => "//server/foo") @catalog.add_resource file puts file.autorequire file.autorequire.should be_empty end it "should remove trailing slashes" do file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/") file[:path].should == "//server/foo/bar/baz" end it "should remove double slashes" do file = Puppet::Type::File.new(:path => "//server/foo/bar//baz") file[:path].should == "//server/foo/bar/baz" end it "should remove trailing double slashes" do file = Puppet::Type::File.new(:path => "//server/foo/bar/baz//") file[:path].should == "//server/foo/bar/baz" end it "should remove a trailing slash from a sharename" do file = Puppet::Type::File.new(:path => "//server/foo/") file[:path].should == "//server/foo" end it "should not modify a sharename" do file = Puppet::Type::File.new(:path => "//server/foo") file[:path].should == "//server/foo" end end describe "on POSIX systems" do before do Puppet.features.stubs(:posix?).returns(true) Puppet.features.stubs(:microsoft_windows?).returns(false) end it "should refuse to work" do lambda { Puppet::Type::File.new(:path => "X:/foo/bar") }.should raise_error(Puppet::Error) end end end describe "when initializing" do it "should set a desired 'ensure' value if none is set and 'content' is set" do file = Puppet::Type::File.new(:name => "/my/file", :content => "/foo/bar") file[:ensure].should == :file end it "should set a desired 'ensure' value if none is set and 'target' is set" do file = Puppet::Type::File.new(:name => "/my/file", :target => "/foo/bar") file[:ensure].should == :symlink end end describe "when validating attributes" do %w{path checksum backup recurse recurselimit source replace force ignore links purge sourceselect}.each do |attr| it "should have a '#{attr}' parameter" do Puppet::Type.type(:file).attrtype(attr.intern).should == :param end end %w{content target ensure owner group mode type}.each do |attr| it "should have a '#{attr}' property" do Puppet::Type.type(:file).attrtype(attr.intern).should == :property end end it "should have its 'path' attribute set as its namevar" do Puppet::Type.type(:file).key_attributes.should == [:path] end end describe "when managing links" do require 'puppettest/support/assertions' include PuppetTest require 'tempfile' if @real_posix describe "on POSIX systems" do before do @basedir = tempfile Dir.mkdir(@basedir) @file = File.join(@basedir, "file") @link = File.join(@basedir, "link") File.open(@file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(@file, @link) @resource = Puppet::Type.type(:file).new( :path => @link, :mode => "755" ) @catalog.add_resource @resource end after do remove_tmp_files end it "should default to managing the link" do @catalog.apply # I convert them to strings so they display correctly if there's an error. ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0644 end it "should be able to follow links" do @resource[:links] = :follow @catalog.apply ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0755 end end else # @real_posix # should recode tests using expectations instead of using the filesystem end describe "on Microsoft Windows systems" do before do Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) end it "should refuse to work with links" end end it "should be able to retrieve a stat instance for the file it is managing" do Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo").should respond_to(:stat) end describe "when stat'ing its file" do before do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") @resource[:links] = :manage # so we always use :lstat end it "should use :stat if it is following links" do @resource[:links] = :follow File.expects(:stat) @resource.stat end it "should use :lstat if is it not following links" do @resource[:links] = :manage File.expects(:lstat) @resource.stat end it "should stat the path of the file" do File.expects(:lstat).with("/foo/bar") @resource.stat end # This only happens in testing. it "should return nil if the stat does not exist" do File.expects(:lstat).returns nil @resource.stat.should be_nil end it "should return nil if the file does not exist" do File.expects(:lstat).raises(Errno::ENOENT) @resource.stat.should be_nil end it "should return nil if the file cannot be stat'ed" do File.expects(:lstat).raises(Errno::EACCES) @resource.stat.should be_nil end it "should return the stat instance" do File.expects(:lstat).returns "mystat" @resource.stat.should == "mystat" end it "should cache the stat instance if it has a catalog and is applying" do stat = mock 'stat' File.expects(:lstat).returns stat catalog = Puppet::Resource::Catalog.new @resource.catalog = catalog catalog.stubs(:applying?).returns true @resource.stat.should equal(@resource.stat) end end describe "when flushing" do it "should flush all properties that respond to :flush" do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo") @resource.parameter(:source).expects(:flush) @resource.flush end it "should reset its stat reference" do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") File.expects(:lstat).times(2).returns("stat1").then.returns("stat2") @resource.stat.should == "stat1" @resource.flush @resource.stat.should == "stat2" end end it "should have a method for performing recursion" do @file.must respond_to(:perform_recursion) end describe "when executing a recursive search" do it "should use Metadata to do its recursion" do Puppet::FileServing::Metadata.expects(:search) @file.perform_recursion(@file[:path]) end it "should use the provided path as the key to the search" do Puppet::FileServing::Metadata.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.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.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.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.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.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.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } @file.perform_recursion(@file[:path]) end end it "should have a method for performing local recursion" do @file.must respond_to(:recurse_local) end describe "when doing local recursion" 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.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] @file.expects(:newchild).with("my/file").returns "fiebar" @file.recurse_local end end it "should have a method for performing link recursion" do @file.must respond_to(:recurse_link) end describe "when doing link recursion" 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" do file = stub 'file' file.expects(:[]=).with(:target, "/my/second") file.expects(:[]=).with(:ensure, :link) @file.stubs(:perform_recursion).returns [@first, @second] @file.recurse_link("first" => @resource, "second" => file) end it "should :ensure to :directory if the file is a directory" do file = stub 'file' file.expects(:[]=).with(:ensure, :directory) @file.stubs(:perform_recursion).returns [@first, @second] @file.recurse_link("first" => file, "second" => @resource) end it "should return a hash with both created and existing resources with the relative paths as the hash keys" do file = stub 'file', :[]= => nil @file.expects(:perform_recursion).returns [@first, @second] @file.stubs(:newchild).returns file @file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file} end end it "should have a method for performing remote recursion" do @file.must respond_to(:recurse_remote) end describe "when doing remote recursion" do 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("/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("/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. it "should set the checksum type to :md5 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, :md5) @file.recurse_remote("first" => @resource) 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 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("/whatever", :relative_path => "foobar") @file[:source] = %w{/one /two /three /four} @file.expects(:perform_recursion).with("/one").returns nil @file.expects(:perform_recursion).with("/two").returns [] @file.expects(:perform_recursion).with("/three").returns [data] @file.expects(:perform_recursion).with("/four").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] = %w{/one /two /three /four} @file.stubs(:newchild).returns @resource one = [klass.new("/one", :relative_path => "a")] @file.expects(:perform_recursion).with("/one").returns one @file.expects(:newchild).with("a").returns @resource two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")] @file.expects(:perform_recursion).with("/two").returns two @file.expects(:newchild).with("b").returns @resource three = [klass.new("/three", :relative_path => "a"), klass.new("/three", :relative_path => "c")] @file.expects(:perform_recursion).with("/three").returns three @file.expects(:newchild).with("c").returns @resource @file.expects(:perform_recursion).with("/four").returns [] @file.recurse_remote({}) end end end end + describe "when specifying both source, and content properties" do + before do + @file[:source] = '/one' + @file[:content] = 'file contents' + end + + it "should raise an exception" do + lambda {@file.validate }.should raise_error(/You cannot specify more than one of/) + end + end + + describe "when using source" do + before do + @file[:source] = '/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 + + lambda { @file.validate }.should_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 + lambda { @file.validate }.should 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 + lambda { @file.validate }.should_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 + + lambda { @file.validate }.should raise_error(/You cannot specify content when using checksum '#{checksum_type}'/) + end + end + end + end + describe "when returning resources with :eval_generate" do before do @graph = stub 'graph', :add_edge => nil @catalog.stubs(:relationship_graph).returns @graph @file.catalog = @catalog @file[:recurse] = true end it "should recurse if recursion is enabled" do resource = stub('resource', :[] => "resource") @file.expects(:recurse?).returns true @file.expects(:recurse).returns [resource] @file.eval_generate.should == [resource] end it "should not recurse if recursion is disabled" do @file.expects(:recurse?).returns false @file.expects(:recurse).never @file.eval_generate.should == [] end it "should return each resource found through recursion" do foo = stub 'foo', :[] => "/foo" bar = stub 'bar', :[] => "/bar" bar2 = stub 'bar2', :[] => "/bar" @file.expects(:recurse).returns [foo, bar] @file.eval_generate.should == [foo, bar] end end describe "when recursing" do before do @file[:recurse] = true @metadata = Puppet::FileServing::Metadata end describe "and a source is set" do before { @file[:source] = "/my/source" } it "should pass the already-discovered resources to recurse_remote" do @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 before { @file[:target] = "/link/target" } it "should use recurse_link" do @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 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 configure each file to be removed" do local = stub 'local' local.stubs(:[]).with(:source).returns nil # Thus, a local file local.stubs(:[]).with(:path).returns "foo" @file.expects(:recurse_local).returns("local" => local) local.expects(:[]=).with(:ensure, :absent) @file.recurse end it "should not remove files that exist in the remote repository" do @file["source"] = "/my/file" @file.expects(:recurse_local).returns({}) remote = stub 'remote' remote.stubs(:[]).with(:source).returns "/whatever" # Thus, a remote file remote.stubs(:[]).with(:path).returns "foo" @file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } remote.expects(:[]=).with(:ensure, :absent).never @file.recurse end end describe "and making a new child resource" do it "should not copy the parent resource's parent" do Puppet::Type.type(:file).expects(:new).with { |options| ! options.include?(:parent) } @file.newchild("my/path") end {:recurse => true, :target => "/foo/bar", :ensure => :present, :alias => "yay", :source => "/foo/bar"}.each do |param, value| it "should not pass on #{param} to the sub resource" do @file = Puppet::Type::File.new(:name => @path, param => value, :catalog => @catalog) @file.class.expects(:new).with { |params| params[param].nil? } @file.newchild("sub/file") end end it "should copy all of the parent resource's 'should' values that were set at initialization" do file = @file.class.new(:path => "/foo/bar", :owner => "root", :group => "wheel") @catalog.add_resource(file) file.class.expects(:new).with { |options| options[:owner] == "root" and options[:group] == "wheel" } file.newchild("my/path") end it "should not copy default values to the new child" do @file.class.expects(:new).with { |params| params[:backup].nil? } @file.newchild("my/path") end it "should not copy values to the child which were set by the source" do @file[:source] = "/foo/bar" metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever" @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 end describe "when setting the backup" do it "should default to 'puppet'" do Puppet::Type::File.new(:name => "/my/file")[:backup].should == "puppet" end it "should allow setting backup to 'false'" do (!Puppet::Type::File.new(:name => "/my/file", :backup => false)[:backup]).should be_true end it "should set the backup to '.puppet-bak' if it is set to true" do Puppet::Type::File.new(:name => "/my/file", :backup => true)[:backup].should == ".puppet-bak" end it "should support any other backup extension" do Puppet::Type::File.new(:name => "/my/file", :backup => ".bak")[:backup].should == ".bak" end it "should set the filebucket when backup is set to a string matching the name of a filebucket in the catalog" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file") catalog.add_resource file file[:backup] = "foo" file.bucket.should == bucket_resource.bucket end it "should find filebuckets added to the catalog after the file resource was created" do catalog = Puppet::Resource::Catalog.new file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") catalog.add_resource file bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file.bucket.should == bucket_resource.bucket end it "should have a nil filebucket if backup is false" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file", :backup => false) catalog.add_resource file file.bucket.should be_nil end it "should have a nil filebucket if backup is set to a string starting with '.'" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") catalog.add_resource file file.bucket.should be_nil end it "should fail if there's no catalog and backup is not false" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") lambda { file.bucket }.should raise_error(Puppet::Error) end it "should fail if a non-existent catalog is specified" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") catalog = Puppet::Resource::Catalog.new catalog.add_resource file lambda { file.bucket }.should raise_error(Puppet::Error) end it "should be able to use the default filebucket without a catalog" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") file.bucket.should be_instance_of(Puppet::FileBucket::Dipper) end it "should look up the filebucket during finish()" do file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") file.expects(:bucket) file.finish end end describe "when retrieving the current file state" do it "should copy the source values if the 'source' parameter is set" do file = Puppet::Type::File.new(:name => "/my/file", :source => "/foo/bar") file.parameter(:source).expects(:copy_source_values) file.retrieve end end describe ".title_patterns" do before do @type_class = Puppet::Type.type(:file) end it "should have a regexp that captures the entire string, except for a terminating slash" do patterns = @type_class.title_patterns string = "abc/\n\tdef/" patterns[0][0] =~ string $1.should == "abc/\n\tdef" end end + describe "when auditing" do + it "should not fail if creating a new file if group is not set" do + File.exists?(@path).should == false + file = Puppet::Type::File.new(:name => @path, :audit => "all", :content => "content") + catalog = Puppet::Resource::Catalog.new + catalog.add_resource(file) + + Puppet::Util::Storage.stubs(:store) # to prevent the catalog from trying to write state.yaml + transaction = catalog.apply + + transaction.report.resource_statuses["File[#{@path}]"].failed.should == false + File.exists?(@path).should == true + end + + it "should not log errors if creating a new file with ensure present and no content" do + File.exists?(@path).should == false + file = Puppet::Type::File.new(:name => @path, :audit => "content", :ensure => "present") + catalog = Puppet::Resource::Catalog.new + catalog.add_resource(file) + + Puppet::Util::Storage.stubs(:store) # to prevent the catalog from trying to write state.yaml + + catalog.apply + @logs.reject {|l| l.level == :notice }.should be_empty + end + end + + describe "when specifying both source and checksum" do + it 'should use the specified checksum when source is first' do + @file[:source] = '/foo' + @file[:checksum] = :md5lite + + @file[:checksum].should be :md5lite + end + it 'should use the specified checksum when source is last' do + @file[:checksum] = :md5lite + @file[:source] = '/foo' + + @file[:checksum].should be :md5lite + end + end end diff --git a/spec/unit/type/mount_spec.rb b/spec/unit/type/mount_spec.rb index ce82cb516..0d74042e3 100755 --- a/spec/unit/type/mount_spec.rb +++ b/spec/unit/type/mount_spec.rb @@ -1,261 +1,261 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:mount) do it "should have a :refreshable feature that requires the :remount method" do Puppet::Type.type(:mount).provider_feature(:refreshable).methods.should == [:remount] end it "should have no default value for :ensure" do mount = Puppet::Type.type(:mount).new(:name => "yay") mount.should(:ensure).should be_nil end end describe Puppet::Type.type(:mount), "when validating attributes" do [:name, :remounts].each do |param| it "should have a #{param} parameter" do Puppet::Type.type(:mount).attrtype(param).should == :param end end [:ensure, :device, :blockdevice, :fstype, :options, :pass, :dump, :atboot, :target].each do |param| it "should have a #{param} property" do Puppet::Type.type(:mount).attrtype(param).should == :property end end end describe Puppet::Type.type(:mount)::Ensure, "when validating values" do before do @provider = stub 'provider', :class => Puppet::Type.type(:mount).defaultprovider, :clear => nil Puppet::Type.type(:mount).defaultprovider.expects(:new).returns(@provider) end it "should alias :present to :defined as a value to :ensure" do mount = Puppet::Type.type(:mount).new(:name => "yay", :ensure => :present) mount.should(:ensure).should == :defined end it "should support :unmounted as a value to :ensure" do mount = Puppet::Type.type(:mount).new(:name => "yay", :ensure => :unmounted) mount.should(:ensure).should == :unmounted end it "should support :absent as a value to :ensure" do Puppet::Type.type(:mount).new(:name => "yay", :ensure => :absent) end it "should support :mounted as a value to :ensure" do Puppet::Type.type(:mount).new(:name => "yay", :ensure => :mounted) end end describe Puppet::Type.type(:mount)::Ensure do before :each do @provider = stub 'provider', :class => Puppet::Type.type(:mount).defaultprovider, :clear => nil, :satisfies? => true, :name => :mock Puppet::Type.type(:mount).defaultprovider.stubs(:new).returns(@provider) @mount = Puppet::Type.type(:mount).new(:name => "yay", :check => :ensure) @ensure = @mount.property(:ensure) end def mount_stub(params) Puppet::Type.type(:mount).validproperties.each do |prop| unless params[prop] params[prop] = :absent @mount[prop] = :absent end end params.each do |param, value| @provider.stubs(param).returns(value) end end describe Puppet::Type.type(:mount)::Ensure, "when retrieving its current state" do it "should return the provider's value if it is :absent" do @provider.expects(:ensure).returns(:absent) @ensure.retrieve.should == :absent end it "should return :mounted if the provider indicates it is mounted and the value is not :absent" do @provider.expects(:ensure).returns(:present) @provider.expects(:mounted?).returns(true) @ensure.retrieve.should == :mounted end it "should return :unmounted if the provider indicates it is not mounted and the value is not :absent" do @provider.expects(:ensure).returns(:present) @provider.expects(:mounted?).returns(false) @ensure.retrieve.should == :unmounted end end describe Puppet::Type.type(:mount)::Ensure, "when changing the host" do it "should destroy itself if it should be absent" do @provider.stubs(:mounted?).returns(false) @provider.expects(:destroy) @ensure.should = :absent @ensure.sync end it "should unmount itself before destroying if it is mounted and should be absent" do @provider.expects(:mounted?).returns(true) @provider.expects(:unmount) @provider.expects(:destroy) @ensure.should = :absent @ensure.sync end it "should create itself if it is absent and should be defined" do @provider.stubs(:ensure).returns(:absent) @provider.stubs(:mounted?).returns(true) @provider.stubs(:mounted?).returns(false) @provider.expects(:create) @ensure.should = :defined @ensure.sync end it "should not unmount itself if it is mounted and should be defined" do @provider.stubs(:ensure).returns(:mounted) @provider.stubs(:mounted?).returns(true) @provider.stubs(:create) @provider.expects(:mount).never @provider.expects(:unmount).never @ensure.should = :defined @ensure.sync end it "should not mount itself if it is unmounted and should be defined" do @provider.stubs(:ensure).returns(:unmounted) @provider.stubs(:mounted?).returns(false) @ensure.stubs(:syncothers) @provider.stubs(:create) @provider.expects(:mount).never @provider.expects(:unmount).never @ensure.should = :present @ensure.sync end it "should unmount itself if it is mounted and should be unmounted" do @provider.stubs(:ensure).returns(:present) @provider.stubs(:mounted?).returns(true) @ensure.stubs(:syncothers) @provider.expects(:unmount) @ensure.should = :unmounted @ensure.sync end it "should create and mount itself if it does not exist and should be mounted" do @provider.stubs(:ensure).returns(:absent) @provider.stubs(:mounted?).returns(false) @provider.expects(:create) @ensure.stubs(:syncothers) @provider.expects(:mount) @ensure.should = :mounted @ensure.sync end it "should mount itself if it is present and should be mounted" do @provider.stubs(:ensure).returns(:present) @provider.stubs(:mounted?).returns(false) @ensure.stubs(:syncothers) @provider.expects(:mount) @ensure.should = :mounted @ensure.sync end it "should create but not mount itself if it is absent and mounted and should be mounted" do @provider.stubs(:ensure).returns(:absent) @provider.stubs(:mounted?).returns(true) @ensure.stubs(:syncothers) @provider.expects(:create) @ensure.should = :mounted @ensure.sync end it "should be insync if it is mounted and should be defined" do @ensure.should = :defined - @ensure.insync?(:mounted).should == true + @ensure.safe_insync?(:mounted).should == true end it "should be insync if it is unmounted and should be defined" do @ensure.should = :defined - @ensure.insync?(:unmounted).should == true + @ensure.safe_insync?(:unmounted).should == true end it "should be insync if it is mounted and should be present" do @ensure.should = :present - @ensure.insync?(:mounted).should == true + @ensure.safe_insync?(:mounted).should == true end it "should be insync if it is unmounted and should be present" do @ensure.should = :present - @ensure.insync?(:unmounted).should == true + @ensure.safe_insync?(:unmounted).should == true end end describe Puppet::Type.type(:mount), "when responding to events" do it "should remount if it is currently mounted" do @provider.expects(:mounted?).returns(true) @provider.expects(:remount) @mount.refresh end it "should not remount if it is not currently mounted" do @provider.expects(:mounted?).returns(false) @provider.expects(:remount).never @mount.refresh end it "should not remount swap filesystems" do @mount[:fstype] = "swap" @provider.expects(:remount).never @mount.refresh end end end describe Puppet::Type.type(:mount), "when modifying an existing mount entry" do before do @provider = stub 'provider', :class => Puppet::Type.type(:mount).defaultprovider, :clear => nil, :satisfies? => true, :name => :mock, :remount => nil Puppet::Type.type(:mount).defaultprovider.stubs(:new).returns(@provider) @mount = Puppet::Type.type(:mount).new(:name => "yay", :ensure => :mounted) {:device => "/foo/bar", :blockdevice => "/other/bar", :target => "/what/ever", :fstype => 'eh', :options => "", :pass => 0, :dump => 0, :atboot => 0, :ensure => :mounted}.each do |param, value| @mount.provider.stubs(param).returns value @mount[param] = value end @mount.provider.stubs(:mounted?).returns true # stub this to not try to create state.yaml Puppet::Util::Storage.stubs(:store) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @mount end it "should use the provider to change the dump value" do @mount.provider.expects(:dump).returns 0 @mount.provider.expects(:dump=).with(1) @mount[:dump] = 1 @catalog.apply end end diff --git a/spec/unit/type/ssh_authorized_key_spec.rb b/spec/unit/type/ssh_authorized_key_spec.rb index a0b435f80..666616c03 100755 --- a/spec/unit/type/ssh_authorized_key_spec.rb +++ b/spec/unit/type/ssh_authorized_key_spec.rb @@ -1,152 +1,152 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' ssh_authorized_key = Puppet::Type.type(:ssh_authorized_key) describe ssh_authorized_key do before do @class = Puppet::Type.type(:ssh_authorized_key) @provider_class = stub 'provider_class', :name => "fake", :suitable? => true, :supports_parameter? => true @class.stubs(:defaultprovider).returns(@provider_class) @class.stubs(:provider).returns(@provider_class) @provider = stub 'provider', :class => @provider_class, :file_path => "/tmp/whatever", :clear => nil @provider_class.stubs(:new).returns(@provider) @catalog = Puppet::Resource::Catalog.new end it "should have a name parameter" do @class.attrtype(:name).should == :param end it "should have :name be its namevar" do @class.key_attributes.should == [:name] end it "should have a :provider parameter" do @class.attrtype(:provider).should == :param end it "should have an ensure property" do @class.attrtype(:ensure).should == :property end it "should support :present as a value for :ensure" do proc { @class.new(:name => "whev", :ensure => :present, :user => "nobody") }.should_not raise_error end it "should support :absent as a value for :ensure" do proc { @class.new(:name => "whev", :ensure => :absent, :user => "nobody") }.should_not raise_error end it "should have an type property" do @class.attrtype(:type).should == :property end it "should support ssh-dss as an type value" do proc { @class.new(:name => "whev", :type => "ssh-dss", :user => "nobody") }.should_not raise_error end it "should support ssh-rsa as an type value" do proc { @class.new(:name => "whev", :type => "ssh-rsa", :user => "nobody") }.should_not raise_error end it "should support :dsa as an type value" do proc { @class.new(:name => "whev", :type => :dsa, :user => "nobody") }.should_not raise_error end it "should support :rsa as an type value" do proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody") }.should_not raise_error end it "should not support values other than ssh-dss, ssh-rsa, dsa, rsa in the ssh_authorized_key_type" do proc { @class.new(:name => "whev", :type => :something) }.should raise_error(Puppet::Error) end it "should have an key property" do @class.attrtype(:key).should == :property end it "should have an user property" do @class.attrtype(:user).should == :property end it "should have an options property" do @class.attrtype(:options).should == :property end it "'s options property should return well formed string of arrays from is_to_s" do resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"]) resource.property(:options).is_to_s(["a","b","c"]).should == "a,b,c" end it "'s options property should return well formed string of arrays from is_to_s" do resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"]) resource.property(:options).should_to_s(["a","b","c"]).should == "a,b,c" end it "should have a target property" do @class.attrtype(:target).should == :property end describe "when neither user nor target is specified" do it "should raise an error" do proc do @class.create( :name => "Test", :key => "AAA", :type => "ssh-rsa", :ensure => :present) end.should raise_error(Puppet::Error) end end describe "when both target and user are specified" do it "should use target" do resource = @class.create( :name => "Test", :user => "root", :target => "/tmp/blah") resource.should(:target).should == "/tmp/blah" end end describe "when user is specified" do it "should determine target" do resource = @class.create( :name => "Test", :user => "root") target = File.expand_path("~root/.ssh/authorized_keys") resource.should(:target).should == target end # Bug #2124 - ssh_authorized_key always changes target if target is not defined it "should not raise spurious change events" do resource = @class.new(:name => "Test", :user => "root") target = File.expand_path("~root/.ssh/authorized_keys") - resource.property(:target).insync?(target).should == true + resource.property(:target).safe_insync?(target).should == true end end describe "when calling validate" do it "should not crash on a non-existant user" do resource = @class.create( :name => "Test", :user => "ihopesuchuserdoesnotexist") proc { resource.validate }.should_not raise_error end end end diff --git a/spec/unit/type/user_spec.rb b/spec/unit/type/user_spec.rb index ccea9ee4c..297134446 100755 --- a/spec/unit/type/user_spec.rb +++ b/spec/unit/type/user_spec.rb @@ -1,304 +1,332 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' user = Puppet::Type.type(:user) describe user do before do ENV["PATH"] += File::PATH_SEPARATOR + "/usr/sbin" unless ENV["PATH"].split(File::PATH_SEPARATOR).include?("/usr/sbin") @provider = stub 'provider' @resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil end it "should have a default provider inheriting from Puppet::Provider" do user.defaultprovider.ancestors.should be_include(Puppet::Provider) end it "should be able to create a instance" do user.new(:name => "foo").should_not be_nil end it "should have an allows_duplicates feature" do user.provider_feature(:allows_duplicates).should_not be_nil end it "should have an manages_homedir feature" do user.provider_feature(:manages_homedir).should_not be_nil end it "should have an manages_passwords feature" do user.provider_feature(:manages_passwords).should_not be_nil end it "should have a manages_solaris_rbac feature" do user.provider_feature(:manages_solaris_rbac).should_not be_nil end it "should have a manages_expiry feature" do user.provider_feature(:manages_expiry).should_not be_nil end it "should have a manages_password_age feature" do user.provider_feature(:manages_password_age).should_not be_nil end describe "instances" do it "should have a valid provider" do user.new(:name => "foo").provider.class.ancestors.should be_include(Puppet::Provider) end it "should delegate existence questions to its provider" do instance = user.new(:name => "foo") instance.provider.expects(:exists?).returns "eh" instance.exists?.should == "eh" end end properties = [:ensure, :uid, :gid, :home, :comment, :shell, :password, :password_min_age, :password_max_age, :groups, :roles, :auths, :profiles, :project, :keys, :expiry] properties.each do |property| it "should have a #{property} property" do user.attrclass(property).ancestors.should be_include(Puppet::Property) end it "should have documentation for its #{property} property" do user.attrclass(property).doc.should be_instance_of(String) end end list_properties = [:groups, :roles, :auths] list_properties.each do |property| it "should have a list '#{property}'" do user.attrclass(property).ancestors.should be_include(Puppet::Property::List) end end it "should have an ordered list 'profiles'" do user.attrclass(:profiles).ancestors.should be_include(Puppet::Property::OrderedList) end it "should have key values 'keys'" do user.attrclass(:keys).ancestors.should be_include(Puppet::Property::KeyValue) end describe "when retrieving all current values" do before do @user = user.new(:name => "foo", :uid => 10) end it "should return a hash containing values for all set properties" do @user[:gid] = 10 @user.property(:ensure).expects(:retrieve).returns :present @user.property(:uid).expects(:retrieve).returns 15 @user.property(:gid).expects(:retrieve).returns 15 values = @user.retrieve [@user.property(:uid), @user.property(:gid)].each { |property| values.should be_include(property) } end it "should set all values to :absent if the user is absent" do @user.property(:ensure).expects(:retrieve).returns :absent @user.property(:uid).expects(:retrieve).never @user.retrieve[@user.property(:uid)].should == :absent end it "should include the result of retrieving each property's current value if the user is present" do @user.property(:ensure).expects(:retrieve).returns :present @user.property(:uid).expects(:retrieve).returns 15 @user.retrieve[@user.property(:uid)].should == 15 end end describe "when managing the ensure property" do before do @ensure = user.attrclass(:ensure).new(:resource => @resource) end it "should support a :present value" do lambda { @ensure.should = :present }.should_not raise_error end it "should support an :absent value" do lambda { @ensure.should = :absent }.should_not raise_error end it "should call :create on the provider when asked to sync to the :present state" do @provider.expects(:create) @ensure.should = :present @ensure.sync end it "should call :delete on the provider when asked to sync to the :absent state" do @provider.expects(:delete) @ensure.should = :absent @ensure.sync end describe "and determining the current state" do it "should return :present when the provider indicates the user exists" do @provider.expects(:exists?).returns true @ensure.retrieve.should == :present end it "should return :absent when the provider indicates the user does not exist" do @provider.expects(:exists?).returns false @ensure.retrieve.should == :absent end end end describe "when managing the uid property" do it "should convert number-looking strings into actual numbers" do uid = user.attrclass(:uid).new(:resource => @resource) uid.should = "50" uid.should.must == 50 end it "should support UIDs as numbers" do uid = user.attrclass(:uid).new(:resource => @resource) uid.should = 50 uid.should.must == 50 end it "should :absent as a value" do uid = user.attrclass(:uid).new(:resource => @resource) uid.should = :absent uid.should.must == :absent end end describe "when managing the gid" do it "should :absent as a value" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = :absent gid.should.must == :absent end it "should convert number-looking strings into actual numbers" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = "50" gid.should.must == 50 end it "should support GIDs specified as integers" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = 50 gid.should.must == 50 end it "should support groups specified by name" do gid = user.attrclass(:gid).new(:resource => @resource) gid.should = "foo" gid.should.must == "foo" end describe "when testing whether in sync" do before do @gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar}) end it "should return true if no 'should' values are set" do @gid = user.attrclass(:gid).new(:resource => @resource) - @gid.must be_insync(500) + @gid.must be_safe_insync(500) end it "should return true if any of the specified groups are equal to the current integer" do Puppet::Util.expects(:gid).with("foo").returns 300 Puppet::Util.expects(:gid).with("bar").returns 500 - @gid.must be_insync(500) + @gid.must be_safe_insync(500) end it "should return false if none of the specified groups are equal to the current integer" do Puppet::Util.expects(:gid).with("foo").returns 300 Puppet::Util.expects(:gid).with("bar").returns 500 - @gid.should_not be_insync(700) + @gid.should_not be_safe_insync(700) end end describe "when syncing" do before do @gid = user.attrclass(:gid).new(:resource => @resource, :should => %w{foo bar}) end it "should use the first found, specified group as the desired value and send it to the provider" do Puppet::Util.expects(:gid).with("foo").returns nil Puppet::Util.expects(:gid).with("bar").returns 500 @provider.expects(:gid=).with 500 @gid.sync end end end describe "when managing expiry" do before do @expiry = user.attrclass(:expiry).new(:resource => @resource) end it "should fail if given an invalid date" do lambda { @expiry.should = "200-20-20" }.should raise_error(Puppet::Error) end end + describe "when managing minimum password age" do + before do + @age = user.attrclass(:password_min_age).new(:resource => @resource) + end + + it "should accept a negative minimum age" do + expect { @age.should = -1 }.should_not raise_error + end + + it "should fail with an empty minimum age" do + expect { @age.should = '' }.should raise_error(Puppet::Error) + end + end + + describe "when managing maximum password age" do + before do + @age = user.attrclass(:password_max_age).new(:resource => @resource) + end + + it "should accept a negative maximum age" do + expect { @age.should = -1 }.should_not raise_error + end + + it "should fail with an empty maximum age" do + expect { @age.should = '' }.should raise_error(Puppet::Error) + end + end + describe "when managing passwords" do before do @password = user.attrclass(:password).new(:resource => @resource, :should => "mypass") end it "should not include the password in the change log when adding the password" do @password.change_to_s(:absent, "mypass").should_not be_include("mypass") end it "should not include the password in the change log when changing the password" do @password.change_to_s("other", "mypass").should_not be_include("mypass") end it "should fail if a ':' is included in the password" do lambda { @password.should = "some:thing" }.should raise_error(Puppet::Error) end it "should allow the value to be set to :absent" do lambda { @password.should = :absent }.should_not raise_error end end describe "when manages_solaris_rbac is enabled" do before do @provider.stubs(:satisfies?).returns(false) @provider.expects(:satisfies?).with([:manages_solaris_rbac]).returns(true) end it "should support a :role value for ensure" do @ensure = user.attrclass(:ensure).new(:resource => @resource) lambda { @ensure.should = :role }.should_not raise_error end end describe "when user has roles" do before do # To test this feature, we have to support it. user.new(:name => "foo").provider.class.stubs(:feature?).returns(true) end it "should autorequire roles" do testuser = Puppet::Type.type(:user).new(:name => "testuser") testuser[:roles] = "testrole" testrole = Puppet::Type.type(:user).new(:name => "testrole") config = Puppet::Resource::Catalog.new :testing do |conf| [testuser, testrole].each { |resource| conf.add_resource resource } end Puppet::Type::User::ProviderDirectoryservice.stubs(:get_macosx_version_major).returns "10.5" rel = testuser.autorequire[0] rel.source.ref.should == testrole.ref rel.target.ref.should == testuser.ref end end end diff --git a/spec/unit/type/zpool_spec.rb b/spec/unit/type/zpool_spec.rb index db12459ab..be8cb12ba 100755 --- a/spec/unit/type/zpool_spec.rb +++ b/spec/unit/type/zpool_spec.rb @@ -1,110 +1,110 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } zpool = Puppet::Type.type(:zpool) describe zpool do before do @provider = stub 'provider' @resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil end properties = [:ensure, :disk, :mirror, :raidz, :spare, :log] properties.each do |property| it "should have a #{property} property" do zpool.attrclass(property).ancestors.should be_include(Puppet::Property) end end parameters = [:pool, :raid_parity] parameters.each do |parameter| it "should have a #{parameter} parameter" do zpool.attrclass(parameter).ancestors.should be_include(Puppet::Parameter) end end end vdev_property = Puppet::Property::VDev describe vdev_property do before do vdev_property.initvars @resource = stub 'resource', :[]= => nil, :property => nil @property = vdev_property.new(:resource => @resource) end it "should be insync if the devices are the same" do @property.should = ["dev1 dev2"] - @property.insync?(["dev2 dev1"]).must be_true + @property.safe_insync?(["dev2 dev1"]).must be_true end it "should be out of sync if the devices are not the same" do @property.should = ["dev1 dev3"] - @property.insync?(["dev2 dev1"]).must be_false + @property.safe_insync?(["dev2 dev1"]).must be_false end it "should be insync if the devices are the same and the should values are comma seperated" do @property.should = ["dev1", "dev2"] - @property.insync?(["dev2 dev1"]).must be_true + @property.safe_insync?(["dev2 dev1"]).must be_true end it "should be out of sync if the device is absent and should has a value" do @property.should = ["dev1", "dev2"] - @property.insync?(:absent).must be_false + @property.safe_insync?(:absent).must be_false end it "should be insync if the device is absent and should is absent" do @property.should = [:absent] - @property.insync?(:absent).must be_true + @property.safe_insync?(:absent).must be_true end end multi_vdev_property = Puppet::Property::MultiVDev describe multi_vdev_property do before do multi_vdev_property.initvars @resource = stub 'resource', :[]= => nil, :property => nil @property = multi_vdev_property.new(:resource => @resource) end it "should be insync if the devices are the same" do @property.should = ["dev1 dev2"] - @property.insync?(["dev2 dev1"]).must be_true + @property.safe_insync?(["dev2 dev1"]).must be_true end it "should be out of sync if the devices are not the same" do @property.should = ["dev1 dev3"] - @property.insync?(["dev2 dev1"]).must be_false + @property.safe_insync?(["dev2 dev1"]).must be_false end it "should be out of sync if the device is absent and should has a value" do @property.should = ["dev1", "dev2"] - @property.insync?(:absent).must be_false + @property.safe_insync?(:absent).must be_false end it "should be insync if the device is absent and should is absent" do @property.should = [:absent] - @property.insync?(:absent).must be_true + @property.safe_insync?(:absent).must be_true end describe "when there are multiple lists of devices" do it "should be in sync if each group has the same devices" do @property.should = ["dev1 dev2", "dev3 dev4"] - @property.insync?(["dev2 dev1", "dev3 dev4"]).must be_true + @property.safe_insync?(["dev2 dev1", "dev3 dev4"]).must be_true end it "should be out of sync if any group has the different devices" do @property.should = ["dev1 devX", "dev3 dev4"] - @property.insync?(["dev2 dev1", "dev3 dev4"]).must be_false + @property.safe_insync?(["dev2 dev1", "dev3 dev4"]).must be_false end it "should be out of sync if devices are in the wrong group" do @property.should = ["dev1 dev2", "dev3 dev4"] - @property.insync?(["dev2 dev3", "dev1 dev4"]).must be_false + @property.safe_insync?(["dev2 dev3", "dev1 dev4"]).must be_false end end end diff --git a/spec/unit/util/zaml_spec.rb b/spec/unit/util/zaml_spec.rb index f2bcefe01..fd506ea93 100755 --- a/spec/unit/util/zaml_spec.rb +++ b/spec/unit/util/zaml_spec.rb @@ -1,39 +1,90 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/util/monkey_patches' describe "Pure ruby yaml implementation" do { 7 => "--- 7", 3.14159 => "--- 3.14159", 'test' => "--- test", [] => "--- []", :symbol => "--- !ruby/sym symbol", {:a => "A"} => "--- \n !ruby/sym a: A", {:a => "x\ny"} => "--- \n !ruby/sym a: |-\n x\n y" }.each { |o,y| it "should convert the #{o.class} #{o.inspect} to yaml" do o.to_yaml.should == y end it "should produce yaml for the #{o.class} #{o.inspect} that can be reconstituted" do YAML.load(o.to_yaml).should == o end } # # Can't test for equality on raw objects { Object.new => "--- !ruby/object {}", [Object.new] => "--- \n - !ruby/object {}", {Object.new => Object.new} => "--- \n ? !ruby/object {}\n : !ruby/object {}" }.each { |o,y| it "should convert the #{o.class} #{o.inspect} to yaml" do o.to_yaml.should == y end it "should produce yaml for the #{o.class} #{o.inspect} that can be reconstituted" do lambda { YAML.load(o.to_yaml) }.should_not raise_error end } + + def set_of_lines(l) + l.split("\n").sort + end + + it "should handle references to Array in Hash values correctly" do + list = [1] + data = { "one" => list, "two" => list } + data.to_yaml.should == "--- \n two: &id001 \n - 1\n one: *id001" + expect { YAML.load(data.to_yaml).should == data }.should_not raise_error + end + + it "should handle references to Hash in Hash values correctly" do + hash = { 1 => 1 } + data = { "one" => hash, "two" => hash } + # This could still someday fail because the order change would also change which one got the back ref + set_of_lines(data.to_yaml).should == set_of_lines("--- \n two: &id001 \n 1: 1\n one: *id001") + expect { YAML.load(data.to_yaml).should == data }.should_not raise_error + end + + it "should handle references to Scalar in Hash" do + str = "hello" + data = { "one" => str, "two" => str } + set_of_lines(data.to_yaml).should == set_of_lines("--- \n two: hello\n one: hello") + expect { YAML.load(data.to_yaml).should == data }.should_not raise_error + end + + class Zaml_test_class_A + attr_reader :false,:true + def initialize + @false = @true = 7 + end + end + it "should not blow up when magic strings are used as field names" do + data = Zaml_test_class_A.new + data.to_yaml.should == %Q{--- !ruby/object:Zaml_test_class_A\n \"false\": 7\n \"true\": 7} + expect { + r = YAML.load(data.to_yaml) + r.class.should == data.class + r.true.should == data.true + r.false.should == data.false + }.should_not raise_error + end + + it "should not blow up on back references inside arrays" do + s = [1,2] + data = [s,s] + data.to_yaml.should == %Q{--- \n - &id001 \n - 1\n - 2\n - *id001} + expect { YAML.load(data.to_yaml).should == data }.should_not raise_error + end + end