diff --git a/acceptance/tests/pluginsync/feature/pluginsync_should_sync_features.rb b/acceptance/tests/pluginsync/feature/pluginsync_should_sync_features.rb index a61033348..69c8a89ce 100644 --- a/acceptance/tests/pluginsync/feature/pluginsync_should_sync_features.rb +++ b/acceptance/tests/pluginsync/feature/pluginsync_should_sync_features.rb @@ -1,192 +1,192 @@ test_name "the pluginsync functionality should sync feature definitions" # # This test is intended to ensure that pluginsync syncs feature definitions to # the agents. It checks the feature twice; once to make sure that it gets # loaded successfully during the run in which it was synced, and once to ensure # that it still gets loaded successfully during the subsequent run (in which it # should not be synced because the files haven't changed.) # require 'puppet/acceptance/temp_file_utils' extend Puppet::Acceptance::TempFileUtils initialize_temp_dirs() all_tests_passed = false ############################################################################### # BEGIN TEST LOGIC ############################################################################### # create some vars to point to the directories that we're going to point the master/agents at test_identifier = "pluginsync_should_sync_features" environments_dir = "environments" master_module_dir = "#{environments_dir}/production/modules" agent_lib_dir = "agent_lib" module_name = "superbogus" # here we create a custom type, which basically doesn't do anything except for test the value of # our custom feature and write the result to a file agent_module_type_file = "#{agent_lib_dir}/puppet/type/#{module_name}.rb" master_module_type_file = "#{master_module_dir}/#{module_name}/lib/puppet/type/#{module_name}.rb" master_module_type_content = < "Hi. I'm setting the testfeature property of #{module_name} here in site.pp", } HERE # for convenience we build up a list of all of the files we are expecting to deploy on the master all_master_files = [ [master_module_feature_file, 'feature'], [master_module_type_file, 'type'], [master_manifest_file, 'manifest'] ] # for convenience we build up a list of all of the files we are expecting to deploy on the agents all_agent_files = [ [agent_module_feature_file, 'feature'], [agent_module_type_file, 'type'] ] # the command line args we'll pass to the agent each time we call it agent_args = "--trace --libdir=\"%s\" --pluginsync --no-daemonize --verbose " + "--onetime --test --server #{master}" # legal exit codes whenever we run the agent # we need to allow exit code 2, which means "changes were applied" on the agent agent_exit_codes = [0, 2] # this begin block is here for handling temp file cleanup via an "ensure" block at the very end of the # test. begin # copy all the files to the master step "write our simple module out to the master" do create_test_file(master, master_module_type_file, master_module_type_content, :mkdirs => true) create_test_file(master, master_module_feature_file, master_module_feature_content, :mkdirs => true) create_test_file(master, master_manifest_file, master_manifest_content, :mkdirs => true) end step "verify that the module and manifest files exist on the master" do all_master_files.each do |file_path, desc| unless test_file_exists?(master, file_path) then fail_test("Failed to create #{desc} file '#{get_test_file_path(master, file_path)}' on master") end end end step "start the master" do master_opts = { 'main' => { 'environmentpath' => "#{get_test_file_path(master, environments_dir)}", }, } with_puppet_running_on master, master_opts do # the module files shouldn't exist on the agent yet because they haven't been synced step "verify that the module files don't exist on the agent path" do agents.each do |agent| all_agent_files.each do |file_path, desc| if test_file_exists?(agent, file_path) then fail_test("#{desc} file already exists on agent: '#{get_test_file_path(agent, file_path)}'") end end end end step "run the agent and verify that it loaded the feature" do agents.each do |agent| on(agent, puppet('agent', agent_args % get_test_file_path(agent, agent_lib_dir)), :acceptable_exit_codes => agent_exit_codes) do assert_match(/The value of the #{module_name} feature is: true/, result.stdout, "Expected agent stdout to include confirmation that the feature was 'true'") end end end step "verify that the module files were synced down to the agent" do agents.each do |agent| all_agent_files.each do |file_path, desc| unless test_file_exists?(agent, file_path) then fail_test("Expected #{desc} file not synced to agent: '#{get_test_file_path(agent, file_path)}'") end end end end step "run the agent again" do agents.each do |agent| on(agent, puppet('agent', agent_args % get_test_file_path(agent, agent_lib_dir)), :acceptable_exit_codes => agent_exit_codes) do assert_match(/The value of the #{module_name} feature is: true/, result.stdout, "Expected agent stdout to include confirmation that the feature was 'true'") end end end #TODO: was thinking about putting in a check for the timestamps on the files (maybe add a method for that to # the framework?) to verify that they didn't get re-synced, but it seems like more trouble than it's worth # at the moment. #step "verify that the module files were not re-synced" do # fail_test("NOT YET IMPLEMENTED: verify that the module files were not re-synced") #end end all_tests_passed = true end ensure ########################################################################################## # Clean up all of the temp files created by this test. It would be nice if this logic # could be handled outside of the test itself; I envision a stanza like this one appearing # in a very large number of the tests going forward unless it is handled by the framework. ########################################################################################## if all_tests_passed then remove_temp_dirs() end end diff --git a/lib/puppet.rb b/lib/puppet.rb index 8ca27189d..38d3a3ead 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,246 +1,247 @@ require 'puppet/version' # see the bottom of the file for further inclusions # Also see the new Vendor support - towards the end # require 'facter' require 'puppet/error' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/settings' require 'puppet/util/feature' require 'puppet/util/suidmanager' require 'puppet/util/run_mode' require 'puppet/external/pson/common' require 'puppet/external/pson/version' require 'puppet/external/pson/pure' #------------------------------------------------------------ # the top-level module # # all this really does is dictate how the whole system behaves, through # preferences for things like debugging # # it's also a place to find top-level commands like 'debug' # The main Puppet class. Everything is contained here. # # @api public module Puppet require 'puppet/file_system' require 'puppet/context' require 'puppet/environments' class << self include Puppet::Util attr_reader :features end # the hash that determines how our system behaves @@settings = Puppet::Settings.new # Note: It's important that these accessors (`self.settings`, `self.[]`) are # defined before we try to load any "features" (which happens a few lines below), # because the implementation of the features loading may examine the values of # settings. def self.settings @@settings end # Get the value for a setting # # @param [Symbol] param the setting to retrieve # # @api public def self.[](param) if param == :debug return Puppet::Util::Log.level == :debug else return @@settings[param] end end require 'puppet/util/logging' extend Puppet::Util::Logging # The feature collection @features = Puppet::Util::Feature.new('puppet/feature') # Load the base features. require 'puppet/feature/base' # Store a new default value. def self.define_settings(section, hash) @@settings.define_settings(section, hash) end # setting access and stuff def self.[]=(param,value) @@settings[param] = value end def self.clear @@settings.clear end def self.debug=(value) if value Puppet::Util::Log.level=(:debug) else Puppet::Util::Log.level=(:notice) end end def self.run_mode # This sucks (the existence of this method); there are a lot of places in our code that branch based the value of # "run mode", but there used to be some really confusing code paths that made it almost impossible to determine # when during the lifecycle of a puppet application run the value would be set properly. A lot of the lifecycle # stuff has been cleaned up now, but it still seems frightening that we rely so heavily on this value. # # I'd like to see about getting rid of the concept of "run_mode" entirely, but there are just too many places in # the code that call this method at the moment... so I've settled for isolating it inside of the Settings class # (rather than using a global variable, as we did previously...). Would be good to revisit this at some point. # # --cprice 2012-03-16 Puppet::Util::RunMode[@@settings.preferred_run_mode] end # Load all of the settings. require 'puppet/defaults' # Initialize puppet's settings. This is intended only for use by external tools that are not # built off of the Faces API or the Puppet::Util::Application class. It may also be used # to initialize state so that a Face may be used programatically, rather than as a stand-alone # command-line tool. # # @api public # @param args [Array] the command line arguments to use for initialization # @return [void] def self.initialize_settings(args = []) do_initialize_settings_for_run_mode(:user, args) end # private helper method to provide the implementation details of initializing for a run mode, # but allowing us to control where the deprecation warning is issued def self.do_initialize_settings_for_run_mode(run_mode, args) Puppet.settings.initialize_global_settings(args) run_mode = Puppet::Util::RunMode[run_mode] Puppet.settings.initialize_app_defaults(Puppet::Settings.app_defaults_for_run_mode(run_mode)) Puppet.push_context(Puppet.base_context(Puppet.settings), "Initial context after settings initialization") Puppet::Parser::Functions.reset end private_class_method :do_initialize_settings_for_run_mode # Create a new type. Just proxy to the Type class. The mirroring query # code was deprecated in 2008, but this is still in heavy use. I suppose # this can count as a soft deprecation for the next dev. --daniel 2011-04-12 def self.newtype(name, options = {}, &block) + Puppet.deprecation_warning("Puppet.newtype is deprecated and will be removed in a future release. Use Puppet::Type.newtype instead.") Puppet::Type.newtype(name, options, &block) end # Load vendored (setup paths, and load what is needed upfront). # See the Vendor class for how to add additional vendored gems/code require "puppet/vendor" Puppet::Vendor.load_vendored # The bindings used for initialization of puppet # @api private def self.base_context(settings) environments = settings[:environmentpath] modulepath = Puppet::Node::Environment.split_path(settings[:basemodulepath]) if environments.empty? loaders = [Puppet::Environments::Legacy.new] else loaders = Puppet::Environments::Directories.from_path(environments, modulepath) # in case the configured environment (used for the default sometimes) # doesn't exist default_environment = Puppet[:environment].to_sym if default_environment == :production loaders << Puppet::Environments::StaticPrivate.new( Puppet::Node::Environment.create(Puppet[:environment].to_sym, [], Puppet::Node::Environment::NO_MANIFEST)) end end { :environments => Puppet::Environments::Cached.new(Puppet::Environments::Combined.new(*loaders)), :http_pool => proc { require 'puppet/network/http' Puppet::Network::HTTP::NoCachePool.new } } end # A simple set of bindings that is just enough to limp along to # initialization where the {base_context} bindings are put in place # @api private def self.bootstrap_context root_environment = Puppet::Node::Environment.create(:'*root*', [], Puppet::Node::Environment::NO_MANIFEST) { :current_environment => root_environment, :root_environment => root_environment } end # @param overrides [Hash] A hash of bindings to be merged with the parent context. # @param description [String] A description of the context. # @api private def self.push_context(overrides, description = "") @context.push(overrides, description) end # Return to the previous context. # @raise [StackUnderflow] if the current context is the root # @api private def self.pop_context @context.pop end # Lookup a binding by name or return a default value provided by a passed block (if given). # @api private def self.lookup(name, &block) @context.lookup(name, &block) end # @param bindings [Hash] A hash of bindings to be merged with the parent context. # @param description [String] A description of the context. # @yield [] A block executed in the context of the temporarily pushed bindings. # @api private def self.override(bindings, description = "", &block) @context.override(bindings, description, &block) end # @api private def self.mark_context(name) @context.mark(name) end # @api private def self.rollback_context(name) @context.rollback(name) end require 'puppet/node' # The single instance used for normal operation @context = Puppet::Context.new(bootstrap_context) end # This feels weird to me; I would really like for us to get to a state where there is never a "require" statement # anywhere besides the very top of a file. That would not be possible at the moment without a great deal of # effort, but I think we should strive for it and revisit this at some point. --cprice 2012-03-16 require 'puppet/indirector' require 'puppet/type' require 'puppet/resource' require 'puppet/parser' require 'puppet/network' require 'puppet/ssl' require 'puppet/module' require 'puppet/data_binding' require 'puppet/util/storage' require 'puppet/status' require 'puppet/file_bucket/file' diff --git a/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb index cfe00bb66..e2ac02c2c 100644 --- a/lib/puppet/type/exec.rb +++ b/lib/puppet/type/exec.rb @@ -1,592 +1,592 @@ module Puppet - newtype(:exec) do + Type.newtype(:exec) do include Puppet::Util::Execution require 'timeout' @doc = "Executes external commands. Any command in an `exec` resource **must** be able to run multiple times without causing harm --- that is, it must be *idempotent*. There are three main ways for an exec to be idempotent: * The command itself is already idempotent. (For example, `apt-get update`.) * The exec has an `onlyif`, `unless`, or `creates` attribute, which prevents Puppet from running the command unless some condition is met. * The exec has `refreshonly => true`, which only allows Puppet to run the command when some other resource is changed. (See the notes on refreshing below.) A caution: There's a widespread tendency to use collections of execs to manage resources that aren't covered by an existing resource type. This works fine for simple tasks, but once your exec pile gets complex enough that you really have to think to understand what's happening, you should consider developing a custom resource type instead, as it will be much more predictable and maintainable. **Refresh:** `exec` resources can respond to refresh events (via `notify`, `subscribe`, or the `~>` arrow). The refresh behavior of execs is non-standard, and can be affected by the `refresh` and `refreshonly` attributes: * If `refreshonly` is set to true, the exec will _only_ run when it receives an event. This is the most reliable way to use refresh with execs. * If the exec already would have run and receives an event, it will run its command **up to two times.** (If an `onlyif`, `unless`, or `creates` condition is no longer met after the first run, the second run will not occur.) * If the exec already would have run, has a `refresh` command, and receives an event, it will run its normal command, then run its `refresh` command (as long as any `onlyif`, `unless`, or `creates` conditions are still met after the normal command finishes). * If the exec would **not** have run (due to an `onlyif`, `unless`, or `creates` attribute) and receives an event, it still will not run. * If the exec has `noop => true`, would otherwise have run, and receives an event from a non-noop resource, it will run once (or run its `refresh` command instead, if it has one). In short: If there's a possibility of your exec receiving refresh events, it becomes doubly important to make sure the run conditions are restricted. **Autorequires:** If Puppet is managing an exec's cwd or the executable file used in an exec's command, the exec resource will autorequire those files. If Puppet is managing the user that an exec should run as, the exec resource will autorequire that user." # Create a new check mechanism. It's basically just a parameter that # provides one extra 'check' method. def self.newcheck(name, options = {}, &block) @checks ||= {} check = newparam(name, options, &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 exit code(s). An error will be returned if the executed command has some other exit code. Defaults to 0. Can be specified as an array of acceptable exit codes or a single value. On POSIX systems, exit codes are always integers between 0 and 255. On Windows, **most** exit codes should be integers between 0 and 2147483647. Larger exit codes on Windows can behave inconsistently across different tools. The Win32 APIs define exit codes as 32-bit unsigned integers, but both the cmd.exe shell and the .NET runtime cast them to signed integers. This means some tools will report negative numbers for exit codes above 2147483647. (For example, cmd.exe reports 4294967295 as -1.) Since Puppet uses the plain Win32 APIs, it will report the very large number instead of the negative number, which might not be what you expect if you got the exit code from a cmd.exe session. Microsoft recommends against using negative/very large exit codes, and you should avoid them when possible. To convert a negative exit code to the positive one Puppet will use, add it to 4294967296." # Make output a bit prettier def change_to_s(currentvalue, newvalue) "executed successfully" end # First verify that all of our checks pass. def retrieve # We need to return :notrun to trigger evaluation; when that isn't # true, we *LIE* about what happened and return a "success" for the # value, which causes us to be treated as in_sync?, which means we # don't actually execute anything. I think. --daniel 2011-03-10 if @resource.check_all_attributes return :notrun else return self.should end end # Actually execute the command. def sync 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 = provider.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 Puppet::Error, "Command exceeded timeout", $! 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." validate do |command| raise ArgumentError, "Command must be a String, got value of class #{command.class}" unless command.is_a? String end 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 '#{File::PATH_SEPARATOR}' separated list." # Support both arrays and colon-separated fields. def value=(*values) @value = values.flatten.collect { |val| val.split(File::PATH_SEPARATOR) }.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. Please note that the $HOME environment variable is not automatically set when using this attribute." validate do |user| if Puppet.features.microsoft_windows? self.fail "Unable to execute commands as other users on Windows" elsif !Puppet.features.root? && resource.current_username() != user self.fail "Only root can execute commands as other users" end 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 commands as different users in the shell." # Validation is handled by the SUIDManager class. end newparam(:cwd, :parent => Puppet::Parameter::Path) do desc "The directory from which to run the command. If this directory does not exist, the command will fail." end newparam(:logoutput) do desc "Whether to log command output in addition to logging the exit code. Defaults to `on_failure`, which only logs the output when the command has an exit code that does not match any value specified by the `returns` attribute. As with any resource type, the log level can be controlled with the `loglevel` metaparameter." defaultto :on_failure 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| provider.validatecmd(command) 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(:umask, :required_feature => :umask) do desc "Sets the umask to be used while executing this command" munge do |value| if value =~ /^0?[0-7]{1,4}$/ return value.to_i(8) else raise Puppet::Error, "The umask specification is invalid: #{value.inspect}" 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. The timeout is specified in seconds. The default timeout is 300 seconds and you can set it to 0 to disable the timeout." munge do |value| value = value.shift if value.is_a?(Array) begin value = Float(value) rescue ArgumentError raise ArgumentError, "The timeout must be a number.", $!.backtrace end [value, 0.0].max 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 parameter 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 <<-'EOT' 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`. EOT 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, :parent => Puppet::Parameter::Path) do desc <<-'EOT' A file to look for before running the command. The command will only run if the file **doesn't exist.** This parameter doesn't cause Puppet to create a file; it is only useful if **the command itself** creates a file. exec { "tar -xf /Volumes/nfs02/important.tar": cwd => "/var/tmp", creates => "/var/tmp/myfile", path => ["/usr/bin", "/usr/sbin"] } In this example, `myfile` is assumed to be a file inside `important.tar`. If it is ever deleted, the exec will bring it back by re-extracting the tarball. If `important.tar` does **not** actually contain `myfile`, the exec will keep running every time Puppet runs. EOT accept_arrays # If the file exists, return false (i.e., don't run the command), # else return true def check(value) ! Puppet::FileSystem.exist?(value) end end newcheck(:unless) do desc <<-'EOT' If this parameter is set, then this `exec` will run unless the command has an exit code of 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. It also uses the same provider as the main command, so any behavior that differs by provider will match. EOT validate do |cmds| cmds = [cmds] unless cmds.is_a? Array cmds.each do |command| provider.validatecmd(command) end end # Return true if the command does not return 0. def check(value) begin output, status = provider.run(value, true) rescue Timeout::Error err "Check #{value.inspect} exceeded timeout" return false end output.split(/\n/).each { |line| self.debug(line) } status.exitstatus != 0 end end newcheck(:onlyif) do desc <<-'EOT' If this parameter is set, then this `exec` will only run if the command has an exit code of 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. It also uses the same provider as the main command, so any behavior that differs by provider will match. 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. EOT validate do |cmds| cmds = [cmds] unless cmds.is_a? Array cmds.each do |command| provider.validatecmd(command) end end # Return true if the command returns 0. def check(value) begin output, status = provider.run(value, true) rescue Timeout::Error err "Check #{value.inspect} exceeded timeout" return false end output.split(/\n/).each { |line| self.debug(line) } status.exitstatus == 0 end end # Exec names are not isomorphic with the objects. @isomorphic = false validate do provider.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] file_regex = Puppet.features.microsoft_windows? ? %r{^([a-zA-Z]:[\\/]\S+)} : %r{^(/\S+)} self[:command].scan(file_regex) { |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(file_regex) 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_all_attributes(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 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_all_attributes(true) if cmd = self[:refresh] provider.run(cmd) else self.property(:returns).sync end end end def current_username Etc.getpwuid(Process.uid).name end end end diff --git a/lib/puppet/type/filebucket.rb b/lib/puppet/type/filebucket.rb index cbb551464..597a1e4af 100644 --- a/lib/puppet/type/filebucket.rb +++ b/lib/puppet/type/filebucket.rb @@ -1,122 +1,122 @@ module Puppet require 'puppet/file_bucket/dipper' - newtype(:filebucket) do + Type.newtype(:filebucket) do @doc = <<-EOT A repository for storing and retrieving file content by MD5 checksum. Can be local to each agent node, or centralized on a puppet master server. All puppet masters provide a filebucket service that agent nodes can access via HTTP, but you must declare a filebucket resource before any agents will do so. Filebuckets are used for the following features: - **Content backups.** If the `file` type's `backup` attribute is set to the name of a filebucket, Puppet will back up the _old_ content whenever it rewrites a file; see the documentation for the `file` type for more details. These backups can be used for manual recovery of content, but are more commonly used to display changes and differences in a tool like Puppet Dashboard. - **Content distribution.** The optional static compiler populates the puppet master's filebucket with the _desired_ content for each file, then instructs the agent to retrieve the content for a specific checksum. For more details, [see the `static_compiler` section in the catalog indirection docs](http://docs.puppetlabs.com/references/latest/indirection.html#catalog). To use a central filebucket for backups, you will usually want to declare a filebucket resource and a resource default for the `backup` attribute in site.pp: # /etc/puppet/manifests/site.pp filebucket { 'main': path => false, # This is required for remote filebuckets. server => 'puppet.example.com', # Optional; defaults to the configured puppet master. } File { backup => main, } Puppet master servers automatically provide the filebucket service, so this will work in a default configuration. If you have a heavily restricted `auth.conf` file, you may need to allow access to the `file_bucket_file` endpoint. EOT newparam(:name) do desc "The name of the filebucket." isnamevar end newparam(:server) do desc "The server providing the remote filebucket service. Defaults to the value of the `server` setting (that is, the currently configured puppet master server). This setting is _only_ consulted if the `path` attribute is set to `false`." defaultto { Puppet[:server] } end newparam(:port) do desc "The port on which the remote server is listening. Defaults to the value of the `masterport` setting, which is usually %s." % Puppet[:masterport] defaultto { Puppet[:masterport] } end newparam(:path) do desc "The path to the _local_ filebucket; defaults to the value of the `clientbucketdir` setting. To use a remote filebucket, you _must_ set this attribute to `false`." defaultto { Puppet[:clientbucketdir] } validate do |value| if value.is_a? Array raise ArgumentError, "You can only have one filebucket path" end if value.is_a? String and not Puppet::Util.absolute_path?(value) raise ArgumentError, "Filebucket paths must be absolute" end true end end # Create a default filebucket. def self.mkdefaultbucket new(:name => "puppet", :path => Puppet[:clientbucketdir]) end def bucket mkbucket unless defined?(@bucket) @bucket end private def mkbucket # Default is a local filebucket, if no server is given. # If the default path has been removed, too, then # the puppetmaster is used as default server type = "local" args = {} if self[:path] args[:Path] = self[:path] else args[:Server] = self[:server] args[:Port] = self[:port] end begin @bucket = Puppet::FileBucket::Dipper.new(args) rescue => detail message = "Could not create #{type} filebucket: #{detail}" self.log_exception(detail, message) self.fail(message) end @bucket.name = self.name end end end diff --git a/lib/puppet/type/group.rb b/lib/puppet/type/group.rb index d5adaf455..7718c40c8 100644 --- a/lib/puppet/type/group.rb +++ b/lib/puppet/type/group.rb @@ -1,188 +1,188 @@ require 'etc' require 'facter' require 'puppet/property/keyvalue' require 'puppet/parameter/boolean' module Puppet - newtype(:group) do + Type.newtype(:group) do @doc = "Manage groups. On most platforms this can only create groups. Group membership must be managed on individual users. On some platforms such as OS X, group membership is managed as an attribute of the group, not the user record. Providers must have the feature 'manages_members' to manage the 'members' property of a group record." feature :manages_members, "For directories where membership is an attribute of groups not users." feature :manages_aix_lam, "The provider can manage AIX Loadable Authentication Module (LAM) system." feature :system_groups, "The provider allows you to create system groups with lower GIDs." feature :libuser, "Allows local groups to be managed on systems that also use some other remote NSS method of managing accounts." ensurable do desc "Create or remove the group." newvalue(:present) do provider.create end newvalue(:absent) do provider.delete end end newproperty(:gid) do desc "The group ID. Must be specified numerically. If no group ID is specified when creating a new group, then one will be chosen automatically according to local system standards. This will likely result in the same group having different GIDs on different systems, which is not recommended. On Windows, this property is read-only and will return the group's security identifier (SID)." def retrieve provider.gid end def sync if self.should == :absent raise Puppet::DevError, "GID cannot be deleted" else provider.gid = self.should end end munge do |gid| case gid when String if gid =~ /^[-0-9]+$/ gid = Integer(gid) else self.fail "Invalid GID #{gid}" end when Symbol unless gid == :absent self.devfail "Invalid GID #{gid}" end end return gid end end newproperty(:members, :array_matching => :all, :required_features => :manages_members) do desc "The members of the group. For directory services where group membership is stored in the group objects, not the users." def change_to_s(currentvalue, newvalue) currentvalue = currentvalue.join(",") if currentvalue != :absent newvalue = newvalue.join(",") super(currentvalue, newvalue) end def insync?(current) if provider.respond_to?(:members_insync?) return provider.members_insync?(current, @should) end super(current) end def is_to_s(currentvalue) if provider.respond_to?(:members_to_s) currentvalue = '' if currentvalue.nil? return provider.members_to_s(currentvalue.split(',')) end super(currentvalue) end alias :should_to_s :is_to_s end newparam(:auth_membership) do desc "whether the provider is authoritative for group membership." defaultto true end newparam(:name) do desc "The group name. While naming limitations vary by operating system, it is advisable to restrict names to the lowest common denominator, which is a maximum of 8 characters beginning with a letter. Note that Puppet considers group names to be case-sensitive, regardless of the platform's own rules; be sure to always use the same case when referring to a given group." isnamevar end newparam(:allowdupe, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether to allow duplicate GIDs. Defaults to `false`." defaultto false end newparam(:ia_load_module, :required_features => :manages_aix_lam) do desc "The name of the I&A module to use to manage this user" end newproperty(:attributes, :parent => Puppet::Property::KeyValue, :required_features => :manages_aix_lam) do desc "Specify group AIX attributes in an array of `key=value` pairs." def membership :attribute_membership end def delimiter " " end validate do |value| raise ArgumentError, "Attributes value pairs must be separated by an =" unless value.include?("=") end end newparam(:attribute_membership) do desc "Whether specified attribute 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 newparam(:system, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether the group is a system group with lower GID." defaultto false end newparam(:forcelocal, :boolean => true, :required_features => :libuser, :parent => Puppet::Parameter::Boolean) do desc "Forces the management of local accounts when accounts are also being managed by some other NSS" defaultto false end # This method has been exposed for puppet to manage users and groups of # files in its settings and should not be considered available outside of # puppet. # # (see Puppet::Settings#service_group_available?) # # @return [Boolean] if the group exists on the system # @api private def exists? provider.exists? end end end diff --git a/lib/puppet/type/host.rb b/lib/puppet/type/host.rb index 11cb6ed51..510c9a9df 100644 --- a/lib/puppet/type/host.rb +++ b/lib/puppet/type/host.rb @@ -1,95 +1,95 @@ require 'puppet/property/ordered_list' module Puppet - newtype(:host) do + Type.newtype(:host) do ensurable newproperty(:ip) do desc "The host's IP address, IPv4 or IPv6." def valid_v4?(addr) if /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ =~ addr return $~.captures.all? {|i| i = i.to_i; i >= 0 and i <= 255 } end return false end def valid_v6?(addr) # http://forums.dartware.com/viewtopic.php?t=452 # ...and, yes, it is this hard. Doing it programatically is harder. return true if addr =~ /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/ return false end def valid_newline?(addr) return false if (addr =~ /\n/ || addr =~ /\r/) return true end validate do |value| return true if ((valid_v4?(value) || valid_v6?(value)) && (valid_newline?(value))) raise Puppet::Error, "Invalid IP address #{value.inspect}" end end # for now we use OrderedList to indicate that the order does matter. newproperty(:host_aliases, :parent => Puppet::Property::OrderedList) do desc "Any aliases the host might have. Multiple values must be specified as an array." def delimiter " " end def inclusive? true end validate do |value| # This regex already includes newline check. raise Puppet::Error, "Host aliases cannot include whitespace" if value =~ /\s/ raise Puppet::Error, "Host aliases cannot be an empty string. Use an empty array to delete all host_aliases " if value =~ /^\s*$/ end end newproperty(:comment) do desc "A comment that will be attached to the line with a # character." validate do |value| raise Puppet::Error, "Comment cannot include newline" if (value =~ /\n/ || value =~ /\r/) end end newproperty(:target) do desc "The file in which to store service information. Only used by 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| value.split('.').each do |hostpart| unless hostpart =~ /^([\d\w]+|[\d\w][\d\w\-]+[\d\w])$/ raise Puppet::Error, "Invalid host name" end end raise Puppet::Error, "Hostname cannot include newline" if (value =~ /\n/ || value =~ /\r/) 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/mailalias.rb b/lib/puppet/type/mailalias.rb index ce7ca790b..0a8f03af1 100644 --- a/lib/puppet/type/mailalias.rb +++ b/lib/puppet/type/mailalias.rb @@ -1,48 +1,48 @@ module Puppet - newtype(:mailalias) do + Type.newtype(:mailalias) do @doc = "Creates an email alias in the local alias database." ensurable newparam(:name, :namevar => true) do desc "The alias name." end newproperty(:recipient, :array_matching => :all) do desc "Where email should be sent. Multiple values should be specified as an array." def is_to_s(value) if value.include?(:absent) super else value.join(",") end end def should @should end def should_to_s(value) if value.include?(:absent) super else value.join(",") end end end newproperty(:target) do desc "The file in which to store the aliases. 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 end end diff --git a/lib/puppet/type/maillist.rb b/lib/puppet/type/maillist.rb index 4e0542c83..dfe5e22d5 100644 --- a/lib/puppet/type/maillist.rb +++ b/lib/puppet/type/maillist.rb @@ -1,62 +1,62 @@ module Puppet - newtype(:maillist) do + Type.newtype(:maillist) do @doc = "Manage email lists. This resource type can only create and remove lists; it cannot currently reconfigure them." ensurable do defaultvalues newvalue(:purged) do provider.purge end def change_to_s(current_value, newvalue) return "Purged #{resource}" if newvalue == :purged super end def insync?(is) return true if is == :absent && should == :purged super end end newparam(:name, :namevar => true) do desc "The name of the email list." end newparam(:description) do desc "The description of the mailing list." end newparam(:password) do desc "The admin password." end newparam(:webserver) do desc "The name of the host providing web archives and the administrative interface." end newparam(:mailserver) do desc "The name of the host handling email for the list." end newparam(:admin) do desc "The email address of the administrator." end def generate if provider.respond_to?(:aliases) should = self.should(:ensure) || :present if should == :purged should = :absent end atype = Puppet::Type.type(:mailalias) provider.aliases. reject { |name,recipient| catalog.resource(:mailalias, name) }. collect { |name,recipient| atype.new(:name => name, :recipient => recipient, :ensure => should) } end end end end diff --git a/lib/puppet/type/mount.rb b/lib/puppet/type/mount.rb index 7dd91018b..b160776b7 100644 --- a/lib/puppet/type/mount.rb +++ b/lib/puppet/type/mount.rb @@ -1,287 +1,287 @@ require 'puppet/property/boolean' module Puppet # We want the mount to refresh when it changes. - newtype(:mount, :self_refresh => true) do + Type.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. **Refresh:** `mount` resources can respond to refresh events (via `notify`, `subscribe`, or the `~>` arrow). If a `mount` receives an event from another resource **and** its `ensure` attribute is set to `mounted`, Puppet will try to unmount then remount that filesystem. **Autorequires:** If Puppet is managing any parents of a mount resource --- that is, other mount points higher up in the filesystem --- the child mount will autorequire them." 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 `unmounted` 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." # IS -> SHOULD In Sync Action # ghost -> present NO create # absent -> present NO create # (mounted -> present YES) # (unmounted -> present YES) newvalue(:defined) do provider.create return :mount_created end aliasvalue :present, :defined # IS -> SHOULD In Sync Action # ghost -> unmounted NO create, unmount # absent -> unmounted NO create # mounted -> unmounted NO unmount newvalue(:unmounted) do case self.retrieve when :ghost # (not in fstab but mounted) provider.create @resource.flush provider.unmount return :mount_unmounted when nil, :absent # (not in fstab and not mounted) provider.create return :mount_created when :mounted # (in fstab and mounted) provider.unmount syncothers # I guess it's more likely that the mount was originally mounted with # the wrong attributes so I sync AFTER the umount return :mount_unmounted else raise Puppet::Error, "Unexpected change from #{current_value} to unmounted}" end end # IS -> SHOULD In Sync Action # ghost -> absent NO unmount # mounted -> absent NO provider.destroy AND unmount # unmounted -> absent NO provider.destroy newvalue(:absent, :event => :mount_deleted) do current_value = self.retrieve provider.unmount if provider.mounted? provider.destroy unless current_value == :ghost end # IS -> SHOULD In Sync Action # ghost -> mounted NO provider.create # absent -> mounted NO provider.create AND mount # unmounted -> mounted NO mount newvalue(:mounted, :event => :mount_mounted) do # Create the mount point if it does not already exist. current_value = self.retrieve currently_mounted = provider.mounted? provider.create if [nil, :absent, :ghost].include?(current_value) syncothers # The fs can be already mounted if it was absent but mounted provider.property_hash[:needs_mount] = true unless currently_mounted end # insync: mounted -> present # unmounted -> present def insync?(is) if should == :defined and [:mounted,:unmounted].include?(is) true else super 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.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." validate do |value| raise Puppet::Error, "device must not contain whitespace: #{value}" if value =~ /\s/ end 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.value(:osfamily) == "Solaris" if device = resource[:device] and device =~ %r{/dsk/} device.sub(%r{/dsk/}, "/rdsk/") elsif fstype = resource[:fstype] and fstype == 'nfs' '-' else nil end else nil end end validate do |value| raise Puppet::Error, "blockdevice must not contain whitespace: #{value}" if value =~ /\s/ end end newproperty(:fstype) do desc "The mount type. Valid values depend on the operating system. This is a required option." validate do |value| raise Puppet::Error, "fstype must not contain whitespace: #{value}" if value =~ /\s/ end end newproperty(:options) do desc "A single string containing options for the mount, as they would appear in fstab. For many platforms this is a comma delimited string. Consult the fstab(5) man page for system-specific details." validate do |value| raise Puppet::Error, "options must not contain whitespace: #{value}" if value =~ /\s/ end end newproperty(:pass) do desc "The pass in which the mount is checked." defaultto { if @resource.managed? if Facter.value(:osfamily) == 'Solaris' '-' else 0 end end } end newproperty(:atboot, :parent => Puppet::Property::Boolean) do desc "Whether to mount the mount at boot. Not all platforms support this." def munge(value) munged = super if munged :yes else :no end end 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.value(:operatingsystem) == "FreeBSD" newvalue(%r{(0|1|2)}) else newvalue(%r{(0|1)}) end 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 validate do |value| raise Puppet::Error, "name must not contain whitespace: #{value}" if value =~ /\s/ end munge do |value| value.gsub(/^(.+?)\/*$/, '\1') 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", "AIX", "DragonFly", "OpenBSD" 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 = name.intern if property = @parameters[name] return property.value end end # Ensure that mounts higher up in the filesystem are mounted first autorequire(:mount) do dependencies = [] Pathname.new(@parameters[:name].value).ascend do |parent| dependencies.unshift parent.to_s end dependencies[0..-2] end end end diff --git a/lib/puppet/type/notify.rb b/lib/puppet/type/notify.rb index 606df84cf..57c97084f 100644 --- a/lib/puppet/type/notify.rb +++ b/lib/puppet/type/notify.rb @@ -1,44 +1,44 @@ # # Simple module for logging messages on the client-side module Puppet - newtype(:notify) do + Type.newtype(:notify) do @doc = "Sends an arbitrary message to the agent run-time log." newproperty(:message) do desc "The message to be sent to the log." def sync case @resource["withpath"] when :true send(@resource[:loglevel], self.should) else Puppet.send(@resource[:loglevel], self.should) end return end def retrieve :absent end def insync?(is) false end defaultto { @resource[:name] } end newparam(:withpath) do desc "Whether to show the full object path. Defaults to false." defaultto :false newvalues(:true, :false) end newparam(:name) do desc "An arbitrary tag for your own reference; the name of the message." isnamevar end end end diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb index cfa630582..816c1a0a5 100644 --- a/lib/puppet/type/package.rb +++ b/lib/puppet/type/package.rb @@ -1,500 +1,500 @@ # 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. require 'puppet/parameter/package_options' require 'puppet/parameter/boolean' module Puppet - newtype(:package) do + Type.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. **Autorequires:** If Puppet is managing the files specified as a package's `adminfile`, `responsefile`, or `source`, the package resource will autorequire those files." feature :reinstallable, "The provider can reinstall packages.", :methods => [:reinstall] 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] feature :install_options, "The provider accepts options to be passed to the installer command." feature :uninstall_options, "The provider accepts options to be passed to the uninstaller command." feature :package_settings, "The provider accepts package_settings to be ensured for the given package. The meaning and format of these settings is provider-specific.", :methods => [:package_settings_insync?, :package_settings, :package_settings=] feature :virtual_packages, "The provider accepts virtual package names for install and uninstall." ensurable do desc <<-EOT What state the package should be in. On packaging systems that can retrieve new packages on their own, you can choose which package to retrieve by specifying a version number or `latest` as the ensure value. On packaging systems that manage configuration files separately from "normal" system files, you can uninstall config files by specifying `purged` as the ensure value. This defaults to `installed`. EOT 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 Puppet::Error, "Could not update: #{detail}", detail end if current == :absent :package_installed else :package_changed end end newvalue(/./, :required_features => :versionable) do begin provider.install rescue => detail self.fail Puppet::Error, "Could not update: #{detail}", 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) @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 when is.is_a?(Array) && is.include?(@latest) return true when is == @latest return true when is == :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 # this handles version number matches and # supports providers that can have multiple versions installed when *Array(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 validate do |value| if !value.is_a?(String) raise ArgumentError, "Name must be a String not #{value.class}" end end end newproperty(:package_settings, :required_features=>:package_settings) do desc "Settings that can change the contents or configuration of a package. The formatting and effects of package_settings are provider-specific; any provider that implements them must explain how to use them in its documentation. (Our general expectation is that if a package is installed but its settings are out of sync, the provider should re-install that package with the desired settings.) An example of how package_settings could be used is FreeBSD's port build options --- a future version of the provider could accept a hash of options, and would reinstall the port if the installed version lacked the correct settings. package { 'www/apache22': package_settings => { 'SUEXEC' => false } } Again, check the documentation of your platform's package provider to see the actual usage." validate do |value| if provider.respond_to?(:package_settings_validate) provider.package_settings_validate(value) else super(value) end end munge do |value| if provider.respond_to?(:package_settings_munge) provider.package_settings_munge(value) else super(value) end end def insync?(is) provider.package_settings_insync?(should, is) end def should_to_s(newvalue) if provider.respond_to?(:package_settings_should_to_s) provider.package_settings_should_to_s(should, newvalue) else super(newvalue) end end def is_to_s(currentvalue) if provider.respond_to?(:package_settings_is_to_s) provider.package_settings_is_to_s(should, currentvalue) else super(currentvalue) end end def change_to_s(currentvalue, newvalue) if provider.respond_to?(:package_settings_change_to_s) provider.package_settings_change_to_s(currentvalue, newvalue) else super(currentvalue,newvalue) end end 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, although you can manage packages as `file` resources." validate do |value| provider.validate_source(value) end 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(: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. Defaults to `keep`." 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 "OpenBSD supports 'flavors', which are further specifications for which type of package you want." end newparam(:install_options, :parent => Puppet::Parameter::PackageOptions, :required_features => :install_options) do desc <<-EOT An array of additional options to pass when installing a package. These options are package-specific, and should be documented by the software vendor. One commonly implemented option is `INSTALLDIR`: package { 'mysql': ensure => installed, source => 'N:/packages/mysql-5.5.16-winx64.msi', install_options => [ '/S', { 'INSTALLDIR' => 'C:\\mysql-5.5' } ], } Each option in the array can either be a string or a hash, where each key and value pair are interpreted in a provider specific way. Each option will automatically be quoted when passed to the install command. On Windows, this is the **only** place in Puppet where backslash separators should be used. Note that backslashes in double-quoted strings _must_ be double-escaped and backslashes in single-quoted strings _may_ be double-escaped. EOT end newparam(:uninstall_options, :parent => Puppet::Parameter::PackageOptions, :required_features => :uninstall_options) do desc <<-EOT An array of additional options to pass when uninstalling a package. These options are package-specific, and should be documented by the software vendor. For example: package { 'VMware Tools': ensure => absent, uninstall_options => [ { 'REMOVE' => 'Sync,VSS' } ], } Each option in the array can either be a string or a hash, where each key and value pair are interpreted in a provider specific way. Each option will automatically be quoted when passed to the uninstall command. On Windows, this is the **only** place in Puppet where backslash separators should be used. Note that backslashes in double-quoted strings _must_ be double-escaped and backslashes in single-quoted strings _may_ be double-escaped. EOT end newparam(:allow_virtual, :boolean => true, :parent => Puppet::Parameter::Boolean, :required_features => :virtual_packages) do desc 'Specifies if virtual package names are allowed for install and uninstall.' defaultto true end autorequire(:file) do autos = [] [:responsefile, :adminfile].each { |param| if val = self[param] autos << val end } if source = self[:source] and absolute_path?(source) autos << source 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 def present?(current_values) super && current_values[:ensure] != :purged end # This parameter exists to ensure backwards compatibility is preserved. # See https://github.com/puppetlabs/puppet/pull/2614 for discussion. # If/when a metaparameter for controlling how arbitrary resources respond # to refreshing is created, that will supersede this, and this will be # deprecated. newparam(:reinstall_on_refresh) do desc "Whether this resource should respond to refresh events (via `subscribe`, `notify`, or the `~>` arrow) by reinstalling the package. Only works for providers that support the `reinstallable` feature. This is useful for source-based distributions, where you may want to recompile a package if the build options change. If you use this, be careful of notifying classes when you want to restart services. If the class also contains a refreshable package, doing so could cause unnecessary re-installs. Defaults to `false`." newvalues(:true, :false) defaultto :false end # When a refresh event is triggered, calls reinstall on providers # that support the reinstall_on_refresh parameter. def refresh if provider.reinstallable? && @parameters[:reinstall_on_refresh].value == :true && @parameters[:ensure].value != :purged && @parameters[:ensure].value != :absent && @parameters[:ensure].value != :held provider.reinstall end end end end diff --git a/lib/puppet/type/router.rb b/lib/puppet/type/router.rb index 06be96054..1ee666b12 100644 --- a/lib/puppet/type/router.rb +++ b/lib/puppet/type/router.rb @@ -1,17 +1,17 @@ # # Manage a router abstraction module Puppet - newtype(:router) do + Type.newtype(:router) do @doc = "Manages connected router." newparam(:url) do desc <<-EOT An SSH or telnet URL at which to access the router, in the form `ssh://user:pass:enable@host/` or `telnet://user:pass:enable@host/`. EOT isnamevar end end end diff --git a/lib/puppet/type/schedule.rb b/lib/puppet/type/schedule.rb index f7c9b82df..1f98c9642 100644 --- a/lib/puppet/type/schedule.rb +++ b/lib/puppet/type/schedule.rb @@ -1,469 +1,469 @@ module Puppet - newtype(:schedule) do + Type.newtype(:schedule) do @doc = <<-'EOT' Define schedules for Puppet. Resources can be limited to a schedule by using the [`schedule`](http://docs.puppetlabs.com/references/latest/metaparameter.html#schedule) metaparameter. Currently, **schedules can only be used to stop a resource from being applied;** they cannot cause a resource to be applied when it otherwise wouldn't be, and they cannot accurately specify a time when a resource should run. Every time Puppet applies its configuration, it will apply the set of resources whose schedule does not eliminate them from running right then, but there is currently no system in place to guarantee that a given resource runs at a given time. If you specify a very restrictive schedule and Puppet happens to run at a time within that schedule, then the resources will get applied; otherwise, that work may never get done. Thus, it is advisable to use wider scheduling (e.g., over a couple of hours) combined with periods and repetitions. For instance, if you wanted to restrict certain resources to only running once, between the hours of two and 4 AM, then you would use this schedule: schedule { 'maint': range => "2 - 4", period => daily, repeat => 1, } With this schedule, the first time that Puppet runs between 2 and 4 AM, all resources with this schedule will get applied, but they won't get applied again between 2 and 4 because they will have already run once that day, and they won't get applied outside that schedule because they will be outside the scheduled range. Puppet automatically creates a schedule for each of the valid periods with the same name as that period (e.g., hourly and daily). Additionally, a schedule named `puppet` is created and used as the default, with the following attributes: schedule { 'puppet': period => hourly, repeat => 2, } This will cause resources to be applied every 30 minutes by default. EOT apply_to_all newparam(:name) do desc <<-EOT The name of the schedule. This name is used when assigning the schedule to a resource with the `schedule` metaparameter: schedule { 'everyday': period => daily, range => "2 - 4", } exec { "/usr/bin/apt-get update": schedule => 'everyday', } EOT isnamevar end newparam(:range) do desc <<-EOT The earliest and latest that a resource can be applied. This is always a hyphen-separated range within a 24 hour period, and hours must be specified in numbers between 0 and 23, inclusive. Minutes and seconds can optionally be provided, using the normal colon as a separator. For instance: schedule { 'maintenance': range => "1:30 - 4:30", } This is mostly useful for restricting certain resources to being applied in maintenance windows or during off-peak hours. Multiple ranges can be applied in array context. As a convenience when specifying ranges, you may cross midnight (e.g.: range => "22:00 - 04:00"). EOT # This is lame; properties all use arrays as values, but parameters don't. # That's going to hurt eventually. validate do |values| values = [values] unless values.is_a?(Array) values.each { |value| unless value.is_a?(String) and value =~ /\d+(:\d+){0,2}\s*-\s*\d+(:\d+){0,2}/ self.fail "Invalid range value '#{value}'" end } end munge do |values| values = [values] unless values.is_a?(Array) ret = [] values.each { |value| range = [] # Split each range value into a hour, minute, second triad value.split(/\s*-\s*/).each { |val| # Add the values as an array. range << val.split(":").collect { |n| n.to_i } } self.fail "Invalid range #{value}" if range.length != 2 # Make sure the hours are valid [range[0][0], range[1][0]].each do |n| raise ArgumentError, "Invalid hour '#{n}'" if n < 0 or n > 23 end [range[0][1], range[1][1]].each do |n| raise ArgumentError, "Invalid minute '#{n}'" if n and (n < 0 or n > 59) end ret << range } # Now our array of arrays ret end def match?(previous, now) # The lowest-level array is of the hour, minute, second triad # then it's an array of two of those, to present the limits # then it's an array of those ranges @value = [@value] unless @value[0][0].is_a?(Array) @value.each do |value| limits = value.collect do |range| ary = [now.year, now.month, now.day, range[0]] if range[1] ary << range[1] else ary << 0 end if range[2] ary << range[2] else ary << 0 end time = Time.local(*ary) unless time.hour == range[0] self.devfail( "Incorrectly converted time: #{time}: #{time.hour} vs #{range[0]}" ) end time end unless limits[0] < limits[1] self.info( "Assuming upper limit should be that time the next day" ) # Find midnight between the two days. Adding one second # to the end of the day is easier than dealing with dates. ary = limits[0].to_a ary[0] = 59 ary[1] = 59 ary[2] = 23 midnight = Time.local(*ary)+1 # If it is currently between the range start and midnight # we consider that a successful match. if now.between?(limits[0], midnight) # We have to check the weekday match here as it is special-cased # to support day-spanning ranges. if @resource[:weekday] return false unless @resource[:weekday].has_key?(now.wday) end return true end # If we didn't match between the starting time and midnight # we must now move our midnight back 24 hours and try # between the new midnight (24 hours prior) and the # ending time. midnight -= 86400 # Now we compare the current time between midnight and the # end time. if now.between?(midnight, limits[1]) # This case is the reason weekday matching is special cased # in the range parameter. If we match a range that has spanned # past midnight we want to match against the weekday when the range # started, not when it currently is. if @resource[:weekday] return false unless @resource[:weekday].has_key?((now - 86400).wday) end return true end # If neither of the above matched then we don't match the # range schedule. return false end # Check to see if a weekday parameter was specified and, if so, # do we match it or not. If we fail we can stop here. # This is required because spanning ranges forces us to check # weekday within the range parameter. if @resource[:weekday] return false unless @resource[:weekday].has_key?(now.wday) end return true if now.between?(*limits) end # Else, return false, since our current time isn't between # any valid times false end end newparam(:periodmatch) do desc "Whether periods should be matched by number (e.g., the two times are in the same hour) or by distance (e.g., the two times are 60 minutes apart)." newvalues(:number, :distance) defaultto :distance end newparam(:period) do desc <<-EOT The period of repetition for resources on this schedule. The default is for resources to get applied every time Puppet runs. Note that the period defines how often a given resource will get applied but not when; if you would like to restrict the hours that a given resource can be applied (e.g., only at night during a maintenance window), then use the `range` attribute. If the provided periods are not sufficient, you can provide a value to the *repeat* attribute, which will cause Puppet to schedule the affected resources evenly in the period the specified number of times. Take this schedule: schedule { 'veryoften': period => hourly, repeat => 6, } This can cause Puppet to apply that resource up to every 10 minutes. At the moment, Puppet cannot guarantee that level of repetition; that is, the resource can applied _up to_ every 10 minutes, but internal factors might prevent it from actually running that often (e.g. if a Puppet run is still in progress when the next run is scheduled to start, that next run will be suppressed). See the `periodmatch` attribute for tuning whether to match times by their distance apart or by their specific value. EOT newvalues(:hourly, :daily, :weekly, :monthly, :never) ScheduleScales = { :hourly => 3600, :daily => 86400, :weekly => 604800, :monthly => 2592000 } ScheduleMethods = { :hourly => :hour, :daily => :day, :monthly => :month, :weekly => proc do |prev, now| # Run the resource if the previous day was after this weekday (e.g., prev is wed, current is tue) # or if it's been more than a week since we ran prev.wday > now.wday or (now - prev) > (24 * 3600 * 7) end } def match?(previous, now) return false if value == :never value = self.value case @resource[:periodmatch] when :number method = ScheduleMethods[value] if method.is_a?(Proc) return method.call(previous, now) else # We negate it, because if they're equal we don't run return now.send(method) != previous.send(method) end when :distance scale = ScheduleScales[value] # If the number of seconds between the two times is greater # than the unit of time, we match. We divide the scale # by the repeat, so that we'll repeat that often within # the scale. return (now.to_i - previous.to_i) >= (scale / @resource[:repeat]) end end end newparam(:repeat) do desc "How often a given resource may be applied in this schedule's `period`. Defaults to 1; must be an integer." defaultto 1 validate do |value| unless value.is_a?(Integer) or value =~ /^\d+$/ raise Puppet::Error, "Repeat must be a number" end # This implicitly assumes that 'periodmatch' is distance -- that # is, if there's no value, we assume it's a valid value. return unless @resource[:periodmatch] if value != 1 and @resource[:periodmatch] != :distance raise Puppet::Error, "Repeat must be 1 unless periodmatch is 'distance', not '#{@resource[:periodmatch]}'" end end munge do |value| value = Integer(value) unless value.is_a?(Integer) value end def match?(previous, now) true end end newparam(:weekday) do desc <<-EOT The days of the week in which the schedule should be valid. You may specify the full day name (Tuesday), the three character abbreviation (Tue), or a number corresponding to the day of the week where 0 is Sunday, 1 is Monday, etc. Multiple days can be specified as an array. If not specified, the day of the week will not be considered in the schedule. If you are also using a range match that spans across midnight then this parameter will match the day that it was at the start of the range, not necessarily the day that it is when it matches. For example, consider this schedule: schedule { 'maintenance_window': range => '22:00 - 04:00', weekday => 'Saturday', } This will match at 11 PM on Saturday and 2 AM on Sunday, but not at 2 AM on Saturday. EOT validate do |values| values = [values] unless values.is_a?(Array) values.each { |value| unless value.is_a?(String) and (value =~ /^[0-6]$/ or value =~ /^(Mon|Tues?|Wed(?:nes)?|Thu(?:rs)?|Fri|Sat(?:ur)?|Sun)(day)?$/i) raise ArgumentError, "%s is not a valid day of the week" % value end } end weekdays = { 'sun' => 0, 'mon' => 1, 'tue' => 2, 'wed' => 3, 'thu' => 4, 'fri' => 5, 'sat' => 6, } munge do |values| values = [values] unless values.is_a?(Array) ret = {} values.each { |value| if value =~ /^[0-6]$/ index = value.to_i else index = weekdays[value[0,3].downcase] end ret[index] = true } ret end def match?(previous, now) # Special case weekday matching with ranges to a no-op here. # If the ranges span days then we can't simply match the current # weekday, as we want to match the weekday as it was when the range # started. As a result, all of that logic is in range, not here. return true if @resource[:range] return true if value.has_key?(now.wday) false end end def self.instances [] end def self.mkdefaultschedules result = [] unless Puppet[:default_schedules] Puppet.debug "Not creating default schedules: default_schedules is false" return result end Puppet.debug "Creating default schedules" result << self.new( :name => "puppet", :period => :hourly, :repeat => "2" ) # And then one for every period @parameters.find { |p| p.name == :period }.value_collection.values.each { |value| result << self.new( :name => value.to_s, :period => value ) } result end def match?(previous = nil, now = nil) # If we've got a value, then convert it to a Time instance previous &&= Time.at(previous) now ||= Time.now # Pull them in order self.class.allattrs.each { |param| if @parameters.include?(param) and @parameters[param].respond_to?(:match?) return false unless @parameters[param].match?(previous, now) end } # If we haven't returned false, then return true; in other words, # any provided schedules need to all match true end end end diff --git a/lib/puppet/type/selboolean.rb b/lib/puppet/type/selboolean.rb index c02e7e1b9..16f933c43 100644 --- a/lib/puppet/type/selboolean.rb +++ b/lib/puppet/type/selboolean.rb @@ -1,26 +1,26 @@ module Puppet - newtype(:selboolean) do + Type.newtype(:selboolean) do @doc = "Manages SELinux booleans on systems with SELinux support. The supported booleans are any of the ones found in `/selinux/booleans/`." newparam(:name) do desc "The name of the SELinux boolean to be managed." isnamevar end newproperty(:value) do desc "Whether the SELinux boolean should be enabled or disabled." newvalue(:on) newvalue(:off) end newparam(:persistent) do desc "If set true, SELinux booleans will be written to disk and persist across reboots. The default is `false`." defaultto :false newvalues(:true, :false) end end end diff --git a/lib/puppet/type/service.rb b/lib/puppet/type/service.rb index dc54f1d61..e6b1e4d9b 100644 --- a/lib/puppet/type/service.rb +++ b/lib/puppet/type/service.rb @@ -1,234 +1,234 @@ # 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 + Type.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 is usually capable of doing the right thing, but the more information you can provide, the better behaviour you will get. Puppet 2.7 and newer expect init scripts to have a working status command. If this isn't the case for any of your services' init scripts, you will need to set `hasstatus` to false and possibly specify a custom status command in the `status` attribute. As a last resort, Puppet will attempt to search the process table by calling whatever command is listed in the `ps` fact. The default search pattern is the name of the service, but you can specify it with the `pattern` attribute. **Refresh:** `service` resources can respond to refresh events (via `notify`, `subscribe`, or the `~>` arrow). If a `service` receives an event from another resource, Puppet will restart the service it manages. The actual command used to restart the service depends on the platform and can be configured: * If you set `hasrestart` to true, Puppet will use the init script's restart command. * You can provide an explicit command for restarting with the `restart` attribute. * If you do neither, the service's stop and start commands will be used." 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." feature :flaggable, "The provider can pass flags to the service." 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 newvalue(:manual, :event => :service_manual_start) do provider.manual_start end def retrieve provider.enabled? end validate do |value| if value == :manual and !Puppet.features.microsoft_windows? raise Puppet::Error.new("Setting enable to manual is only supported on Microsoft Windows.") end 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, :invalidate_refreshes => true) 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.safe_insync?(val) end event end end newproperty(:flags, :required_features => :flaggable) do desc "Specify a string of flags to pass to the startup script." 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 whether the service's init script has a functional status command; defaults to `true`. This attribute's default value changed in Puppet 2.7.0. The init script's status command must return 0 if the service is running and a nonzero value otherwise. Ideally, these exit codes should conform to [the LSB's specification][lsb-exit-codes] for init script status actions, but Puppet only considers the difference between 0 and nonzero to be relevant. If a service's init script does not support any kind of status command, you should set `hasstatus` to false and either provide a specific command using the `status` attribute or expect that Puppet will look for the service name in the process table. Be aware that 'virtual' init scripts (like 'network' under Red Hat systems) will respond poorly to refresh events from other resources if you override the default behavior without providing a status command." newvalues(:true, :false) defaultto :true end newparam(:name) do desc <<-EOT The name of the service to run. This name is used to find the service; on platforms where services have short system names and long display names, this should be the short name. (To take an example from Windows, you would use "wuauserv" rather than "Automatic Updates.") EOT 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) value.flatten.collect { |p| p.split(File::PATH_SEPARATOR) }.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. Defaults to the name of the service. The pattern can be a simple string or any legal Ruby pattern, including regular expressions (which should be quoted without enclosing slashes)." 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. This command must return 0 if the service is running and a nonzero value otherwise. Ideally, these exit codes should conform to [the LSB's specification][lsb-exit-codes] for init script status actions, but Puppet only considers the difference between 0 and nonzero to be relevant. If left unspecified, the status of the service will be determined automatically, usually by looking for the service in the process table. [lsb-exit-codes]: http://refspecs.linuxfoundation.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html" 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` command. If this is false and you do not specify a command in the `restart` attribute, the init script's `stop` and `start` commands will be used. Defaults to false." 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/ssh_authorized_key.rb b/lib/puppet/type/ssh_authorized_key.rb index 8b40907f3..8b8a251db 100644 --- a/lib/puppet/type/ssh_authorized_key.rb +++ b/lib/puppet/type/ssh_authorized_key.rb @@ -1,153 +1,153 @@ module Puppet - newtype(:ssh_authorized_key) do + Type.newtype(:ssh_authorized_key) do @doc = "Manages SSH authorized keys. Currently only type 2 keys are supported. In their native habitat, SSH keys usually appear as a single long line. This resource type requires you to split that line into several attributes. Thus, a key that appears in your `~/.ssh/id_rsa.pub` file like this... ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAy5mtOAMHwA2ZAIfW6Ap70r+I4EclYHEec5xIN59ROUjss23Skb1OtjzYpVPaPH8mSdSmsN0JHaBLiRcu7stl4O8D8zA4mz/vw32yyQ/Kqaxw8l0K76k6t2hKOGqLTY4aFbFISV6GDh7MYLn8KU7cGp96J+caO5R5TqtsStytsUhSyqH+iIDh4e4+BrwTc6V4Y0hgFxaZV5d18mLA4EPYKeG5+zyBCVu+jueYwFqM55E0tHbfiaIN9IzdLV+7NEEfdLkp6w2baLKPqWUBmuvPF1Mn3FwaFLjVsMT3GQeMue6b3FtUdTDeyAYoTxrsRo/WnDkS6Pa3YhrFwjtUqXfdaQ== nick@magpie.puppetlabs.lan ...would translate to the following resource: ssh_authorized_key { 'nick@magpie.puppetlabs.lan': user => 'nick', type => 'ssh-rsa', key => 'AAAAB3NzaC1yc2EAAAABIwAAAQEAy5mtOAMHwA2ZAIfW6Ap70r+I4EclYHEec5xIN59ROUjss23Skb1OtjzYpVPaPH8mSdSmsN0JHaBLiRcu7stl4O8D8zA4mz/vw32yyQ/Kqaxw8l0K76k6t2hKOGqLTY4aFbFISV6GDh7MYLn8KU7cGp96J+caO5R5TqtsStytsUhSyqH+iIDh4e4+BrwTc6V4Y0hgFxaZV5d18mLA4EPYKeG5+zyBCVu+jueYwFqM55E0tHbfiaIN9IzdLV+7NEEfdLkp6w2baLKPqWUBmuvPF1Mn3FwaFLjVsMT3GQeMue6b3FtUdTDeyAYoTxrsRo/WnDkS6Pa3YhrFwjtUqXfdaQ==', } To ensure that only the currently approved keys are present, you can purge unmanaged SSH keys on a per-user basis. Do this with the `user` resource type's `purge_ssh_keys` attribute: user { 'nick': ensure => present, purge_ssh_keys => true, } This will remove any keys in `~/.ssh/authorized_keys` that aren't being managed with `ssh_authorized_key` resources. See the documentation of the `user` type for more details. **Autorequires:** If Puppet is managing the user account in which this SSH key should be installed, the `ssh_authorized_key` resource will autorequire that user." ensurable newparam(:name) do desc "The SSH key comment. This attribute is currently used as a system-wide primary key and therefore has to be unique." isnamevar end newproperty(:type) do desc "The encryption type used." newvalues :'ssh-dss', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521', :'ssh-ed25519' aliasvalue(:dsa, :'ssh-dss') aliasvalue(:ed25519, :'ssh-ed25519') aliasvalue(:rsa, :'ssh-rsa') end newproperty(:key) do desc "The public key itself; generally a long string of hex characters. The `key` attribute may not contain whitespace. Make sure to omit the following in this attribute (and specify them in other attributes): * Key headers (e.g. 'ssh-rsa') --- put these in the `type` attribute. * Key identifiers / comments (e.g. 'joe@joescomputer.local') --- put these in the `name` attribute/resource title." validate do |value| raise Puppet::Error, "Key must not contain whitespace: #{value}" if value =~ /\s/ end end newproperty(:user) do desc "The user account in which the SSH key should be installed. The resource will autorequire this user if it is being managed as a `user` resource." end newproperty(:target) do desc "The absolute filename in which to store the SSH key. This property is optional and should only be used in cases where keys are stored in a non-standard location (i.e.` not in `~user/.ssh/authorized_keys`)." defaultto :absent def should return super if defined?(@should) and @should[0] != :absent return nil unless user = resource[:user] begin return File.expand_path("~#{user}/.ssh/authorized_keys") rescue Puppet.debug "The required user is not yet present on the system" return nil end end def insync?(is) is == should end end newproperty(:options, :array_matching => :all) do desc "Key options; see sshd(8) for possible values. Multiple values should be specified as an array." defaultto do :absent end def is_to_s(value) if value == :absent or value.include?(:absent) super else value.join(",") end end def should_to_s(value) if value == :absent or value.include?(:absent) super else value.join(",") end end validate do |value| unless value == :absent or value =~ /^[-a-z0-9A-Z_]+(?:=\".*?\")?$/ raise Puppet::Error, "Option #{value} is not valid. A single option must either be of the form 'option' or 'option=\"value\". Multiple options must be provided as an array" end end end autorequire(:user) do should(:user) if should(:user) end validate do # Go ahead if target attribute is defined return if @parameters[:target].shouldorig[0] != :absent # Go ahead if user attribute is defined return if @parameters.include?(:user) # If neither target nor user is defined, this is an error raise Puppet::Error, "Attribute 'user' or 'target' is mandatory" end # regular expression suitable for use by a ParsedFile based provider REGEX = /^(?:(.+) )?(ssh-dss|ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521) ([^ ]+) ?(.*)$/ def self.keyline_regex REGEX end end end diff --git a/lib/puppet/type/sshkey.rb b/lib/puppet/type/sshkey.rb index 2a3d5fedf..5ad63cd6c 100644 --- a/lib/puppet/type/sshkey.rb +++ b/lib/puppet/type/sshkey.rb @@ -1,73 +1,73 @@ module Puppet - newtype(:sshkey) do + Type.newtype(:sshkey) do @doc = "Installs and manages ssh host keys. At this point, this type only knows how to install keys into `/etc/ssh/ssh_known_hosts`. See the `ssh_authorized_key` type to manage authorized keys." ensurable newproperty(:type) do desc "The encryption type used. Probably ssh-dss or ssh-rsa." newvalues :'ssh-dss', :'ssh-ed25519', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521' aliasvalue(:dsa, :'ssh-dss') aliasvalue(:ed25519, :'ssh-ed25519') aliasvalue(:rsa, :'ssh-rsa') end newproperty(:key) do desc "The key itself; generally a long string of uuencoded characters." end # FIXME This should automagically check for aliases to the hosts, just # to see if we can automatically glean any aliases. newproperty(:host_aliases) do desc 'Any aliases the host might have. Multiple values must be specified as an array.' attr_accessor :meta def insync?(is) is == @should end # We actually want to return the whole array here, not just the first # value. def should defined?(@should) ? @should : nil end validate do |value| if value =~ /\s/ raise Puppet::Error, "Aliases cannot include whitespace" end if value =~ /,/ raise Puppet::Error, "Aliases must be provided as an array, not a comma-separated list" end end end newparam(:name) do desc "The host name that the key is associated with." isnamevar validate do |value| raise Puppet::Error, "Resourcename cannot include whitespaces" if value =~ /\s/ raise Puppet::Error, "No comma in resourcename allowed. If you want to specify aliases use the host_aliases property" if value.include?(',') end end newproperty(:target) do desc "The file in which to store the ssh key. Only used by the `parsed` provider." defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) @resource.class.defaultprovider.default_target else nil end } end end end diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index 03ef67659..72536243f 100644 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -1,698 +1,698 @@ require 'etc' require 'facter' require 'puppet/parameter/boolean' require 'puppet/property/list' require 'puppet/property/ordered_list' require 'puppet/property/keyvalue' module Puppet - newtype(:user) do + Type.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. **Autorequires:** If Puppet is managing the user's primary group (as provided in the `gid` attribute), the user resource will autorequire that group. If Puppet is managing any role accounts corresponding to the user's roles, the user resource will autorequire those role accounts." 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_password_salt, "The provider can set a password salt. This is for providers that implement PBKDF2 passwords with salt properties." 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." feature :system_users, "The provider allows you to create system users with lower UIDs." feature :manages_aix_lam, "The provider can manage AIX Loadable Authentication Module (LAM) system." feature :libuser, "Allows local users to be managed on systems that also use some other remote NSS method of managing accounts." feature :manages_shell, "The provider allows for setting shell and validates if possible" 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. If no user ID is specified when creating a new user, then one will be chosen automatically. This will likely result in the same user having different UIDs on different systems, which is not recommended. This is especially noteworthy when managing the same user on both Darwin and other platforms, since Puppet does UID generation on Darwin, but the underlying tools do so on other platforms. On Windows, this property is read-only and will return the user's security identifier (SID)." 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. This attribute is not supported on Windows systems; use the `groups` attribute instead. (On Windows, designating a primary group is only meaningful for domain accounts, which Puppet does not currently manage.)" munge do |value| if value.is_a?(String) and value =~ /^[-0-9]+$/ Integer(value) else value end end def insync?(is) # 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 the user's full name." munge do |v| v.respond_to?(:force_encoding) ? v.force_encoding(Encoding::ASCII_8BIT) : v end end newproperty(:shell, :required_features => :manages_shell) do desc "The user's login shell. The shell must exist and be executable. This attribute cannot be managed on Windows systems." end newproperty(:password, :required_features => :manages_passwords) do desc %q{The user's password, in whatever encrypted format the local system requires. * Most modern Unix-like systems use salted SHA1 password hashes. You can use Puppet's built-in `sha1` function to generate a hash from a password. * Mac OS X 10.5 and 10.6 also use salted SHA1 hashes. * Mac OS X 10.7 (Lion) uses salted SHA512 hashes. The Puppet Labs [stdlib][] module contains a `str2saltedsha512` function which can generate password hashes for Lion. * Mac OS X 10.8 and higher use salted SHA512 PBKDF2 hashes. When managing passwords on these systems the salt and iterations properties need to be specified as well as the password. * Windows passwords can only be managed in cleartext, as there is no Windows API for setting the password hash. [stdlib]: https://github.com/puppetlabs/puppetlabs-stdlib/ Be sure to enclose any value that includes a dollar sign ($) in single quotes (') to avoid accidental variable interpolation.} 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 def is_to_s( currentvalue ) return '[old password hash redacted]' end def should_to_s( newvalue ) return '[new password hash redacted]' end end newproperty(:password_min_age, :required_features => :manages_password_age) do desc "The minimum number of 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+$/ 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 number of 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+$/ raise ArgumentError, "Password maximum age must be provided as a number." end end end newproperty(:groups, :parent => Puppet::Property::List) do desc "The groups to which the user belongs. The primary group should not be listed, and groups should be identified by name rather than by GID. Multiple groups should be specified as an array." validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Group names must be provided, not GID numbers." end raise ArgumentError, "Group names must be provided as an array, not a comma-separated list." if value.include?(",") raise ArgumentError, "Group names must not be empty. If you want to specify \"no groups\" pass an empty array" if value.empty? end end newparam(:name) do desc "The user name. While naming limitations vary by operating system, it is advisable to restrict names to the lowest common denominator, which is a maximum of 8 characters beginning with a letter. Note that Puppet considers user names to be case-sensitive, regardless of the platform's own rules; be sure to always use the same case when referring to a given user." isnamevar end newparam(:membership) do desc "Whether specified groups should be considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of groups to which the user belongs. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newparam(:system, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether the user is a system user, according to the OS's criteria; on most platforms, a UID less than or equal to 500 indicates a system user. This parameter is only used when the resource is created and will not affect the UID when the user is present. Defaults to `false`." defaultto false end newparam(:allowdupe, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether to allow duplicate UIDs. Defaults to `false`." defaultto false end newparam(:managehome, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether to manage the home directory when managing the user. This will create the home directory when `ensure => present`, and delete the home directory when `ensure => absent`. Defaults to `false`." defaultto false validate do |val| if munge(val) raise ArgumentError, "User provider #{provider.class.name} can not manage home directories" if provider and not 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. If you want to ensure the user account never expires, you can pass the special value `absent`." newvalues :absent newvalues /^\d{4}-\d{2}-\d{2}$/ validate do |value| if value.intern != :absent and value !~ /^\d{4}-\d{2}-\d{2}$/ raise ArgumentError, "Expiry dates must be YYYY-MM-DD or the string \"absent\"" 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 # This method has been exposed for puppet to manage users and groups of # files in its settings and should not be considered available outside of # puppet. # # (see Puppet::Settings#service_user_available?) # # @return [Boolean] if the user exists on the system # @api private 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 considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of roles the user has. Defaults to `minimum`." 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 considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of auths the user has. Defaults to `minimum`." 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 **complete list** (`inclusive`) or the **minimum list** (`minimum`) of roles of which the user is a member. Defaults to `minimum`." 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 key = value pairs." def membership :key_membership end validate do |value| raise ArgumentError, "Key/value pairs must be separated by an =" unless value.include?("=") end end newparam(:key_membership) do desc "Whether specified key/value pairs should be considered the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of the user's attributes. Defaults to `minimum`." 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 newparam(:ia_load_module, :required_features => :manages_aix_lam) do desc "The name of the I&A module to use to manage this user." end newproperty(:attributes, :parent => Puppet::Property::KeyValue, :required_features => :manages_aix_lam) do desc "Specify AIX attributes for the user in an array of attribute = value pairs." def membership :attribute_membership end def delimiter " " end validate do |value| raise ArgumentError, "Attributes value pairs must be separated by an =" unless value.include?("=") end end newparam(:attribute_membership) do desc "Whether specified attribute value pairs should be treated as the **complete list** (`inclusive`) or the **minimum list** (`minimum`) of attribute/value pairs for the user. Defaults to `minimum`." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:salt, :required_features => :manages_password_salt) do desc "This is the 32 byte salt used to generate the PBKDF2 password used in OS X. This field is required for managing passwords on OS X >= 10.8." end newproperty(:iterations, :required_features => :manages_password_salt) do desc "This is the number of iterations of a chained computation of the password hash (http://en.wikipedia.org/wiki/PBKDF2). This parameter is used in OS X. This field is required for managing passwords on OS X >= 10.8." munge do |value| if value.is_a?(String) and value =~/^[-0-9]+$/ Integer(value) else value end end end newparam(:forcelocal, :boolean => true, :required_features => :libuser, :parent => Puppet::Parameter::Boolean) do desc "Forces the management of local accounts when accounts are also being managed by some other NSS" defaultto false end def generate return [] if self[:purge_ssh_keys].empty? find_unmanaged_keys end newparam(:purge_ssh_keys) do desc "Whether to purge authorized SSH keys for this user if they are not managed with the `ssh_authorized_key` resource type. Allowed values are: * `false` (default) --- don't purge SSH keys for this user. * `true` --- look for keys in the `.ssh/authorized_keys` file in the user's home directory. Purge any keys that aren't managed as `ssh_authorized_key` resources. * An array of file paths --- look for keys in all of the files listed. Purge any keys that aren't managed as `ssh_authorized_key` resources. If any of these paths starts with `~` or `%h`, that token will be replaced with the user's home directory." defaultto :false # Use Symbols instead of booleans until PUP-1967 is resolved. newvalues(:true, :false) validate do |value| if [ :true, :false ].include? value.to_s.intern return end value = [ value ] if value.is_a?(String) if value.is_a?(Array) value.each do |entry| raise ArgumentError, "Each entry for purge_ssh_keys must be a string, not a #{entry.class}" unless entry.is_a?(String) valid_home = Puppet::Util.absolute_path?(entry) || entry =~ %r{^~/|^%h/} raise ArgumentError, "Paths to keyfiles must be absolute, not #{entry}" unless valid_home end return end raise ArgumentError, "purge_ssh_keys must be true, false, or an array of file names, not #{value.inspect}" end munge do |value| # Resolve string, boolean and symbol forms of true and false to a # single representation. test_sym = value.to_s.intern value = test_sym if [:true, :false].include? test_sym return [] if value == :false home = resource[:home] if value == :true and not home raise ArgumentError, "purge_ssh_keys can only be true for users with a defined home directory" end return [ "#{home}/.ssh/authorized_keys" ] if value == :true # value is an array - munge each value [ value ].flatten.map do |entry| if entry =~ /^~|^%h/ and not home raise ArgumentError, "purge_ssh_keys value '#{value}' meta character ~ or %h only allowed for users with a defined home directory" end entry.gsub!(/^~\//, "#{home}/") entry.gsub!(/^%h\//, "#{home}/") entry end end end # Generate ssh_authorized_keys resources for purging. The key files are # taken from the purge_ssh_keys parameter. The generated resources inherit # all metaparameters from the parent user resource. # # @return [Array keyname, :target => keyfile) }.reject { |res| catalog.resource_refs.include? res.ref } end end end diff --git a/lib/puppet/type/zfs.rb b/lib/puppet/type/zfs.rb index f4faa38f3..14629fb2f 100644 --- a/lib/puppet/type/zfs.rb +++ b/lib/puppet/type/zfs.rb @@ -1,150 +1,150 @@ module Puppet - newtype(:zfs) do + Type.newtype(:zfs) do @doc = "Manage zfs. Create destroy and set properties on zfs instances. **Autorequires:** If Puppet is managing the zpool at the root of this zfs instance, the zfs resource will autorequire it. If Puppet is managing any parent zfs instances, the zfs resource will autorequire them." ensurable newparam(:name) do desc "The full name for this filesystem (including the zpool)." end newproperty(:aclinherit) do desc "The aclinherit property. Valid values are `discard`, `noallow`, `restricted`, `passthrough`, `passthrough-x`." end newproperty(:aclmode) do desc "The aclmode property. Valid values are `discard`, `groupmask`, `passthrough`." end newproperty(:atime) do desc "The atime property. Valid values are `on`, `off`." end newproperty(:canmount) do desc "The canmount property. Valid values are `on`, `off`, `noauto`." end newproperty(:checksum) do desc "The checksum property. Valid values are `on`, `off`, `fletcher2`, `fletcher4`, `sha256`." end newproperty(:compression) do desc "The compression property. Valid values are `on`, `off`, `lzjb`, `gzip`, `gzip-[1-9]`, `zle`." end newproperty(:copies) do desc "The copies property. Valid values are `1`, `2`, `3`." end newproperty(:dedup) do desc "The dedup property. Valid values are `on`, `off`." end newproperty(:devices) do desc "The devices property. Valid values are `on`, `off`." end newproperty(:exec) do desc "The exec property. Valid values are `on`, `off`." end newproperty(:logbias) do desc "The logbias property. Valid values are `latency`, `throughput`." end newproperty(:mountpoint) do desc "The mountpoint property. Valid values are ``, `legacy`, `none`." end newproperty(:nbmand) do desc "The nbmand property. Valid values are `on`, `off`." end newproperty(:primarycache) do desc "The primarycache property. Valid values are `all`, `none`, `metadata`." end newproperty(:quota) do desc "The quota property. Valid values are ``, `none`." end newproperty(:readonly) do desc "The readonly property. Valid values are `on`, `off`." end newproperty(:recordsize) do desc "The recordsize property. Valid values are powers of two between 512 and 128k." end newproperty(:refquota) do desc "The refquota property. Valid values are ``, `none`." end newproperty(:refreservation) do desc "The refreservation property. Valid values are ``, `none`." end newproperty(:reservation) do desc "The reservation property. Valid values are ``, `none`." end newproperty(:secondarycache) do desc "The secondarycache property. Valid values are `all`, `none`, `metadata`." end newproperty(:setuid) do desc "The setuid property. Valid values are `on`, `off`." end newproperty(:shareiscsi) do desc "The shareiscsi property. Valid values are `on`, `off`, `type=`." end newproperty(:sharenfs) do desc "The sharenfs property. Valid values are `on`, `off`, share(1M) options" end newproperty(:sharesmb) do desc "The sharesmb property. Valid values are `on`, `off`, sharemgr(1M) options" end newproperty(:snapdir) do desc "The snapdir property. Valid values are `hidden`, `visible`." end newproperty(:version) do desc "The version property. Valid values are `1`, `2`, `3`, `4`, `current`." end newproperty(:volsize) do desc "The volsize property. Valid values are ``" end newproperty(:vscan) do desc "The vscan property. Valid values are `on`, `off`." end newproperty(:xattr) do desc "The xattr property. Valid values are `on`, `off`." end newproperty(:zoned) do desc "The zoned property. Valid values are `on`, `off`." end autorequire(:zpool) do #strip the zpool off the zfs name and autorequire it [@parameters[:name].value.split('/')[0]] end autorequire(:zfs) do #slice and dice, we want all the zfs before this one names = @parameters[:name].value.split('/') names.slice(1..-2).inject([]) { |a,v| a << "#{a.last}/#{v}" }.collect { |fs| names[0] + fs } end end end diff --git a/lib/puppet/type/zpool.rb b/lib/puppet/type/zpool.rb index 043deecc4..d2c7d511f 100644 --- a/lib/puppet/type/zpool.rb +++ b/lib/puppet/type/zpool.rb @@ -1,91 +1,91 @@ module Puppet class Property class VDev < Property def flatten_and_sort(array) array = [array] unless array.is_a? Array array.collect { |a| a.split(' ') }.flatten.sort end def insync?(is) return @should == [:absent] if is == :absent flatten_and_sort(is) == flatten_and_sort(@should) end end class MultiVDev < VDev def insync?(is) 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 + Type.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 a 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. This type does not currently support mirroring of log disks." end newparam(:pool) do desc "The name for this pool." isnamevar end newparam(:raid_parity) do desc "Determines parity when using the `raidz` parameter." 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/spec/unit/parameter/path_spec.rb b/spec/unit/parameter/path_spec.rb index ae9de5e2d..0202dcb57 100755 --- a/spec/unit/parameter/path_spec.rb +++ b/spec/unit/parameter/path_spec.rb @@ -1,24 +1,24 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parameter/path' [false, true].each do |arrays| describe "Puppet::Parameter::Path with arrays #{arrays}" do it_should_behave_like "all path parameters", :path, :array => arrays do # The new type allows us a test that is guaranteed to go direct to our # validation code, without passing through any "real type" overrides or # whatever on the way. - Puppet::newtype(:test_puppet_parameter_path) do + Puppet::Type.newtype(:test_puppet_parameter_path) do newparam(:path, :parent => Puppet::Parameter::Path, :arrays => arrays) do isnamevar accept_arrays arrays end end def instance(path) Puppet::Type.type(:test_puppet_parameter_path).new(:path => path) end end end end diff --git a/spec/unit/puppet_spec.rb b/spec/unit/puppet_spec.rb index fc70c7ac1..edca3af0a 100755 --- a/spec/unit/puppet_spec.rb +++ b/spec/unit/puppet_spec.rb @@ -1,41 +1,48 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet' require 'puppet_spec/files' require 'semver' describe Puppet do include PuppetSpec::Files context "#version" do it "should be valid semver" do SemVer.should be_valid Puppet.version end end Puppet::Util::Log.eachlevel do |level| it "should have a method for sending '#{level}' logs" do Puppet.should respond_to(level) end end it "should be able to change the path" do newpath = ENV["PATH"] + File::PATH_SEPARATOR + "/something/else" Puppet[:path] = newpath ENV["PATH"].should == newpath end it "should change $LOAD_PATH when :libdir changes" do one = tmpdir('load-path-one') two = tmpdir('load-path-two') one.should_not == two Puppet[:libdir] = one $LOAD_PATH.should include one $LOAD_PATH.should_not include two Puppet[:libdir] = two $LOAD_PATH.should_not include one $LOAD_PATH.should include two end + + context "newtype" do + it "should issue a deprecation warning" do + subject.expects(:deprecation_warning).with("Puppet.newtype is deprecated and will be removed in a future release. Use Puppet::Type.newtype instead.") + subject.newtype("sometype") + end + end end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index 997932460..d1a3215c8 100755 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -1,1007 +1,1007 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/resource' describe Puppet::Resource do include PuppetSpec::Files let(:basepath) { make_absolute("/somepath") } let(:environment) { Puppet::Node::Environment.create(:testing, []) } [: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 expect { Puppet::Resource.new }.to 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 not interpret the title as a reference if the type is a non component or whit reference" do ref = Puppet::Resource.new("Notify", "foo::bar[baz]") ref.type.should == "Notify" ref.title.should =="foo::bar[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 expect { Puppet::Resource.new("resource-spec-foo") }.to raise_error(ArgumentError) end it 'should fail if strict is set and type does not exist' do expect { Puppet::Resource.new('resource-spec-foo', 'title', {:strict=>true}) }.to raise_error(ArgumentError, 'Invalid resource type resource-spec-foo') end it 'should fail if strict is set and class does not exist' do expect { Puppet::Resource.new('Class', 'resource-spec-foo', {:strict=>true}) }.to raise_error(ArgumentError, 'Could not find declared class resource-spec-foo') end it "should fail if the title is a hash and the type is not a valid resource reference string" do expect { Puppet::Resource.new({:type => "resource-spec-foo", :title => "bar"}) }. to raise_error ArgumentError, /Puppet::Resource.new does not take a hash/ 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 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") environment.known_resource_types.add @type end it "should set its type to the capitalized type name" do Puppet::Resource.new("foo::bar", "/my/file", :environment => environment).type.should == "Foo::Bar" end it "should be able to find the resource type" do Puppet::Resource.new("foo::bar", "/my/file", :environment => environment).resource_type.should equal(@type) end it "should set its title to the provided title" do Puppet::Resource.new("foo::bar", "/my/file", :environment => environment).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") environment.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", :environment => environment).title.should == "Foo::Bar" end it "should be able to find the resource type" do Puppet::Resource.new("class", "foo::bar", :environment => environment).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, "") environment.known_resource_types.add @type Puppet::Resource.new("class", "", :environment => environment).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, "") environment.known_resource_types.add @type Puppet::Resource.new("class", :main, :environment => environment).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") environment.known_resource_types.add type resource = Puppet::Resource.new("foobar", "/my/file", :environment => environment) 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 setting default parameters" do let(:foo_node) { Puppet::Node.new('foo', :environment => environment) } let(:compiler) { Puppet::Parser::Compiler.new(foo_node) } let(:scope) { Puppet::Parser::Scope.new(compiler) } def ast_string(value) Puppet::Parser::AST::String.new({:value => value}) end it "should fail when asked to set default values and it is not a parser resource" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "default_param", :arguments => {"a" => ast_string("default")}) ) resource = Puppet::Resource.new("default_param", "name", :environment => environment) lambda { resource.set_default_parameters(scope) }.should raise_error(Puppet::DevError) end it "should evaluate and set any default values when no value is provided" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "default_param", :arguments => {"a" => ast_string("a_default_value")}) ) resource = Puppet::Parser::Resource.new("default_param", "name", :scope => scope) resource.set_default_parameters(scope) resource["a"].should == "a_default_value" end it "should skip attributes with no default value" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "no_default_param", :arguments => {"a" => ast_string("a_default_value")}) ) resource = Puppet::Parser::Resource.new("no_default_param", "name", :scope => scope) lambda { resource.set_default_parameters(scope) }.should_not raise_error end it "should return the list of default parameters set" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "default_param", :arguments => {"a" => ast_string("a_default_value")}) ) resource = Puppet::Parser::Resource.new("default_param", "name", :scope => scope) resource.set_default_parameters(scope).should == ["a"] end describe "when the resource type is :hostclass" do let(:environment_name) { "testing env" } let(:fact_values) { { :a => 1 } } let(:port) { Puppet::Parser::AST::String.new(:value => '80') } let(:apache) { Puppet::Resource::Type.new(:hostclass, 'apache', :arguments => { 'port' => port }) } before do environment.known_resource_types.add(apache) scope.stubs(:host).returns('host') scope.stubs(:environment).returns(environment) scope.stubs(:facts).returns(Puppet::Node::Facts.new("facts", fact_values)) end context "when no value is provided" do before(:each) do Puppet[:binder] = true end let(:resource) do Puppet::Parser::Resource.new("class", "apache", :scope => scope) end it "should query the data_binding terminus using a namespaced key" do Puppet::DataBinding.indirection.expects(:find).with( 'apache::port', all_of(has_key(:environment), has_key(:variables))) resource.set_default_parameters(scope) end it "should use the value from the data_binding terminus" do Puppet::DataBinding.indirection.expects(:find).returns('443') resource.set_default_parameters(scope) resource[:port].should == '443' end it "should use the default value if the data_binding terminus returns nil" do Puppet::DataBinding.indirection.expects(:find).returns(nil) resource.set_default_parameters(scope) resource[:port].should == '80' end it "should fail with error message about data binding on a hiera failure" do Puppet::DataBinding.indirection.expects(:find).raises(Puppet::DataBinding::LookupError, 'Forgettabotit') expect { resource.set_default_parameters(scope) }.to raise_error(Puppet::Error, /Error from DataBinding 'hiera' while looking up 'apache::port':.*Forgettabotit/) end end context "when a value is provided" do let(:port_parameter) do Puppet::Parser::Resource::Param.new( { :name => 'port', :value => '8080' } ) end let(:resource) do Puppet::Parser::Resource.new("class", "apache", :scope => scope, :parameters => [port_parameter]) end it "should not query the data_binding terminus" do Puppet::DataBinding.indirection.expects(:find).never resource.set_default_parameters(scope) end it "should not query the injector" do # enable the injector Puppet[:binder] = true compiler.injector.expects(:find).never resource.set_default_parameters(scope) end it "should use the value provided" do Puppet::DataBinding.indirection.expects(:find).never resource.set_default_parameters(scope).should == [] resource[:port].should == '8080' end end end end describe "when validating all required parameters are present" do it "should be able to validate that all required parameters are present" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "required_param", :arguments => {"a" => nil}) ) lambda { Puppet::Resource.new("required_param", "name", :environment => environment).validate_complete }.should raise_error(Puppet::ParseError) end it "should not fail when all required parameters are present" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "no_required_param") ) resource = Puppet::Resource.new("no_required_param", "name", :environment => environment) resource["a"] = "meh" lambda { resource.validate_complete }.should_not raise_error end it "should not validate against builtin types" do lambda { Puppet::Resource.new("file", "/bar").validate_complete }.should_not raise_error end 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 expect { Puppet::Resource.new("file", "/path", :strict => true, :parameters => {:nosuchparam => "bar"}) }.to raise_error end it "should fail if the resource type cannot be resolved" do expect { Puppet::Resource.new("nosuchtype", "/path", :strict => true) }.to 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") environment.known_resource_types.add type Puppet::Resource.new("foobar", "/my/file", :environment => environment).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}) environment.known_resource_types.add type Puppet::Resource.new("foobar", "/my/file", :environment => environment).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" expect { resource[:name] = "eh" }.to_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 a native type" do before do @resource = Puppet::Resource.new("file", "/my/file") @resource["one"] = "test" @resource["two"] = "other" end # PUP-3272, needs to work becuse serialization is not only to network # it "should produce an equivalent yaml object" do text = @resource.render('yaml') newresource = Puppet::Resource.convert_from('yaml', text) newresource.should equal_resource_attributes_of(@resource) end # PUP-3272, since serialization to network is done in pson, not yaml it "should produce an equivalent pson object" do text = @resource.render('pson') newresource = Puppet::Resource.convert_from('pson', text) newresource.should equal_resource_attributes_of(@resource) end end describe "when serializing a defined type" do before do type = Puppet::Resource::Type.new(:definition, "foo::bar") environment.known_resource_types.add type @resource = Puppet::Resource.new('foo::bar', 'xyzzy', :environment => environment) @resource['one'] = 'test' @resource['two'] = 'other' @resource.resource_type end it "doesn't include transient instance variables (#4506)" do expect(@resource.to_yaml_properties).to_not include(:@rstype) end it "produces an equivalent pson object" do text = @resource.render('pson') newresource = Puppet::Resource.convert_from('pson', text) newresource.should equal_resource_attributes_of(@resource) 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.must 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.must be_instance_of(Puppet::Type.type(:component)) result.title.should == "Foobar[somename]" end 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}, :ensure => 'present', } ) end it "should align, sort and add trailing commas to attributes with ensure first" do @resource.to_manifest.should == <<-HEREDOC.gsub(/^\s{8}/, '').gsub(/\n$/, '') one::two { '/my/file': ensure => 'present', foo => ['one', 'two'], noop => 'true', } HEREDOC end end describe "when converting to Yaml for Hiera" do before do @resource = Puppet::Resource.new("one::two", "/my/file", :parameters => { :noop => true, :foo => %w{one two}, :ensure => 'present', } ) end it "should align and sort to attributes with ensure first" do @resource.to_hierayaml.should == <<-HEREDOC.gsub(/^\s{8}/, '') /my/file: ensure: 'present' foo : ['one', 'two'] noop : 'true' HEREDOC end end describe "when converting to pson" do # 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_data_hash(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_data_hash(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_data_hash(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_data_hash(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_data_hash(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_data_hash(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_data_hash(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_data_hash(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_data_hash(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_data_hash(PSON.parse(resource.to_pson)) result[:requires].should == [ "File[/bar]", "File[/baz]" ] end end describe "when converting from 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_data_hash(@data).type.should == "File" end it "should set its title to the provided title" do Puppet::Resource.from_data_hash(@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_data_hash(@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_data_hash(@data).file.should == "/foo/bar" end it "should set its line to the provided line" do @data['line'] = 50 Puppet::Resource.from_data_hash(@data).line.should == 50 end it "should 'exported' to true if set in the pson data" do @data['exported'] = true Puppet::Resource.from_data_hash(@data).exported.should be_true end it "should 'exported' to false if not set in the pson data" do Puppet::Resource.from_data_hash(@data).exported.should be_false end it "should fail if no title is provided" do @data.delete('title') expect { Puppet::Resource.from_data_hash(@data) }.to raise_error(ArgumentError) end it "should fail if no type is provided" do @data.delete('type') expect { Puppet::Resource.from_data_hash(@data) }.to 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_data_hash(@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_data_hash(@data) resource['foo'].should == %w{one} end end it "implements copy_as_resource" do resource = Puppet::Resource.new("file", "/my/file") resource.copy_as_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 be 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 describe '#parse_title' do describe 'with a composite namevar' do before do Puppet::Type.newtype(:composite) do newparam(:name) newparam(:value) # Configure two title patterns to match a title that is either # separated with a colon or exclamation point. The first capture # will be used for the :name param, and the second capture will be # used for the :value param. def self.title_patterns identity = lambda {|x| x } reverse = lambda {|x| x.reverse } [ [ /^(.*?):(.*?)$/, [ [:name, identity], [:value, identity], ] ], [ /^(.*?)!(.*?)$/, [ [:name, reverse], [:value, reverse], ] ], ] end end end describe "with no matching title patterns" do subject { Puppet::Resource.new(:composite, 'unmatching title')} it "should raise an exception if no title patterns match" do expect do subject.to_hash end.to raise_error(Puppet::Error, /No set of title patterns matched/) end end describe "with a matching title pattern" do subject { Puppet::Resource.new(:composite, 'matching:title') } it "should not raise an exception if there was a match" do expect do subject.to_hash end.to_not raise_error end it "should set the resource parameters from the parsed title values" do h = subject.to_hash h[:name].should == 'matching' h[:value].should == 'title' end end describe "and multiple title patterns" do subject { Puppet::Resource.new(:composite, 'matching!title') } it "should use the first title pattern that matches" do h = subject.to_hash h[:name].should == 'gnihctam' h[:value].should == 'eltit' end end end end describe "#prune_parameters" do before do - Puppet.newtype('blond') do + Puppet::Type.newtype('blond') do newproperty(:ensure) newproperty(:height) newproperty(:weight) newproperty(:sign) newproperty(:friends) newparam(:admits_to_dying_hair) newparam(:admits_to_age) newparam(:name) end end it "should strip all parameters and strip properties that are nil, empty or absent except for ensure" do resource = Puppet::Resource.new("blond", "Bambi", :parameters => { :ensure => 'absent', :height => '', :weight => 'absent', :friends => [], :admits_to_age => true, :admits_to_dying_hair => false }) pruned_resource = resource.prune_parameters pruned_resource.should == Puppet::Resource.new("blond", "Bambi", :parameters => {:ensure => 'absent'}) end it "should leave parameters alone if in parameters_to_include" do resource = Puppet::Resource.new("blond", "Bambi", :parameters => { :admits_to_age => true, :admits_to_dying_hair => false }) pruned_resource = resource.prune_parameters(:parameters_to_include => [:admits_to_dying_hair]) pruned_resource.should == Puppet::Resource.new("blond", "Bambi", :parameters => {:admits_to_dying_hair => false}) end it "should leave properties if not nil, absent or empty" do resource = Puppet::Resource.new("blond", "Bambi", :parameters => { :ensure => 'silly', :height => '7 ft 5 in', :friends => ['Oprah'], }) pruned_resource = resource.prune_parameters pruned_resource.should == resource = Puppet::Resource.new("blond", "Bambi", :parameters => { :ensure => 'silly', :height => '7 ft 5 in', :friends => ['Oprah'], }) end end end