diff options
Diffstat (limited to 'lib')
142 files changed, 5384 insertions, 4215 deletions
diff --git a/lib/puppet.rb b/lib/puppet.rb index 4b0091cd0..2cbf7fb29 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -8,7 +8,6 @@ end require 'singleton' require 'facter' require 'puppet/error' -require 'puppet/external/event-loop' require 'puppet/util' require 'puppet/util/log' require 'puppet/util/autoload' @@ -32,14 +31,7 @@ module Puppet end class << self - # So we can monitor signals and such. - include SignalObserver - include Puppet::Util - - # To keep a copy of arguments. Set within Config#addargs, because I'm - # lazy. - attr_accessor :args attr_reader :features attr_writer :name end @@ -117,7 +109,6 @@ module Puppet # Load all of the configuration parameters. require 'puppet/defaults' - def self.genmanifest if Puppet[:genmanifest] puts Puppet.settings.to_manifest @@ -125,218 +116,14 @@ module Puppet end end - # Run all threads to their ends - def self.join - defined? @threads and @threads.each do |t| t.join end - end - - # Create a new service that we're supposed to run - def self.newservice(service) - @services ||= [] - - @services << service - end - - def self.newthread(&block) - @threads ||= [] - - @threads << Thread.new do - yield - end - end - - def self.newtimer(hash, &block) - timer = nil - threadlock(:timers) do - @timers ||= [] - timer = EventLoop::Timer.new(hash) - @timers << timer - - if block_given? - observe_signal(timer, :alarm, &block) - end - end - - # In case they need it for something else. - timer - end - # Parse the config file for this process. - def self.parse_config(oldconfig = nil) - # First look for the old configuration file. - oldconfig ||= File.join(Puppet[:confdir], Puppet[:name].to_s + ".conf") - if FileTest.exists?(oldconfig) and Puppet[:name] != "puppet" - Puppet.warning "Individual config files are deprecated; remove %s and use puppet.conf" % oldconfig - Puppet.settings.old_parse(oldconfig) - return - end - - # Now check for the normal config. + def self.parse_config if Puppet[:config] and File.exists? Puppet[:config] Puppet.debug "Parsing %s" % Puppet[:config] Puppet.settings.parse(Puppet[:config]) end end - # Relaunch the executable. - def self.restart - command = $0 + " " + self.args.join(" ") - Puppet.notice "Restarting with '%s'" % command - Puppet.shutdown(false) - Puppet::Util::Log.reopen - exec(command) - end - - # Trap a couple of the main signals. This should probably be handled - # in a way that anyone else can register callbacks for traps, but, eh. - def self.settraps - [:INT, :TERM].each do |signal| - trap(signal) do - Puppet.notice "Caught #{signal}; shutting down" - Puppet.debug "Signal caught here:" - caller.each { |l| Puppet.debug l } - Puppet.shutdown - end - end - - # Handle restarting. - trap(:HUP) do - if client = @services.find { |s| s.is_a? Puppet::Network::Client.master } and client.running? - client.restart - else - Puppet.restart - end - end - - # Provide a hook for running clients where appropriate - trap(:USR1) do - done = 0 - Puppet.notice "Caught USR1; triggering client run" - @services.find_all { |s| s.is_a? Puppet::Network::Client }.each do |client| - if client.respond_to? :running? - if client.running? - Puppet.info "Ignoring running %s" % client.class - else - done += 1 - begin - client.runnow - rescue => detail - Puppet.err "Could not run client: %s" % detail - end - end - else - Puppet.info "Ignoring %s; cannot test whether it is running" % - client.class - end - end - - unless done > 0 - Puppet.notice "No clients were run" - end - end - - trap(:USR2) do - Puppet::Util::Log.reopen - end - end - - # Shutdown our server process, meaning stop all services and all threads. - # Optionally, exit. - def self.shutdown(leave = true) - Puppet.notice "Shutting down" - # Unmonitor our timers - defined? @timers and @timers.each do |timer| - EventLoop.current.ignore_timer timer - end - - # This seems to exit the process, although I can't find where it does - # so. Leaving it out doesn't seem to hurt anything. - #if EventLoop.current.running? - # EventLoop.current.quit - #end - - # Stop our services - defined? @services and @services.each do |svc| - next unless svc.respond_to?(:shutdown) - begin - timeout(20) do - svc.shutdown - end - rescue TimeoutError - Puppet.err "%s could not shut down within 20 seconds" % svc.class - end - end - - # And wait for them all to die, giving a decent amount of time - defined? @threads and @threads.each do |thr| - begin - timeout(20) do - thr.join - end - rescue TimeoutError - # Just ignore this, since we can't intelligently provide a warning - end - end - - if leave - exit(0) - end - end - - # Start all of our services and optionally our event loop, which blocks, - # waiting for someone, somewhere, to generate events of some kind. - def self.start(block = true) - # Starting everything in its own thread, fwiw - defined? @services and @services.dup.each do |svc| - newthread do - begin - svc.start - rescue => detail - if Puppet[:trace] - puts detail.backtrace - end - @services.delete svc - Puppet.err "Could not start %s: %s" % [svc.class, detail] - end - end - end - - # We need to give the services a chance to register their timers before - # we try to start monitoring them. - sleep 0.5 - - unless @services.length > 0 - Puppet.notice "No remaining services; exiting" - exit(1) - end - - if defined? @timers and ! @timers.empty? - @timers.each do |timer| - EventLoop.current.monitor_timer timer - end - end - - if block - EventLoop.current.run - end - end - - # Create the timer that our different objects (uh, mostly the client) - # check. - def self.timer - unless defined? @timer - #Puppet.info "Interval is %s" % Puppet[:runinterval] - #@timer = EventLoop::Timer.new(:interval => Puppet[:runinterval]) - @timer = EventLoop::Timer.new( - :interval => Puppet[:runinterval], - :tolerance => 1, - :start? => true - ) - EventLoop.current.monitor_timer @timer - end - @timer - end - # XXX this should all be done using puppet objects, not using # normal mkdir def self.recmkdir(dir,mode = 0755) @@ -375,15 +162,19 @@ module Puppet # Retrieve a type by name. Just proxy to the Type class. def self.type(name) + # LAK:DEP Deprecation notice added 12/17/2008 + Puppet.warning "Puppet.type is deprecated; use Puppet::Type.type" Puppet::Type.type(name) end end require 'puppet/type' require 'puppet/network' +require 'puppet/ssl' require 'puppet/module' require 'puppet/util/storage' require 'puppet/parser/interpreter' + if Puppet[:storeconfigs] require 'puppet/rails' end diff --git a/lib/puppet/agent.rb b/lib/puppet/agent.rb new file mode 100644 index 000000000..3add43ec1 --- /dev/null +++ b/lib/puppet/agent.rb @@ -0,0 +1,134 @@ +require 'sync' +require 'puppet/external/event-loop' + +# A general class for triggering a run of another +# class. +class Puppet::Agent + require 'puppet/agent/locker' + include Puppet::Agent::Locker + + require 'puppet/agent/runner' + + attr_reader :client_class, :client, :needing_restart, :splayed + attr_accessor :stopping + + def configure_delayed_restart + @needing_restart = true + end + + # Just so we can specify that we are "the" instance. + def initialize(client_class) + @splayed = false + + @client_class = client_class + end + + def lockfile_path + client_class.lockfile_path + end + + def needing_restart? + @needing_restart + end + + def restart + configure_delayed_restart and return if running? + Process.kill(:HUP, $$) + @needing_restart = false + end + + # Perform a run with our client. + def run + if running? + Puppet.notice "Run of %s already in progress; skipping" % client_class + return + end + if stopping? + Puppet.notice "In shutdown progress; skipping run" + return + end + splay + with_client do |client| + begin + sync.synchronize { lock { client.run } } + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not run %s: %s" % [client_class, detail] + end + end + end + + def stop + if self.stopping? + Puppet.notice "Already in shutdown" + return + end + self.stopping = true + if client and client.respond_to?(:stop) + begin + client.stop + rescue + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not stop %s: %s" % [client_class, detail] + end + end + ensure + self.stopping = false + end + + def stopping? + stopping + end + + # Have we splayed already? + def splayed? + splayed + end + + # Sleep when splay is enabled; else just return. + def splay + return unless Puppet[:splay] + return if splayed? + + time = rand(Integer(Puppet[:splaylimit]) + 1) + Puppet.info "Sleeping for %s seconds (splay is enabled)" % time + sleep(time) + @splayed = true + end + + # Start listening for events. We're pretty much just listening for + # timer events here. + def start + # Create our timer. Puppet will handle observing it and such. + timer = EventLoop::Timer.new(:interval => Puppet[:runinterval], :tolerance => 1, :start? => true) do + run() + end + + # Run once before we start following the timer + timer.sound_alarm + end + + def sync + unless defined?(@sync) and @sync + @sync = Sync.new + end + @sync + end + + private + + # Create and yield a client instance, keeping a reference + # to it during the yield. + def with_client + begin + @client = client_class.new + rescue => details + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not create instance of %s: %s" % [client_class, details] + return + end + yield @client + ensure + @client = nil + end +end diff --git a/lib/puppet/agent/locker.rb b/lib/puppet/agent/locker.rb new file mode 100644 index 000000000..eaf19177a --- /dev/null +++ b/lib/puppet/agent/locker.rb @@ -0,0 +1,42 @@ +require 'puppet/util/pidlock' + +# Break out the code related to locking the agent. This module is just +# included into the agent, but having it here makes it easier to test. +module Puppet::Agent::Locker + # Let the daemon run again, freely in the filesystem. + def enable + lockfile.unlock(:anonymous => true) + end + + # Stop the daemon from making any catalog runs. + def disable + lockfile.lock(:anonymous => true) + end + + # Yield if we get a lock, else do nothing. Return + # true/false depending on whether we get the lock. + def lock + if lockfile.lock + begin + yield + ensure + lockfile.unlock + end + return true + else + return false + end + end + + def lockfile + unless defined?(@lockfile) + @lockfile = Puppet::Util::Pidlock.new(lockfile_path) + end + + @lockfile + end + + def running? + lockfile.locked? + end +end diff --git a/lib/puppet/agent/runner.rb b/lib/puppet/agent/runner.rb new file mode 100644 index 000000000..705b6c269 --- /dev/null +++ b/lib/puppet/agent/runner.rb @@ -0,0 +1,65 @@ +require 'puppet/agent' +require 'puppet/configurer' +require 'puppet/indirector' + +# A basic class for running the agent. Used by +# puppetrun to kick off agents remotely. +class Puppet::Agent::Runner + extend Puppet::Indirector + indirects :runner, :terminus_class => :rest + + attr_reader :status, :background, :options + + def agent + Puppet::Agent.new(Puppet::Configurer) + end + + def background? + background + end + + def initialize(options = {}) + if options.include?(:background) + @background = options[:background] + options.delete(:background) + end + + valid_options = [:tags, :ignoreschedules] + options.each do |key, value| + raise ArgumentError, "Runner does not accept %s" % key unless valid_options.include?(key) + end + + @options = options + end + + def log_run + msg = "" + msg += "triggered run" % + if options[:tags] + msg += " with tags %s" % options[:tags] + end + + if options[:ignoreschedules] + msg += " ignoring schedules" + end + + Puppet.notice msg + end + + def run + if agent.running? + @status = "running" + return + end + + log_run() + + if background? + Thread.new { agent.run(options) } + else + agent.run(options) + end + + @status = "success" + end +end diff --git a/lib/puppet/config_stores/rest.rb b/lib/puppet/config_stores/rest.rb deleted file mode 100644 index bb3d937ac..000000000 --- a/lib/puppet/config_stores/rest.rb +++ /dev/null @@ -1,60 +0,0 @@ -Puppet::Util::SettingsStore.newstore(:rest) do - desc "Store client configurations via a REST web service." - - require 'net/http' - - # Get a client's config. (called in collector?) - def get(client, config) - # Assuming this comes in as Puppet::Parser objects - # we may need way to choose which transport data type we use. - - # hmm.. is this even useful for stored configs? I suppose there could - # be scenarios where it'd be cool, like ralsh or something. - end - - def initialize - @host = Puppet[:puppetstorehost] - @port = Puppet[:puppetstoreport] - - # Not sure if this is bad idea to share. - @http = Net::HTTP.new(@host, @port) - end - - # Store config to the web service. (called in getconfig?) - def store(client, config) - # Probably store as yaml... - puppetstore = Thread.new do - benchmark(:notice, "Stored configuration for %s" % client) do - begin - # config should come from elsewhere; probably in getconfig I assume. - # should probably allow a config option for the serialization type. - yaml = YAML.dump(config) - url = "/collector/create" - @http.post(url, yaml, { 'Content-Type' => 'text/yaml' }) - rescue => detail - Puppet.err("ERROR: storeconfig failed: ", detail.to_s) - end - end - end - puppetstore.run - end - - # Rough first try... assuming the calling method handles the data type conversion - # Can we use a thread here? Probably needs to be the caller's thread. - def collect_exported(client, conditions) - begin - # Gotta be a better way... seems goofy to me. - # maybe using a nested rails rest route... - - # filterhost so we don't get exported resources for the current client - url = "/resources?restype=exported&filterhost=#{client}" - conditions.each_pair {|k,v| url << "&#{k}=#{v}"} - res = @http.get(url) - rescue => detail - Puppet.err("ERROR: collect_exported failed: ", detail.to_s) - end - - return res.body unless !res.is_a?(Net::HTTPOK) - end - -end diff --git a/lib/puppet/configurer.rb b/lib/puppet/configurer.rb new file mode 100644 index 000000000..b29082d5a --- /dev/null +++ b/lib/puppet/configurer.rb @@ -0,0 +1,166 @@ +# The client for interacting with the puppetmaster config server. +require 'sync' +require 'timeout' +require 'puppet/network/http_pool' +require 'puppet/util' + +class Puppet::Configurer + require 'puppet/configurer/fact_handler' + require 'puppet/configurer/plugin_handler' + + include Puppet::Configurer::FactHandler + include Puppet::Configurer::PluginHandler + + # For benchmarking + include Puppet::Util + + attr_accessor :catalog + attr_reader :compile_time + + # Provide more helpful strings to the logging that the Agent does + def self.to_s + "Puppet configuration client" + end + + class << self + # Puppetd should only have one instance running, and we need a way + # to retrieve it. + attr_accessor :instance + include Puppet::Util + end + + # How to lock instances of this class. + def self.lockfile_path + Puppet[:puppetdlockfile] + end + + def clear + @catalog.clear(true) if @catalog + @catalog = nil + end + + # Initialize and load storage + def dostorage + begin + Puppet::Util::Storage.load + @compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time] + rescue => detail + if Puppet[:trace] + puts detail.backtrace + end + Puppet.err "Corrupt state file %s: %s" % [Puppet[:statefile], detail] + begin + ::File.unlink(Puppet[:statefile]) + retry + rescue => detail + raise Puppet::Error.new("Cannot remove %s: %s" % + [Puppet[:statefile], detail]) + end + end + end + + # Just so we can specify that we are "the" instance. + def initialize + Puppet.settings.use(:main, :ssl, :puppetd) + + self.class.instance = self + @running = false + @splayed = false + end + + # Prepare for catalog retrieval. Downloads everything necessary, etc. + def prepare + dostorage() + + download_plugins() + + download_fact_plugins() + + upload_facts() + end + + # Get the remote catalog, yo. Returns nil if no catalog can be found. + def retrieve_catalog + name = Facter.value("hostname") + catalog_class = Puppet::Resource::Catalog + + # First try it with no cache, then with the cache. + result = nil + begin + duration = thinmark do + result = catalog_class.find(name, :ignore_cache => true) + end + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not retrieve catalog from remote server: %s" % detail + end + + unless result + begin + duration = thinmark do + result = catalog_class.find(name, :ignore_terminus => true) + end + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not retrieve catalog from cache: %s" % detail + end + end + + return nil unless result + + convert_catalog(result, duration) + end + + # Convert a plain resource catalog into our full host catalog. + def convert_catalog(result, duration) + catalog = result.to_ral + catalog.retrieval_duration = duration + catalog.host_config = true + catalog.write_class_file + return catalog + end + + # The code that actually runs the catalog. + # This just passes any options on to the catalog, + # which accepts :tags and :ignoreschedules. + def run(options = {}) + prepare() + + unless catalog = retrieve_catalog + Puppet.err "Could not retrieve catalog; skipping run" + return + end + + begin + benchmark(:notice, "Finished catalog run") do + catalog.apply(options) + end + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err "Failed to apply catalog: %s" % detail + end + + # Now close all of our existing http connections, since there's no + # reason to leave them lying open. + Puppet::Network::HttpPool.clear_http_instances + end + + private + + def self.timeout + timeout = Puppet[:configtimeout] + case timeout + when String: + if timeout =~ /^\d+$/ + timeout = Integer(timeout) + else + raise ArgumentError, "Configuration timeout must be an integer" + end + when Integer: # nothing + else + raise ArgumentError, "Configuration timeout must be an integer" + end + + return timeout + end +end diff --git a/lib/puppet/configurer/downloader.rb b/lib/puppet/configurer/downloader.rb new file mode 100644 index 000000000..8a2eb0b82 --- /dev/null +++ b/lib/puppet/configurer/downloader.rb @@ -0,0 +1,79 @@ +require 'puppet/configurer' +require 'puppet/resource/catalog' + +class Puppet::Configurer::Downloader + attr_reader :name, :path, :source, :ignore + + # Determine the timeout value to use. + def self.timeout + timeout = Puppet[:configtimeout] + case timeout + when String: + if timeout =~ /^\d+$/ + timeout = Integer(timeout) + else + raise ArgumentError, "Configuration timeout must be an integer" + end + when Integer: # nothing + else + raise ArgumentError, "Configuration timeout must be an integer" + end + + return timeout + end + + # Evaluate our download, returning the list of changed values. + def evaluate + Puppet.info "Retrieving #{name}" + + files = [] + begin + Timeout.timeout(self.class.timeout) do + catalog.apply do |trans| + trans.changed?.find_all do |resource| + yield resource if block_given? + files << resource[:path] + end + end + end + rescue Puppet::Error, Timeout::Error => detail + puts detail.backtrace if Puppet[:debug] + Puppet.err "Could not retrieve #{name}: %s" % detail + end + + return files + end + + def initialize(name, path, source, ignore = nil) + @name, @path, @source, @ignore = name, path, source, ignore + end + + def catalog + catalog = Puppet::Resource::Catalog.new + catalog.add_resource(file) + catalog + end + + def file + args = default_arguments.merge(:path => path, :source => source) + args[:ignore] = ignore if ignore + Puppet::Type.type(:file).new(args) + end + + private + + def default_arguments + { + :path => path, + :recurse => true, + :source => source, + :tag => name, + :owner => Process.uid, + :group => Process.gid, + :purge => true, + :force => true, + :backup => false, + :noop => false + } + end +end diff --git a/lib/puppet/configurer/fact_handler.rb b/lib/puppet/configurer/fact_handler.rb new file mode 100644 index 000000000..8a6de5e9f --- /dev/null +++ b/lib/puppet/configurer/fact_handler.rb @@ -0,0 +1,55 @@ +require 'puppet/indirector/facts/facter' + +require 'puppet/configurer/downloader' + +# Break out the code related to facts. This module is +# just included into the agent, but having it here makes it +# easier to test. +module Puppet::Configurer::FactHandler + def download_fact_plugins? + Puppet[:factsync] + end + + def upload_facts + # XXX down = Puppet[:downcasefacts] + + reload_facter() + + # This works because puppetd configures Facts to use 'facter' for + # finding facts and the 'rest' terminus for caching them. Thus, we'll + # compile them and then "cache" them on the server. + begin + Puppet::Node::Facts.find(Puppet[:certname]) + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err("Could not retrieve local facts: %s" % detail) + end + end + + # Retrieve facts from the central server. + def download_fact_plugins + return unless download_fact_plugins? + + Puppet::Configurer::Downloader.new("fact", Puppet[:factdest], Puppet[:factsource], Puppet[:factsignore]).evaluate + end + + # Clear out all of the loaded facts and reload them from disk. + # NOTE: This is clumsy and shouldn't be required for later (1.5.x) versions + # of Facter. + def reload_facter + Facter.clear + + # Reload everything. + if Facter.respond_to? :loadfacts + Facter.loadfacts + elsif Facter.respond_to? :load + Facter.load + else + Puppet.warning "You should upgrade your version of Facter to at least 1.3.8" + end + + # This loads all existing facts and any new ones. We have to remove and + # reload because there's no way to unload specific facts. + Puppet::Node::Facts::Facter.load_fact_plugins() + end +end diff --git a/lib/puppet/configurer/plugin_handler.rb b/lib/puppet/configurer/plugin_handler.rb new file mode 100644 index 000000000..def6a1707 --- /dev/null +++ b/lib/puppet/configurer/plugin_handler.rb @@ -0,0 +1,25 @@ +# Break out the code related to plugins. This module is +# just included into the agent, but having it here makes it +# easier to test. +module Puppet::Configurer::PluginHandler + def download_plugins? + Puppet[:pluginsync] + end + + # Retrieve facts from the central server. + def download_plugins + return nil unless download_plugins? + Puppet::Configurer::Downloader.new("plugin", Puppet[:plugindest], Puppet[:pluginsource], Puppet[:pluginsignore]).evaluate.each { |file| load_plugin(file) } + end + + def load_plugin(file) + return if FileTest.directory?(file) + + begin + Puppet.info "Loading downloaded plugin %s" % file + load file + rescue Exception => detail + Puppet.err "Could not load downloaded file %s: %s" % [file, detail] + end + end +end diff --git a/lib/puppet/daemon.rb b/lib/puppet/daemon.rb index 24d743764..5f811d897 100755 --- a/lib/puppet/daemon.rb +++ b/lib/puppet/daemon.rb @@ -1,10 +1,11 @@ require 'puppet' require 'puppet/util/pidlock' +require 'puppet/external/event-loop' # A module that handles operations common to all daemons. This is included # into the Server and Client base classes. -module Puppet::Daemon - include Puppet::Util +class Puppet::Daemon + attr_accessor :agent, :server, :argv def daemonname Puppet[:name] @@ -17,7 +18,7 @@ module Puppet::Daemon exit(0) end - setpidfile() + create_pidfile() # Get rid of console logging Puppet::Util::Log.close(:console) @@ -38,18 +39,42 @@ module Puppet::Daemon end end - # The path to the pid file for this server + # Create a pidfile for our daemon, so we can be stopped and others + # don't try to start. + def create_pidfile + Puppet::Util.sync(Puppet[:name]).synchronize(Sync::EX) do + unless Puppet::Util::Pidlock.new(pidfile).lock + raise "Could not create PID file: %s" % [pidfile] + end + end + end + + # Provide the path to our pidfile. def pidfile - if Puppet[:pidfile] != "" - Puppet[:pidfile] - else - File.join(Puppet[:rundir], daemonname() + ".pid") + Puppet[:pidfile] + end + + def reexec + raise Puppet::DevError, "Cannot reexec unless ARGV arguments are set" unless argv + command = $0 + " " + argv.join(" ") + Puppet.notice "Restarting with '%s'" % command + stop(:exit => false) + exec(command) + end + + def reload + return unless agent + if agent.running? + Puppet.notice "Not triggering already-running agent" + return end + + agent.run end - # Remove the pid file - def rmpidfile - threadlock(:pidfile) do + # Remove the pid file for our daemon. + def remove_pidfile + Puppet::Util.sync(Puppet[:name]).synchronize(Sync::EX) do locker = Puppet::Util::Pidlock.new(pidfile) if locker.locked? locker.unlock or Puppet.err "Could not remove PID file %s" % [pidfile] @@ -57,27 +82,52 @@ module Puppet::Daemon end end - # Create the pid file. - def setpidfile - threadlock(:pidfile) do - unless Puppet::Util::Pidlock.new(pidfile).lock - Puppet.err("Could not create PID file: %s" % [pidfile]) - exit(74) - end + def restart + if agent and agent.running? + agent.configure_delayed_restart + else + reexec end end - # Shut down our server - def shutdown - # Remove our pid file - rmpidfile() + def reopen_logs + Puppet::Util::Log.reopen + end - # And close all logs except the console. - Puppet::Util::Log.destinations.reject { |d| d == :console }.each do |dest| - Puppet::Util::Log.close(dest) + # Trap a couple of the main signals. This should probably be handled + # in a way that anyone else can register callbacks for traps, but, eh. + def set_signal_traps + {:INT => :stop, :TERM => :stop, :HUP => :restart, :USR1 => :reload, :USR2 => :reopen_logs}.each do |signal, method| + trap(signal) do + Puppet.notice "Caught #{signal}; calling #{method}" + send(method) + end end + end + + # Stop everything + def stop(args = {:exit => true}) + server.stop if server + + agent.stop if agent + + remove_pidfile() + + Puppet::Util::Log.close_all + + exit if args[:exit] + end + + def start + set_signal_traps + + create_pidfile + + raise Puppet::DevError, "Daemons must have an agent, server, or both" unless agent or server + agent.start if agent + server.start if server - super + EventLoop.current.run end end diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index e1b6dc423..ef170011f 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -60,12 +60,6 @@ module Puppet this directory can be removed without causing harm (although it might result in spurious service restarts)." }, - :ssldir => { - :default => "$confdir/ssl", - :mode => 0771, - :owner => "root", - :desc => "Where SSL certificates are kept." - }, :rundir => { :default => rundir, :mode => 01777, @@ -141,7 +135,25 @@ module Puppet but then ship with tools that do not know how to handle signed ints, so the UIDs show up as huge numbers that can then not be fed back into the system. This is a hackish way to fail in a slightly more useful way when that happens."], - :node_terminus => ["plain", "Where to find information about nodes."] + :node_terminus => ["plain", "Where to find information about nodes."], + :httplog => { :default => "$logdir/http.log", + :owner => "root", + :mode => 0640, + :desc => "Where the puppetd web server logs." + }, + :http_proxy_host => ["none", + "The HTTP proxy host to use for outgoing connections. Note: You + may need to use a FQDN for the server hostname when using a proxy."], + :http_proxy_port => [3128, + "The HTTP proxy port to use for outgoing connections"], + :http_enable_post_connection_check => [true, + "Boolean; wheter or not puppetd should validate the server + SSL certificate against the request hostname."], + :filetimeout => [ 15, + "The minimum time to wait (in seconds) between checking for updates in + configuration files. This timeout determines how quickly Puppet checks whether + a file (such as manifests or templates) has changed on disk." + ] ) hostname = Facter["hostname"].value @@ -152,7 +164,7 @@ module Puppet fqdn = hostname end - Puppet.setdefaults(:ssl, + Puppet.setdefaults(:main, # We have to downcase the fqdn, because the current ssl stuff (as oppsed to in master) doesn't have good facilities for # manipulating naming. :certname => {:default => fqdn.downcase, :desc => "The name to use when handling certificates. Defaults @@ -163,7 +175,14 @@ module Puppet If it's anything other than an empty string, it will be used as an alias in the created certificate. By default, only the server gets an alias set up, and only for 'puppet'."], :certdir => ["$ssldir/certs", "The certificate directory."], + :ssldir => { + :default => "$confdir/ssl", + :mode => 0771, + :owner => "root", + :desc => "Where SSL certificates are kept." + }, :publickeydir => ["$ssldir/public_keys", "The public key directory."], + :requestdir => ["$ssldir/certificate_requests", "Where host certificate requests are stored."], :privatekeydir => { :default => "$ssldir/private_keys", :mode => 0750, :desc => "The private key directory." @@ -179,7 +198,7 @@ module Puppet }, :hostcsr => { :default => "$ssldir/csr_$certname.pem", :mode => 0644, - :desc => "Where individual hosts store and look for their certificates." + :desc => "Where individual hosts store and look for their certificate requests." }, :hostcert => { :default => "$certdir/$certname.pem", :mode => 0644, @@ -196,6 +215,11 @@ module Puppet :localcacert => { :default => "$certdir/ca.pem", :mode => 0644, :desc => "Where each client stores the CA certificate." + }, + :hostcrl => { :default => "$ssldir/crl.pem", + :mode => 0644, + :desc => "Where the host's certificate revocation list can be found. + This is distinct from the certificate authority's CRL." } ) @@ -227,7 +251,12 @@ module Puppet :owner => "$user", :group => "$group", :mode => 0664, - :desc => "The certificate revocation list (CRL) for the CA. Set this to 'false' if you do not want to use a CRL." + :desc => "The certificate revocation list (CRL) for the CA. Will be used if present but otherwise ignored.", + :hook => proc do |value| + if value == 'false' + Puppet.warning "Setting the :cacrl to 'false' is deprecated; Puppet will just ignore the crl if yours is missing" + end + end }, :caprivatedir => { :default => "$cadir/private", :owner => "$user", @@ -255,7 +284,7 @@ module Puppet :serial => { :default => "$cadir/serial", :owner => "$user", :group => "$group", - :mode => 0600, + :mode => 0644, :desc => "Where the serial number for certificates is stored." }, :autosign => { :default => "$confdir/autosign.conf", @@ -288,13 +317,16 @@ module Puppet self.setdefaults(self.settings[:name], :config => ["$confdir/puppet.conf", "The configuration file for #{Puppet[:name]}."], - :pidfile => ["", "The pid file"], - :bindaddress => ["", "The address to bind to. Mongrel servers + :pidfile => ["$rundir/$name.pid", "The pid file"], + :bindaddress => ["", "The address a listening server should bind to. Mongrel servers default to 127.0.0.1 and WEBrick defaults to 0.0.0.0."], - :servertype => ["webrick", "The type of server to use. Currently supported + :servertype => {:default => "webrick", :desc => "The type of server to use. Currently supported options are webrick and mongrel. If you use mongrel, you will need a proxy in front of the process or processes, since Mongrel cannot - speak SSL."] + speak SSL.", + :call_on_define => true, # Call our hook with the default value, so we always get the correct bind address set. + :hook => proc { |value| value == "webrick" ? Puppet.settings[:bindaddress] = "0.0.0.0" : Puppet.settings[:bindaddress] = "127.0.0.1" if Puppet.settings[:bindaddress] == "" } + } ) self.setdefaults(:puppetmasterd, @@ -337,9 +369,9 @@ module Puppet :desc => "Where FileBucket files are stored." }, :ca => [true, "Wether the master should function as a certificate authority."], - :modulepath => [ "$confdir/modules:/usr/share/puppet/modules", - "The search path for modules as a colon-separated list of - directories." ], + :modulepath => {:default => "$confdir/modules:/usr/share/puppet/modules", + :desc => "The search path for modules as a colon-separated list of + directories.", :type => :element }, # We don't want this to be considered a file, since it's multiple files. :ssl_client_header => ["HTTP_X_CLIENT_DN", "The header containing an authenticated client's SSL DN. Only used with Mongrel. This header must be set by the proxy to the authenticated client's SSL DN (e.g., ``/CN=puppet.reductivelabs.com``). @@ -353,7 +385,31 @@ module Puppet :yamldir => {:default => "$vardir/yaml", :owner => "$user", :group => "$user", :mode => "750", :desc => "The directory in which YAML data is stored, usually in a subdirectory."}, :clientyamldir => {:default => "$vardir/client_yaml", :mode => "750", - :desc => "The directory in which client-side YAML data is stored."} + :desc => "The directory in which client-side YAML data is stored."}, + :reports => ["store", + "The list of reports to generate. All reports are looked for + in puppet/reports/<name>.rb, and multiple report names should be + comma-separated (whitespace is okay)." + ], + :reportdir => {:default => "$vardir/reports", + :mode => 0750, + :owner => "$user", + :group => "$group", + :desc => "The directory in which to store reports + received from the client. Each client gets a separate + subdirectory."}, + :fileserverconfig => ["$confdir/fileserver.conf", + "Where the fileserver configuration is stored."], + :rrddir => {:default => "$vardir/rrd", + :owner => "$user", + :group => "$group", + :desc => "The directory where RRD database files are stored. + Directories for each reporting host will be created under + this directory." + }, + :rrdgraph => [false, "Whether RRD information should be graphed."], + :rrdinterval => ["$runinterval", "How often RRD should expect data. + This should match how often the hosts report back to the server."] ) self.setdefaults(:puppetd, @@ -381,19 +437,6 @@ module Puppet :mode => 0640, :desc => "The log file for puppetd. This is generally not used." }, - :httplog => { :default => "$logdir/http.log", - :owner => "root", - :mode => 0640, - :desc => "Where the puppetd web server logs." - }, - :http_proxy_host => ["none", - "The HTTP proxy host to use for outgoing connections. Note: You - may need to use a FQDN for the server hostname when using a proxy."], - :http_proxy_port => [3128, - "The HTTP proxy port to use for outgoing connections"], - :http_enable_post_connection_check => [true, - "Boolean; wheter or not puppetd should validate the server - SSL certificate against the request hostname."], :server => ["puppet", "The server to which server puppetd should connect"], :ignoreschedules => [false, @@ -414,35 +457,7 @@ module Puppet :ca_port => ["$masterport", "The port to use for the certificate authority."], :catalog_format => ["yaml", "What format to use to dump the catalog. Only supports 'marshal' and 'yaml'. Only matters on the client, since it asks the server - for a specific format."] - ) - - self.setdefaults(:filebucket, - :clientbucketdir => { - :default => "$vardir/clientbucket", - :mode => 0750, - :desc => "Where FileBucket files are stored locally." - } - ) - self.setdefaults(:fileserver, - :fileserverconfig => ["$confdir/fileserver.conf", - "Where the fileserver configuration is stored."] - ) - self.setdefaults(:reporting, - :reports => ["store", - "The list of reports to generate. All reports are looked for - in puppet/reports/<name>.rb, and multiple report names should be - comma-separated (whitespace is okay)." - ], - :reportdir => {:default => "$vardir/reports", - :mode => 0750, - :owner => "$user", - :group => "$group", - :desc => "The directory in which to store reports - received from the client. Each client gets a separate - subdirectory."} - ) - self.setdefaults(:puppetd, + for a specific format."], :puppetdlockfile => [ "$statedir/puppetdlock", "A lock file to temporarily stop puppetd from doing anything."], :usecacheonfailure => [true, @@ -468,10 +483,12 @@ module Puppet run interval."], :splay => [false, "Whether to sleep for a pseudo-random (but consistent) amount of time before - a run."] - ) - - self.setdefaults(:puppetd, + a run."], + :clientbucketdir => { + :default => "$vardir/clientbucket", + :mode => 0750, + :desc => "Where FileBucket files are stored locally." + }, :configtimeout => [120, "How long the client should wait for the configuration to be retrieved before considering it a failure. This can help reduce flapping if too @@ -482,16 +499,24 @@ module Puppet ], :report => [false, "Whether to send reports after every transaction." - ] + ], + :graph => [false, "Whether to create dot graph files for the different + configuration graphs. These dot files can be interpreted by tools + like OmniGraffle or dot (which is part of ImageMagick)."], + :graphdir => ["$statedir/graphs", "Where to store dot-outputted graphs."], + :storeconfigs => [false, + "Whether to store each client's configuration. This + requires ActiveRecord from Ruby on Rails."] ) # Plugin information. self.setdefaults(:main, - :pluginpath => ["$vardir/plugins", - "Where Puppet should look for plugins. Multiple directories should + :pluginpath => {:default => "$vardir/plugins/", + :desc => "Where Puppet should look for plugins. Multiple directories should be colon-separated, like normal PATH variables. As of 0.23.1, this option is deprecated; download your custom libraries to the $libdir - instead."], + instead.", + :type => :element}, # Don't consider this a file, since it's a colon-separated list. :plugindest => ["$libdir", "Where Puppet should store plugins that it pulls down from the central server."], @@ -507,15 +532,16 @@ module Puppet # Central fact information. self.setdefaults(:main, - :factpath => {:default => "$vardir/facts", + :factpath => {:default => "$vardir/facts/", :desc => "Where Puppet should look for facts. Multiple directories should be colon-separated, like normal PATH variables.", :call_on_define => true, # Call our hook with the default value, so we always get the value added to facter. + :type => :element, # Don't consider it a file, because it could be multiple colon-separated files :hook => proc { |value| Facter.search(value) if Facter.respond_to?(:search) }}, - :factdest => ["$vardir/facts", + :factdest => ["$vardir/facts/", "Where Puppet should store facts that it pulls down from the central server."], - :factsource => ["puppet://$server/facts", + :factsource => ["puppet://$server/facts/", "From where to retrieve facts. The standard Puppet ``file`` type is used for retrieval, so anything that is a valid file source can be used here."], @@ -566,13 +592,6 @@ module Puppet and other environments normally use ``debug``."] ) - setdefaults(:graphing, - :graph => [false, "Whether to create dot graph files for the different - configuration graphs. These dot files can be interpreted by tools - like OmniGraffle or dot (which is part of ImageMagick)."], - :graphdir => ["$statedir/graphs", "Where to store dot-outputted graphs."] - ) - setdefaults(:transaction, :tags => ["", "Tags to use to find resources. If this is set, then only resources tagged with the specified tags will be applied. @@ -649,12 +668,6 @@ module Puppet branch under your main directory."] ) - setdefaults(:puppetmasterd, - :storeconfigs => [false, - "Whether to store each client's configuration. This - requires ActiveRecord from Ruby on Rails."] - ) - # This doesn't actually work right now. setdefaults(:parser, :lexical => [false, "Whether to use lexical scoping (vs. dynamic)."], @@ -663,26 +676,4 @@ module Puppet directories." ] ) - - setdefaults(:main, - :filetimeout => [ 15, - "The minimum time to wait (in seconds) between checking for updates in - configuration files. This timeout determines how quickly Puppet checks whether - a file (such as manifests or templates) has changed on disk." - ] - ) - - setdefaults(:metrics, - :rrddir => {:default => "$vardir/rrd", - :owner => "$user", - :group => "$group", - :desc => "The directory where RRD database files are stored. - Directories for each reporting host will be created under - this directory." - }, - :rrdgraph => [false, "Whether RRD information should be graphed."], - :rrdinterval => ["$runinterval", "How often RRD should expect data. - This should match how often the hosts report back to the server."] - ) end - diff --git a/lib/puppet/executables/client/certhandler.rb b/lib/puppet/executables/client/certhandler.rb deleted file mode 100644 index b041397ae..000000000 --- a/lib/puppet/executables/client/certhandler.rb +++ /dev/null @@ -1,77 +0,0 @@ - -module Puppet - module Executables - module Client - class CertHandler - attr_writer :wait_for_cert, :one_time - attr_reader :new_cert - - def initialize(wait_time, is_one_time) - @wait_for_cert = wait_time - @one_time = is_one_time - @new_cert = false - end - - # Did we just read a cert? - def new_cert? - new_cert - end - - # Read, or retrieve if necessary, our certificate. Returns true if we retrieved - # a new cert, false if the cert already exists. - def read_retrieve - #NOTE: ACS this is checking that a file exists, maybe next time just do that? - unless read_cert - # If we don't already have the certificate, then create a client to - # request one. Use the special ca stuff, don't use the normal server and port. - retrieve_cert - end - - ! new_cert? - end - - def retrieve_cert - caclient = Puppet::Network::Client.ca.new() - - while true do - begin - if caclient.request_cert - break if read_new_cert - else - Puppet.notice "Did not receive certificate" - if @one_time - Puppet.notice "Set to run 'one time'; exiting with no certificate" - exit(1) - end - end - rescue StandardError => detail - Puppet.err "Could not request certificate: %s" % detail.to_s - exit(23) if @one_time - end - - sleep @wait_for_cert - end - end - - def read_cert - Puppet::Network::HttpPool.read_cert - end - - def read_new_cert - if Puppet::Network::HttpPool.read_cert - # If we read it in, then we need to get rid of our existing http connection. - # The @new_cert flag will help us do that, in that it provides a way - # to notify that the cert status has changed. - @new_cert = true - Puppet.notice "Got signed certificate" - else - Puppet.err "Could not read certificates after retrieving them" - exit(34) if @one_time - end - - return @new_cert - end - end - end - end -end diff --git a/lib/puppet/file_serving/file_base.rb b/lib/puppet/file_serving/base.rb index e87d683aa..2a0199dee 100644 --- a/lib/puppet/file_serving/file_base.rb +++ b/lib/puppet/file_serving/base.rb @@ -6,8 +6,10 @@ require 'puppet/file_serving' # The base class for Content and Metadata; provides common # functionality like the behaviour around links. -class Puppet::FileServing::FileBase - attr_accessor :key +class Puppet::FileServing::Base + # This is for external consumers to store the source that was used + # to retrieve the metadata. + attr_accessor :source # Does our file exist? def exist? @@ -21,17 +23,15 @@ class Puppet::FileServing::FileBase # Return the full path to our file. Fails if there's no path set. def full_path - raise(ArgumentError, "You must set a path to get a file's path") unless self.path - - if relative_path.nil? or relative_path == "" + (if relative_path.nil? or relative_path == "" or relative_path == "." path else File.join(path, relative_path) - end + end).gsub(%r{/+}, "/") end - def initialize(key, options = {}) - @key = key + def initialize(path, options = {}) + self.path = path @links = :manage options.each do |param, value| diff --git a/lib/puppet/file_serving/configuration.rb b/lib/puppet/file_serving/configuration.rb index ccf0957d1..907186ac3 100644 --- a/lib/puppet/file_serving/configuration.rb +++ b/lib/puppet/file_serving/configuration.rb @@ -5,25 +5,23 @@ require 'puppet' require 'puppet/file_serving' require 'puppet/file_serving/mount' +require 'puppet/util/cacher' class Puppet::FileServing::Configuration require 'puppet/file_serving/configuration/parser' + class << self + include Puppet::Util::Cacher + cached_attr(:configuration) { new() } + end + @config_fileuration = nil Mount = Puppet::FileServing::Mount - # Remove our singleton instance. - def self.clear_cache - @config_fileuration = nil - end - # Create our singleton configuration. def self.create - unless @config_fileuration - @config_fileuration = new() - end - @config_fileuration + configuration end private_class_method :new @@ -103,13 +101,14 @@ class Puppet::FileServing::Configuration # Reparse the configuration if necessary. readconfig - raise(ArgumentError, "Cannot find file: Invalid path '%s'" % uri) unless uri =~ %r{/([-\w]+)/?} + raise(ArgumentError, "Cannot find file: Invalid path '%s'" % uri) unless uri =~ %r{^([-\w]+)(/|$)} # the dir is based on one of the mounts # so first retrieve the mount path mount = path = nil + # Strip off the mount name. - mount_name, path = uri.sub(%r{^/}, '').split(File::Separator, 2) + mount_name, path = uri.split(File::Separator, 2) return nil unless mount = @mounts[mount_name] diff --git a/lib/puppet/file_serving/content.rb b/lib/puppet/file_serving/content.rb index 9398513e7..c1ecff749 100644 --- a/lib/puppet/file_serving/content.rb +++ b/lib/puppet/file_serving/content.rb @@ -4,31 +4,46 @@ require 'puppet/indirector' require 'puppet/file_serving' -require 'puppet/file_serving/file_base' +require 'puppet/file_serving/base' require 'puppet/file_serving/indirection_hooks' # A class that handles retrieving file contents. # It only reads the file when its content is specifically # asked for. -class Puppet::FileServing::Content < Puppet::FileServing::FileBase +class Puppet::FileServing::Content < Puppet::FileServing::Base extend Puppet::Indirector indirects :file_content, :extend => Puppet::FileServing::IndirectionHooks - attr_reader :path + attr_writer :content + + def self.supported_formats + [:raw] + end + + def self.from_raw(content) + instance = new("/this/is/a/fake/path") + instance.content = content + instance + end + + # Collect our data. + def collect + return if stat.ftype == "directory" + content + end # Read the content of our file in. def content - # This stat can raise an exception, too. - raise(ArgumentError, "Cannot read the contents of links unless following links") if stat().ftype == "symlink" + unless defined?(@content) and @content + # This stat can raise an exception, too. + raise(ArgumentError, "Cannot read the contents of links unless following links") if stat().ftype == "symlink" - ::File.read(full_path()) + @content = ::File.read(full_path()) + end + @content end - # Just return the file contents as the yaml. This allows us to - # avoid escaping or any such thing. LAK:NOTE Not really sure how - # this will behave if the file contains yaml... I think the far - # side needs to understand that it's a plain string. - def to_yaml + def to_raw content end end diff --git a/lib/puppet/file_serving/fileset.rb b/lib/puppet/file_serving/fileset.rb index 3cb76317d..b28fb2d7e 100644 --- a/lib/puppet/file_serving/fileset.rb +++ b/lib/puppet/file_serving/fileset.rb @@ -30,6 +30,8 @@ class Puppet::FileServing::Fileset # Should we ignore this path? def ignore?(path) + return false if @ignore == [nil] + # 'detect' normally returns the found result, whereas we just want true/false. ! @ignore.detect { |pattern| File.fnmatch?(pattern, path) }.nil? end diff --git a/lib/puppet/file_serving/indirection_hooks.rb b/lib/puppet/file_serving/indirection_hooks.rb index 66ed169dc..15564cf3d 100644 --- a/lib/puppet/file_serving/indirection_hooks.rb +++ b/lib/puppet/file_serving/indirection_hooks.rb @@ -9,36 +9,37 @@ require 'puppet/file_serving' # in file-serving indirections. This is necessary because # the terminus varies based on the URI asked for. module Puppet::FileServing::IndirectionHooks - PROTOCOL_MAP = {"puppet" => :rest, "file" => :file, "puppetmounts" => :file_server} + PROTOCOL_MAP = {"puppet" => :rest, "file" => :file} # Pick an appropriate terminus based on the protocol. def select_terminus(request) - full_uri = request.key - # Short-circuit to :file if it's a fully-qualified path. - return PROTOCOL_MAP["file"] if full_uri =~ /^#{::File::SEPARATOR}/ - begin - uri = URI.parse(URI.escape(full_uri)) - rescue => detail - raise ArgumentError, "Could not understand URI %s: %s" % [full_uri, detail.to_s] - end + # We rely on the request's parsing of the URI. - terminus = PROTOCOL_MAP[uri.scheme] || raise(ArgumentError, "URI protocol '%s' is not supported for file serving" % uri.scheme) + # Short-circuit to :file if it's a fully-qualified path or specifies a 'file' protocol. + return PROTOCOL_MAP["file"] if request.key =~ /^#{::File::SEPARATOR}/ + return PROTOCOL_MAP["file"] if request.protocol == "file" - # This provides a convenient mechanism for people to write configurations work - # well in both a networked and local setting. - if uri.host.nil? and uri.scheme == "puppet" and Puppet.settings[:name] == "puppet" - terminus = :file_server + # We're heading over the wire the protocol is 'puppet' and we've got a server name or we're not named 'puppet' + if request.protocol == "puppet" and (request.server or Puppet.settings[:name] != "puppet") + return PROTOCOL_MAP["puppet"] + end + + if request.protocol and PROTOCOL_MAP[request.protocol].nil? + raise(ArgumentError, "URI protocol '%s' is not currently supported for file serving" % request.protocol) end + # If we're still here, we're using the file_server or modules. + # This is the backward-compatible module terminus. - if terminus == :file_server and uri.path =~ %r{^/([^/]+)\b} - modname = $1 - if modname == "modules" - terminus = :modules - elsif terminus(:modules).find_module(modname, request.options[:node]) - Puppet.warning "DEPRECATION NOTICE: Found file '%s' in module without using the 'modules' mount; please prefix path with '/modules'" % uri.path - terminus = :modules - end + modname = request.key.split("/")[0] + + if modname == "modules" + terminus = :modules + elsif terminus(:modules).find_module(modname, request.options[:node]) + Puppet.warning "DEPRECATION NOTICE: Found file '%s' in module without using the 'modules' mount; please prefix path with 'modules/'" % request.key + terminus = :modules + else + terminus = :file_server end return terminus diff --git a/lib/puppet/file_serving/metadata.rb b/lib/puppet/file_serving/metadata.rb index b277955ac..1fc2b40ab 100644 --- a/lib/puppet/file_serving/metadata.rb +++ b/lib/puppet/file_serving/metadata.rb @@ -5,12 +5,12 @@ require 'puppet' require 'puppet/indirector' require 'puppet/file_serving' -require 'puppet/file_serving/file_base' +require 'puppet/file_serving/base' require 'puppet/util/checksums' require 'puppet/file_serving/indirection_hooks' # A class that handles retrieving file metadata. -class Puppet::FileServing::Metadata < Puppet::FileServing::FileBase +class Puppet::FileServing::Metadata < Puppet::FileServing::Base include Puppet::Util::Checksums @@ -47,7 +47,7 @@ class Puppet::FileServing::Metadata < Puppet::FileServing::FileBase # Retrieve the attributes for this file, relative to a base directory. # Note that File.stat raises Errno::ENOENT if the file is absent and this # method does not catch that exception. - def collect_attributes + def collect real_path = full_path() stat = stat() @owner = stat.uid diff --git a/lib/puppet/file_serving/mount.rb b/lib/puppet/file_serving/mount.rb index 8e5bd03e8..552cf33f2 100644 --- a/lib/puppet/file_serving/mount.rb +++ b/lib/puppet/file_serving/mount.rb @@ -4,6 +4,7 @@ require 'puppet/network/authstore' require 'puppet/util/logging' +require 'puppet/util/cacher' require 'puppet/file_serving' require 'puppet/file_serving/metadata' require 'puppet/file_serving/content' @@ -13,11 +14,16 @@ require 'puppet/file_serving/content' class Puppet::FileServing::Mount < Puppet::Network::AuthStore include Puppet::Util::Logging - @@localmap = nil + class << self + include Puppet::Util::Cacher - # Clear the cache. This is only ever used for testing. - def self.clear_cache - @@localmap = nil + cached_attr(:localmap) do + { "h" => Facter.value("hostname"), + "H" => [Facter.value("hostname"), + Facter.value("domain")].join("."), + "d" => Facter.value("domain") + } + end end attr_reader :name @@ -173,14 +179,6 @@ class Puppet::FileServing::Mount < Puppet::Network::AuthStore # Cache this manufactured map, since if it's used it's likely # to get used a lot. def localmap - unless @@localmap - @@localmap = { - "h" => Facter.value("hostname"), - "H" => [Facter.value("hostname"), - Facter.value("domain")].join("."), - "d" => Facter.value("domain") - } - end - @@localmap + self.class.localmap end end diff --git a/lib/puppet/file_serving/terminus_helper.rb b/lib/puppet/file_serving/terminus_helper.rb index e5da0e29f..b51e27297 100644 --- a/lib/puppet/file_serving/terminus_helper.rb +++ b/lib/puppet/file_serving/terminus_helper.rb @@ -9,10 +9,20 @@ require 'puppet/file_serving/fileset' module Puppet::FileServing::TerminusHelper # Create model instances for all files in a fileset. def path2instances(request, path) - args = [:links, :ignore, :recurse].inject({}) { |hash, param| hash[param] = request.options[param] if request.options[param]; hash } + args = [:links, :ignore, :recurse].inject({}) do |hash, param| + if request.options.include?(param) # use 'include?' so the values can be false + hash[param] = request.options[param] + elsif request.options.include?(param.to_s) + hash[param] = request.options[param.to_s] + end + hash[param] = true if hash[param] == "true" + hash[param] = false if hash[param] == "false" + hash + end Puppet::FileServing::Fileset.new(path, args).files.collect do |file| - inst = model.new(File.join(request.key, file), :path => path, :relative_path => file) + inst = model.new(path, :relative_path => file) inst.links = request.options[:links] if request.options[:links] + inst.collect inst end end diff --git a/lib/puppet/indirector.rb b/lib/puppet/indirector.rb index 2402b9cbe..1beb68ec0 100644 --- a/lib/puppet/indirector.rb +++ b/lib/puppet/indirector.rb @@ -10,6 +10,7 @@ module Puppet::Indirector require 'puppet/indirector/indirection' require 'puppet/indirector/terminus' require 'puppet/indirector/envelope' + require 'puppet/network/format_handler' # Declare that the including class indirects its methods to # this terminus. The terminus name must be the name of a Puppet @@ -22,6 +23,7 @@ module Puppet::Indirector extend ClassMethods include InstanceMethods include Puppet::Indirector::Envelope + extend Puppet::Network::FormatHandler # instantiate the actual Terminus for that type and this name (:ldap, w/ args :node) # & hook the instantiated Terminus into this class (Node: @indirection = terminus) diff --git a/lib/puppet/indirector/catalog/compiler.rb b/lib/puppet/indirector/catalog/compiler.rb index a6a812817..47635d88c 100644 --- a/lib/puppet/indirector/catalog/compiler.rb +++ b/lib/puppet/indirector/catalog/compiler.rb @@ -1,10 +1,10 @@ require 'puppet/node' -require 'puppet/node/catalog' +require 'puppet/resource/catalog' require 'puppet/indirector/code' require 'puppet/parser/interpreter' require 'yaml' -class Puppet::Node::Catalog::Compiler < Puppet::Indirector::Code +class Puppet::Resource::Catalog::Compiler < Puppet::Indirector::Code desc "Puppet's catalog compilation interface, and its back-end is Puppet's compiler" @@ -14,8 +14,14 @@ class Puppet::Node::Catalog::Compiler < Puppet::Indirector::Code # Compile a node's catalog. def find(request) - unless node = request.options[:use_node] || find_node(request.key) - raise ArgumentError, "Could not find node '%s'; cannot compile" % request.key + unless node = request.options[:use_node] + # If the request is authenticated, then the 'node' info will + # be available; if not, then we use the passed-in key. We rely + # on our authorization system to determine whether this is allowed. + name = request.node || request.key + unless node = find_node(name) + raise ArgumentError, "Could not find node '%s'; cannot compile" % name + end end if catalog = compile(node) @@ -81,15 +87,14 @@ class Puppet::Node::Catalog::Compiler < Puppet::Indirector::Code end # Turn our host name into a node object. - def find_node(key) - # If we want to use the cert name as our key - # LAK:FIXME This needs to be figured out somehow, but it requires the routing. - # This should be able to use the request, yay. - #if Puppet[:node_name] == 'cert' and client - # key = client - #end + def find_node(name) + begin + return nil unless node = Puppet::Node.find(name) + rescue => detail + puts detail.backtrace if Puppet[:trace] + raise Puppet::Error, "Failed when searching for node %s: %s" % [name, detail] + end - return nil unless node = Puppet::Node.find(key) # Add any external data to the node. add_node_data(node) @@ -126,17 +131,6 @@ class Puppet::Node::Catalog::Compiler < Puppet::Indirector::Code end end - # Translate our catalog appropriately for sending back to a client. - # LAK:FIXME This method should probably be part of the protocol, but it - # shouldn't be here. - def translate(config) - unless networked? - config - else - CGI.escape(config.to_yaml(:UseBlock => true)) - end - end - # Mark that the node has checked in. LAK:FIXME this needs to be moved into # the Node class, or somewhere that's got abstract backends. def update_node_check(node) diff --git a/lib/puppet/indirector/catalog/rest.rb b/lib/puppet/indirector/catalog/rest.rb new file mode 100644 index 000000000..b70775be2 --- /dev/null +++ b/lib/puppet/indirector/catalog/rest.rb @@ -0,0 +1,6 @@ +require 'puppet/resource/catalog' +require 'puppet/indirector/rest' + +class Puppet::Resource::Catalog::Rest < Puppet::Indirector::REST + desc "Find resource catalogs over HTTP via REST." +end diff --git a/lib/puppet/indirector/catalog/yaml.rb b/lib/puppet/indirector/catalog/yaml.rb index 00241d852..d6fc9b81d 100644 --- a/lib/puppet/indirector/catalog/yaml.rb +++ b/lib/puppet/indirector/catalog/yaml.rb @@ -1,7 +1,7 @@ -require 'puppet/node/catalog' +require 'puppet/resource/catalog' require 'puppet/indirector/yaml' -class Puppet::Node::Catalog::Yaml < Puppet::Indirector::Yaml +class Puppet::Resource::Catalog::Yaml < Puppet::Indirector::Yaml desc "Store catalogs as flat files, serialized using YAML." private @@ -10,8 +10,6 @@ class Puppet::Node::Catalog::Yaml < Puppet::Indirector::Yaml # objects. This is hackish, but eh. def from_yaml(text) if config = YAML.load(text) - # We can't yaml-dump classes. - #config.edgelist_class = Puppet::Relationship return config end end diff --git a/lib/puppet/indirector/certificate/ca.rb b/lib/puppet/indirector/certificate/ca.rb new file mode 100644 index 000000000..b64080e16 --- /dev/null +++ b/lib/puppet/indirector/certificate/ca.rb @@ -0,0 +1,9 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate' + +class Puppet::SSL::Certificate::Ca < Puppet::Indirector::SslFile + desc "Manage the CA collection of signed SSL certificates on disk." + + store_in :signeddir + store_ca_at :cacert +end diff --git a/lib/puppet/indirector/certificate/file.rb b/lib/puppet/indirector/certificate/file.rb new file mode 100644 index 000000000..c19d001f4 --- /dev/null +++ b/lib/puppet/indirector/certificate/file.rb @@ -0,0 +1,9 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate' + +class Puppet::SSL::Certificate::File < Puppet::Indirector::SslFile + desc "Manage SSL certificates on disk." + + store_in :certdir + store_ca_at :localcacert +end diff --git a/lib/puppet/indirector/certificate/rest.rb b/lib/puppet/indirector/certificate/rest.rb new file mode 100644 index 000000000..f88d60d40 --- /dev/null +++ b/lib/puppet/indirector/certificate/rest.rb @@ -0,0 +1,6 @@ +require 'puppet/ssl/certificate' +require 'puppet/indirector/rest' + +class Puppet::SSL::Certificate::Rest < Puppet::Indirector::REST + desc "Find and save certificates over HTTP via REST." +end diff --git a/lib/puppet/indirector/certificate_request/ca.rb b/lib/puppet/indirector/certificate_request/ca.rb new file mode 100644 index 000000000..e90f43a03 --- /dev/null +++ b/lib/puppet/indirector/certificate_request/ca.rb @@ -0,0 +1,14 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate_request' + +class Puppet::SSL::CertificateRequest::Ca < Puppet::Indirector::SslFile + desc "Manage the CA collection of certificate requests on disk." + + store_in :csrdir + + def save(request) + result = super + Puppet.notice "%s has a waiting certificate request" % request.key + result + end +end diff --git a/lib/puppet/indirector/certificate_request/file.rb b/lib/puppet/indirector/certificate_request/file.rb new file mode 100644 index 000000000..274311e2c --- /dev/null +++ b/lib/puppet/indirector/certificate_request/file.rb @@ -0,0 +1,8 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate_request' + +class Puppet::SSL::CertificateRequest::File < Puppet::Indirector::SslFile + desc "Manage the collection of certificate requests on disk." + + store_in :requestdir +end diff --git a/lib/puppet/indirector/certificate_request/rest.rb b/lib/puppet/indirector/certificate_request/rest.rb new file mode 100644 index 000000000..6df014583 --- /dev/null +++ b/lib/puppet/indirector/certificate_request/rest.rb @@ -0,0 +1,6 @@ +require 'puppet/ssl/certificate_request' +require 'puppet/indirector/rest' + +class Puppet::SSL::CertificateRequest::Rest < Puppet::Indirector::REST + desc "Find and save certificate requests over HTTP via REST." +end diff --git a/lib/puppet/indirector/certificate_revocation_list/ca.rb b/lib/puppet/indirector/certificate_revocation_list/ca.rb new file mode 100644 index 000000000..66cc23e50 --- /dev/null +++ b/lib/puppet/indirector/certificate_revocation_list/ca.rb @@ -0,0 +1,8 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate_revocation_list' + +class Puppet::SSL::CertificateRevocationList::Ca < Puppet::Indirector::SslFile + desc "Manage the CA collection of certificate requests on disk." + + store_at :cacrl +end diff --git a/lib/puppet/indirector/certificate_revocation_list/file.rb b/lib/puppet/indirector/certificate_revocation_list/file.rb new file mode 100644 index 000000000..037aa6b8c --- /dev/null +++ b/lib/puppet/indirector/certificate_revocation_list/file.rb @@ -0,0 +1,8 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/certificate_revocation_list' + +class Puppet::SSL::CertificateRevocationList::File < Puppet::Indirector::SslFile + desc "Manage the global certificate revocation list." + + store_at :hostcrl +end diff --git a/lib/puppet/indirector/certificate_revocation_list/rest.rb b/lib/puppet/indirector/certificate_revocation_list/rest.rb new file mode 100644 index 000000000..13cc95c87 --- /dev/null +++ b/lib/puppet/indirector/certificate_revocation_list/rest.rb @@ -0,0 +1,6 @@ +require 'puppet/ssl/certificate_revocation_list' +require 'puppet/indirector/rest' + +class Puppet::SSL::CertificateRevocationList::Rest < Puppet::Indirector::REST + desc "Find and save certificate revocation lists over HTTP via REST." +end diff --git a/lib/puppet/indirector/direct_file_server.rb b/lib/puppet/indirector/direct_file_server.rb index b3b4886f3..bcda92366 100644 --- a/lib/puppet/indirector/direct_file_server.rb +++ b/lib/puppet/indirector/direct_file_server.rb @@ -12,16 +12,14 @@ class Puppet::Indirector::DirectFileServer < Puppet::Indirector::Terminus include Puppet::FileServing::TerminusHelper def find(request) - uri = key2uri(request.key) - return nil unless FileTest.exists?(uri.path) - instance = model.new(request.key, :path => uri.path) + return nil unless FileTest.exists?(request.key) + instance = model.new(request.key) instance.links = request.options[:links] if request.options[:links] return instance end def search(request) - uri = key2uri(request.key) - return nil unless FileTest.exists?(uri.path) - path2instances(request, uri.path) + return nil unless FileTest.exists?(request.key) + path2instances(request, request.key) end end diff --git a/lib/puppet/indirector/facts/facter.rb b/lib/puppet/indirector/facts/facter.rb index a026dfe60..e664e17c7 100644 --- a/lib/puppet/indirector/facts/facter.rb +++ b/lib/puppet/indirector/facts/facter.rb @@ -6,31 +6,33 @@ class Puppet::Node::Facts::Facter < Puppet::Indirector::Code between Puppet and Facter. It's only `somewhat` abstract because it always returns the local host's facts, regardless of what you attempt to find." - def self.loaddir(dir, type) - return unless FileTest.directory?(dir) - - Dir.entries(dir).find_all { |e| e =~ /\.rb$/ }.each do |file| - fqfile = ::File.join(dir, file) - begin - Puppet.info "Loading %s %s" % - [type, ::File.basename(file.sub(".rb",''))] - Timeout::timeout(self.timeout) do - load fqfile - end - rescue => detail - Puppet.warning "Could not load %s %s: %s" % [type, fqfile, detail] - end - end - end - def self.loadfacts + def self.load_fact_plugins # Add any per-module fact directories to the factpath module_fact_dirs = Puppet[:modulepath].split(":").collect do |d| Dir.glob("%s/*/plugins/facter" % d) end.flatten dirs = module_fact_dirs + Puppet[:factpath].split(":") x = dirs.each do |dir| - loaddir(dir, "fact") + load_facts_in_dir(dir) + end + end + + def self.load_facts_in_dir(dir) + return unless FileTest.directory?(dir) + + Dir.chdir(dir) do + Dir.glob("*.rb").each do |file| + fqfile = ::File.join(dir, file) + begin + Puppet.info "Loading facts in %s" % [::File.basename(file.sub(".rb",''))] + Timeout::timeout(self.timeout) do + load file + end + rescue => detail + Puppet.warning "Could not load fact file %s: %s" % [fqfile, detail] + end + end end end @@ -53,7 +55,7 @@ class Puppet::Node::Facts::Facter < Puppet::Indirector::Code def initialize(*args) super - self.class.loadfacts + self.class.load_fact_plugins end def destroy(facts) @@ -62,7 +64,13 @@ class Puppet::Node::Facts::Facter < Puppet::Indirector::Code # Look a host's facts up in Facter. def find(request) - Puppet::Node::Facts.new(request.key, Facter.to_hash) + result = Puppet::Node::Facts.new(request.key, Facter.to_hash) + + result.add_local_facts + result.stringify + result.downcase_if_necessary + + result end def save(facts) diff --git a/lib/puppet/indirector/facts/rest.rb b/lib/puppet/indirector/facts/rest.rb new file mode 100644 index 000000000..0f1c5f0d7 --- /dev/null +++ b/lib/puppet/indirector/facts/rest.rb @@ -0,0 +1,6 @@ +require 'puppet/node/facts' +require 'puppet/indirector/rest' + +class Puppet::Node::Facts::Rest < Puppet::Indirector::REST + desc "Find and save certificates over HTTP via REST." +end diff --git a/lib/puppet/indirector/file.rb b/lib/puppet/indirector/file.rb index e5382155f..99d95ecb2 100644 --- a/lib/puppet/indirector/file.rb +++ b/lib/puppet/indirector/file.rb @@ -11,6 +11,7 @@ class Puppet::Indirector::File < Puppet::Indirector::Terminus end raise Puppet::Error.new("File %s does not exist; cannot destroy" % [request.key]) unless File.exist?(path) + Puppet.notice "Removing file %s %s at '%s'" % [model, request.key, path] begin File.unlink(path) rescue => detail diff --git a/lib/puppet/indirector/file_metadata/file.rb b/lib/puppet/indirector/file_metadata/file.rb index c46015c38..bb586489d 100644 --- a/lib/puppet/indirector/file_metadata/file.rb +++ b/lib/puppet/indirector/file_metadata/file.rb @@ -11,7 +11,7 @@ class Puppet::Indirector::FileMetadata::File < Puppet::Indirector::DirectFileSer def find(request) return unless data = super - data.collect_attributes + data.collect return data end @@ -19,7 +19,7 @@ class Puppet::Indirector::FileMetadata::File < Puppet::Indirector::DirectFileSer def search(request) return unless result = super - result.each { |instance| instance.collect_attributes } + result.each { |instance| instance.collect } return result end diff --git a/lib/puppet/indirector/file_metadata/modules.rb b/lib/puppet/indirector/file_metadata/modules.rb index 5ed7a8a45..4598c2175 100644 --- a/lib/puppet/indirector/file_metadata/modules.rb +++ b/lib/puppet/indirector/file_metadata/modules.rb @@ -11,7 +11,7 @@ class Puppet::Indirector::FileMetadata::Modules < Puppet::Indirector::ModuleFile def find(*args) return unless instance = super - instance.collect_attributes + instance.collect instance end end diff --git a/lib/puppet/indirector/file_server.rb b/lib/puppet/indirector/file_server.rb index b0df7ff5d..46a590f9c 100644 --- a/lib/puppet/indirector/file_server.rb +++ b/lib/puppet/indirector/file_server.rb @@ -25,8 +25,9 @@ class Puppet::Indirector::FileServer < Puppet::Indirector::Terminus # Find our key using the fileserver. def find(request) return nil unless path = find_path(request) - result = model.new(request.key, :path => path) + result = model.new(path) result.links = request.options[:links] if request.options[:links] + result.collect return result end diff --git a/lib/puppet/indirector/indirection.rb b/lib/puppet/indirector/indirection.rb index 4841ec532..5d8cfe9b5 100644 --- a/lib/puppet/indirector/indirection.rb +++ b/lib/puppet/indirector/indirection.rb @@ -1,20 +1,17 @@ require 'puppet/util/docs' require 'puppet/indirector/envelope' require 'puppet/indirector/request' +require 'puppet/util/cacher' # The class that connects functional classes with their different collection # back-ends. Each indirection has a set of associated terminus classes, # each of which is a subclass of Puppet::Indirector::Terminus. class Puppet::Indirector::Indirection + include Puppet::Util::Cacher include Puppet::Util::Docs @@indirections = [] - # Clear all cached termini from all indirections. - def self.clear_cache - @@indirections.each { |ind| ind.clear_cache } - end - # Find an indirection by name. This is provided so that Terminus classes # can specifically hook up with the indirections they are associated with. def self.instance(name) @@ -50,17 +47,10 @@ class Puppet::Indirector::Indirection attr_reader :cache_class # Define a terminus class to be used for caching. def cache_class=(class_name) - validate_terminus_class(class_name) + validate_terminus_class(class_name) if class_name @cache_class = class_name end - # Clear our cached list of termini, and reset the cache name - # so it's looked up again. - # This is only used for testing. - def clear_cache - @termini.clear - end - # This is only used for testing. def delete @@indirections.delete(self) if @@indirections.include?(self) @@ -104,7 +94,6 @@ class Puppet::Indirector::Indirection @model = model @name = name - @termini = {} @cache_class = nil @terminus_class = nil @@ -138,7 +127,7 @@ class Puppet::Indirector::Indirection raise Puppet::DevError, "No terminus specified for %s; cannot redirect" % self.name end - return @termini[terminus_name] ||= make_terminus(terminus_name) + return termini[terminus_name] ||= make_terminus(terminus_name) end # This can be used to select the terminus class. @@ -197,7 +186,7 @@ class Puppet::Indirector::Indirection terminus = prepare(request) # See if our instance is in the cache and up to date. - if cache? and cached = cache.find(request) + if cache? and ! request.ignore_cache? and cached = cache.find(request) if cached.expired? Puppet.info "Not using expired %s for %s from cache; expired at %s" % [self.name, request.key, cached.expiration] else @@ -207,9 +196,9 @@ class Puppet::Indirector::Indirection end # Otherwise, return the result from the terminus, caching if appropriate. - if result = terminus.find(request) + if ! request.ignore_terminus? and result = terminus.find(request) result.expiration ||= self.expiration - if cache? + if cache? and request.use_cache? Puppet.info "Caching %s for %s" % [self.name, request.key] cache.save request(:save, result, *args) end @@ -255,9 +244,12 @@ class Puppet::Indirector::Indirection request = request(:save, instance, *args) terminus = prepare(request) + result = terminus.save(request) + # If caching is enabled, save our document there cache.save(request) if cache? - terminus.save(request) + + result end private @@ -273,7 +265,11 @@ class Puppet::Indirector::Indirection return unless terminus.respond_to?(:authorized?) unless terminus.authorized?(request) - raise ArgumentError, "Not authorized to call %s on %s with %s" % [request.method, request.key, request.options.inspect] + msg = "Not authorized to call %s on %s" % [request.method, request.key] + unless request.options.empty? + msg += " with %s" % request.options.inspect + end + raise ArgumentError, msg end end @@ -281,14 +277,17 @@ class Puppet::Indirector::Indirection def prepare(request) # Pick our terminus. if respond_to?(:select_terminus) - terminus_name = select_terminus(request) + unless terminus_name = select_terminus(request) + raise ArgumentError, "Could not determine appropriate terminus for %s" % request + end else terminus_name = terminus_class end - check_authorization(request, terminus(terminus_name)) + dest_terminus = terminus(terminus_name) + check_authorization(request, dest_terminus) - return terminus(terminus_name) + return dest_terminus end # Create a new terminus instance. @@ -299,4 +298,7 @@ class Puppet::Indirector::Indirection end return klass.new end + + # Cache our terminus instances indefinitely, but make it easy to clean them up. + cached_attr(:termini) { Hash.new } end diff --git a/lib/puppet/indirector/key/ca.rb b/lib/puppet/indirector/key/ca.rb new file mode 100644 index 000000000..3604de22b --- /dev/null +++ b/lib/puppet/indirector/key/ca.rb @@ -0,0 +1,20 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/key' + +class Puppet::SSL::Key::Ca < Puppet::Indirector::SslFile + desc "Manage the CA's private on disk. This terminus *only* works + with the CA key, because that's the only key that the CA ever interacts + with." + + # This is just to pass the validation in the base class. Eh. + store_at :cakey + + store_ca_at :cakey + + def path(name) + unless ca?(name) + raise ArgumentError, "The :ca terminus can only handle the CA private key" + end + super + end +end diff --git a/lib/puppet/indirector/key/file.rb b/lib/puppet/indirector/key/file.rb new file mode 100644 index 000000000..4536f8aa7 --- /dev/null +++ b/lib/puppet/indirector/key/file.rb @@ -0,0 +1,42 @@ +require 'puppet/indirector/ssl_file' +require 'puppet/ssl/key' + +class Puppet::SSL::Key::File < Puppet::Indirector::SslFile + desc "Manage SSL private and public keys on disk." + + store_in :privatekeydir + store_ca_at :cakey + + # Where should we store the public key? + def public_key_path(name) + if ca?(name) + Puppet[:capub] + else + File.join(Puppet[:publickeydir], name.to_s + ".pem") + end + end + + # Remove the public key, in addition to the private key + def destroy(request) + super + + return unless FileTest.exist?(public_key_path(request.key)) + + begin + File.unlink(public_key_path(request.key)) + rescue => detail + raise Puppet::Error, "Could not remove %s public key: %s" % [request.key, detail] + end + end + + # Save the public key, in addition to the private key. + def save(request) + super + + begin + File.open(public_key_path(request.key), "w") { |f| f.print request.instance.content.public_key.to_pem } + rescue => detail + raise Puppet::Error, "Could not write %s: %s" % [key, detail] + end + end +end diff --git a/lib/puppet/indirector/module_files.rb b/lib/puppet/indirector/module_files.rb index cf5c29cab..7c5cf278f 100644 --- a/lib/puppet/indirector/module_files.rb +++ b/lib/puppet/indirector/module_files.rb @@ -21,7 +21,7 @@ class Puppet::Indirector::ModuleFiles < Puppet::Indirector::Terminus # Make sure our file path starts with /modules, so that we authorize # against the 'modules' mount. - path = uri.path =~ /^\/modules/ ? uri.path : "/modules" + uri.path + path = uri.path =~ /^modules\// ? uri.path : "modules/" + uri.path configuration.authorized?(path, :node => request.node, :ipaddress => request.ip) end @@ -30,7 +30,7 @@ class Puppet::Indirector::ModuleFiles < Puppet::Indirector::Terminus def find(request) return nil unless path = find_path(request) - result = model.new(request.key, :path => path) + result = model.new(path) result.links = request.options[:links] if request.options[:links] return result end @@ -66,9 +66,8 @@ class Puppet::Indirector::ModuleFiles < Puppet::Indirector::Terminus def find_path(request) uri = key2uri(request.key) - # Strip off /modules if it's there -- that's how requests get routed to this terminus. - # Also, strip off the leading slash if present. - module_name, relative_path = uri.path.sub(/^\/modules\b/, '').sub(%r{^/}, '').split(File::Separator, 2) + # Strip off modules/ if it's there -- that's how requests get routed to this terminus. + module_name, relative_path = uri.path.sub(/^modules\//, '').sub(%r{^/}, '').split(File::Separator, 2) # And use the environment to look up the module. return nil unless mod = find_module(module_name, request.node) diff --git a/lib/puppet/indirector/report/rest.rb b/lib/puppet/indirector/report/rest.rb new file mode 100644 index 000000000..905b71a5d --- /dev/null +++ b/lib/puppet/indirector/report/rest.rb @@ -0,0 +1,5 @@ +require 'puppet/indirector/rest' + +class Puppet::Transaction::Report::Rest < Puppet::Indirector::REST + desc "Get server report over HTTP via REST." +end diff --git a/lib/puppet/indirector/request.rb b/lib/puppet/indirector/request.rb index 98fa38885..8227db174 100644 --- a/lib/puppet/indirector/request.rb +++ b/lib/puppet/indirector/request.rb @@ -1,9 +1,12 @@ require 'puppet/indirector' -# Provide any attributes or functionality needed for indirected -# instances. +# This class encapsulates all of the information you need to make an +# Indirection call, and as a a result also handles REST calls. It's somewhat +# analogous to an HTTP Request object, except tuned for our Indirector. class Puppet::Indirector::Request - attr_accessor :indirection_name, :key, :method, :options, :instance, :node, :ip, :authenticated + attr_accessor :indirection_name, :key, :method, :options, :instance, :node, :ip, :authenticated, :ignore_cache, :ignore_terminus + + attr_accessor :server, :port, :uri, :protocol # Is this an authenticated request? def authenticated? @@ -11,6 +14,19 @@ class Puppet::Indirector::Request ! ! authenticated end + # LAK:NOTE This is a messy interface to the cache, and it's only + # used by the Configurer class. I decided it was better to implement + # it now and refactor later, when we have a better design, than + # to spend another month coming up with a design now that might + # not be any better. + def ignore_cache? + ignore_cache + end + + def ignore_terminus? + ignore_terminus + end + def initialize(indirection_name, method, key, options = {}) options ||= {} raise ArgumentError, "Request options must be a hash, not %s" % options.class unless options.is_a?(Hash) @@ -28,7 +44,15 @@ class Puppet::Indirector::Request end if key.is_a?(String) or key.is_a?(Symbol) - @key = key + # If the request key is a URI, then we need to treat it specially, + # because it rewrites the key. We could otherwise strip server/port/etc + # info out in the REST class, but it seemed bad design for the REST + # class to rewrite the key. + if key.to_s =~ /^\w+:\/\// # it's a URI + set_uri_key(key) + else + @key = key + end else @instance = key @key = @instance.name @@ -37,6 +61,51 @@ class Puppet::Indirector::Request # Look up the indirection based on the name provided. def indirection - Puppet::Indirector::Indirection.instance(@indirection_name) + Puppet::Indirector::Indirection.instance(indirection_name) + end + + # Should we allow use of the cached object? + def use_cache? + if defined?(@use_cache) + ! ! use_cache + else + true + end + end + + # Are we trying to interact with multiple resources, or just one? + def plural? + method == :search + end + + private + + # Parse the key as a URI, setting attributes appropriately. + def set_uri_key(key) + @uri = key + begin + uri = URI.parse(URI.escape(key)) + rescue => detail + raise ArgumentError, "Could not understand URL %s: %s" % [source, detail.to_s] + end + + # Just short-circuit these to full paths + if uri.scheme == "file" + @key = uri.path + return + end + + @server = uri.host if uri.host + + # If the URI class can look up the scheme, it will provide a port, + # otherwise it will default to '0'. + if uri.port.to_i == 0 and uri.scheme == "puppet" + @port = Puppet.settings[:masterport].to_i + else + @port = uri.port.to_i + end + + @protocol = uri.scheme + @key = uri.path.sub(/^\//, '') end end diff --git a/lib/puppet/indirector/rest.rb b/lib/puppet/indirector/rest.rb index d33150fc2..5ac25f02d 100644 --- a/lib/puppet/indirector/rest.rb +++ b/lib/puppet/indirector/rest.rb @@ -1,56 +1,97 @@ require 'net/http' require 'uri' +require 'puppet/network/http_pool' + # Access objects via REST class Puppet::Indirector::REST < Puppet::Indirector::Terminus - def rest_connection_details - { :host => Puppet[:server], :port => Puppet[:masterport].to_i } + class << self + attr_reader :server_setting, :port_setting end - def network_fetch(path) - network {|conn| conn.get("/#{path}").body } + # Specify the setting that we should use to get the server name. + def self.use_server_setting(setting) + @server_setting = setting end - - def network_delete(path) - network {|conn| conn.delete("/#{path}").body } + + def self.server + return Puppet.settings[server_setting || :server] end - - def network_put(path, data) - network {|conn| conn.put("/#{path}", data).body } + + # Specify the setting that we should use to get the port. + def self.use_port_setting(setting) + @port_setting = setting end - + + def self.port + return Puppet.settings[port_setting || :masterport].to_i + end + + # Figure out the content type, turn that into a format, and use the format + # to extract the body of the response. + def deserialize(response, multiple = false) + case response.code + when "404" + return nil + when /^2/ + unless response['content-type'] + raise "No content type in http response; cannot parse" + end + + # Convert the response to a deserialized object. + if multiple + model.convert_from_multiple(response['content-type'], response.body) + else + model.convert_from(response['content-type'], response.body) + end + else + # Raise the http error if we didn't get a 'success' of some kind. + message = "Server returned %s: %s" % [response.code, response.message] + raise Net::HTTPError.new(message, response) + end + end + + # Provide appropriate headers. + def headers + {"Accept" => model.supported_formats.join(", ")} + end + + def network(request) + Puppet::Network::HttpPool.http_instance(request.server || self.class.server, request.port || self.class.port) + end + def find(request) - network_result = network_fetch("#{indirection.name}/#{request.key}") - raise YAML.load(network_result) if exception?(network_result) - indirection.model.from_yaml(network_result) + deserialize network(request).get("/#{indirection.name}/#{request.key}#{query_string(request)}", headers) end def search(request) - network_results = network_fetch("#{indirection.name}s/#{request.key}") - raise YAML.load(network_results) if exception?(network_results) - YAML.load(network_results.to_s).collect {|result| indirection.model.from_yaml(result) } + if request.key + path = "/#{indirection.name}s/#{request.key}#{query_string(request)}" + else + path = "/#{indirection.name}s#{query_string(request)}" + end + unless result = deserialize(network(request).get(path, headers), true) + return [] + end + return result end def destroy(request) - network_result = network_delete("#{indirection.name}/#{request.key}") - raise YAML.load(network_result) if exception?(network_result) - YAML.load(network_result.to_s) + raise ArgumentError, "DELETE does not accept options" unless request.options.empty? + deserialize network(request).delete("/#{indirection.name}/#{request.key}", headers) end def save(request) - network_result = network_put("#{indirection.name}/", request.instance.to_yaml) - raise YAML.load(network_result) if exception?(network_result) - indirection.model.from_yaml(network_result) + raise ArgumentError, "PUT does not accept options" unless request.options.empty? + deserialize network(request).put("/#{indirection.name}/", request.instance.render, headers) end - - private - - def network(&block) - Net::HTTP.start(rest_connection_details[:host], rest_connection_details[:port]) {|conn| yield(conn) } - end - - def exception?(yaml_string) - yaml_string =~ %r{--- !ruby/exception} + + private + + # Create the query string, if options are present. + def query_string(request) + return "" unless request.options and ! request.options.empty? + "?" + request.options.collect { |key, value| "%s=%s" % [key, value] }.join("&") end end diff --git a/lib/puppet/indirector/runner/rest.rb b/lib/puppet/indirector/runner/rest.rb new file mode 100644 index 000000000..25d3def3f --- /dev/null +++ b/lib/puppet/indirector/runner/rest.rb @@ -0,0 +1,7 @@ +require 'puppet/agent' +require 'puppet/agent/runner' +require 'puppet/indirector/rest' + +class Puppet::Agent::Runner::Rest < Puppet::Indirector::REST + desc "Trigger Agent runs via REST." +end diff --git a/lib/puppet/indirector/ssl_file.rb b/lib/puppet/indirector/ssl_file.rb new file mode 100644 index 000000000..4119a656f --- /dev/null +++ b/lib/puppet/indirector/ssl_file.rb @@ -0,0 +1,172 @@ +require 'puppet/ssl' + +class Puppet::Indirector::SslFile < Puppet::Indirector::Terminus + # Specify the directory in which multiple files are stored. + def self.store_in(setting) + @directory_setting = setting + end + + # Specify a single file location for storing just one file. + # This is used for things like the CRL. + def self.store_at(setting) + @file_setting = setting + end + + # Specify where a specific ca file should be stored. + def self.store_ca_at(setting) + @ca_setting = setting + end + + class << self + attr_reader :directory_setting, :file_setting, :ca_setting + end + + # The full path to where we should store our files. + def self.collection_directory + return nil unless directory_setting + Puppet.settings[directory_setting] + end + + # The full path to an individual file we would be managing. + def self.file_location + return nil unless file_setting + Puppet.settings[file_setting] + end + + # The full path to a ca file we would be managing. + def self.ca_location + return nil unless ca_setting + Puppet.settings[ca_setting] + end + + # We assume that all files named 'ca' are pointing to individual ca files, + # rather than normal host files. It's a bit hackish, but all the other + # solutions seemed even more hackish. + def ca?(name) + name == Puppet::SSL::Host.ca_name + end + + def initialize + Puppet.settings.use(:main, :ssl) + + (collection_directory || file_location) or raise Puppet::DevError, "No file or directory setting provided; terminus %s cannot function" % self.class.name + end + + # Use a setting to determine our path. + def path(name) + if ca?(name) and ca_location + ca_location + elsif collection_directory + File.join(collection_directory, name.to_s + ".pem") + else + file_location + end + end + + # Remove our file. + def destroy(request) + path = path(request.key) + return false unless FileTest.exist?(path) + + Puppet.notice "Removing file %s %s at '%s'" % [model, request.key, path] + begin + File.unlink(path) + rescue => detail + raise Puppet::Error, "Could not remove %s: %s" % [request.key, detail] + end + end + + # Find the file on disk, returning an instance of the model. + def find(request) + path = path(request.key) + + return nil unless FileTest.exist?(path) or rename_files_with_uppercase(path) + + result = model.new(request.key) + result.read(path) + result + end + + # Save our file to disk. + def save(request) + path = path(request.key) + dir = File.dirname(path) + + raise Puppet::Error.new("Cannot save %s; parent directory %s does not exist" % [request.key, dir]) unless FileTest.directory?(dir) + raise Puppet::Error.new("Cannot save %s; parent directory %s is not writable" % [request.key, dir]) unless FileTest.writable?(dir) + + write(request.key, path) { |f| f.print request.instance.to_s } + end + + # Search for more than one file. At this point, it just returns + # an instance for every file in the directory. + def search(request) + dir = collection_directory + Dir.entries(dir).reject { |file| file !~ /\.pem$/ }.collect do |file| + name = file.sub(/\.pem$/, '') + result = model.new(name) + result.read(File.join(dir, file)) + result + end + end + + private + + # Demeterish pointers to class info. + def collection_directory + self.class.collection_directory + end + + def file_location + self.class.file_location + end + + def ca_location + self.class.ca_location + end + + # A hack method to deal with files that exist with a different case. + # Just renames it; doesn't read it in or anything. + # LAK:NOTE This is a copy of the method in sslcertificates/support.rb, + # which we'll be EOL'ing at some point. This method was added at 20080702 + # and should be removed at some point. + def rename_files_with_uppercase(file) + dir, short = File.split(file) + return nil unless FileTest.exist?(dir) + + raise ArgumentError, "Tried to fix SSL files to a file containing uppercase" unless short.downcase == short + real_file = Dir.entries(dir).reject { |f| f =~ /^\./ }.find do |other| + other.downcase == short + end + + return nil unless real_file + + full_file = File.join(dir, real_file) + + Puppet.notice "Fixing case in %s; renaming to %s" % [full_file, file] + File.rename(full_file, file) + + return true + end + + # Yield a filehandle set up appropriately, either with our settings doing + # the work or opening a filehandle manually. + def write(name, path) + if ca?(name) and ca_location + Puppet.settings.write(self.class.ca_setting) { |f| yield f } + elsif file_location + Puppet.settings.write(self.class.file_setting) { |f| yield f } + else + begin + File.open(path, "w") { |f| yield f } + rescue => detail + raise Puppet::Error, "Could not write %s: %s" % [path, detail] + end + end + end +end + +# LAK:NOTE This has to be at the end, because classes like SSL::Key use this +# class, and this require statement loads those, which results in a load loop +# and lots of failures. +require 'puppet/ssl/host' diff --git a/lib/puppet/indirector/ssl_rsa.rb b/lib/puppet/indirector/ssl_rsa.rb deleted file mode 100644 index 162d8200a..000000000 --- a/lib/puppet/indirector/ssl_rsa.rb +++ /dev/null @@ -1,5 +0,0 @@ -# This is a stub class - -class Puppet::Indirector::SslRsa #:nodoc: -end - diff --git a/lib/puppet/indirector/ssl_rsa/file.rb b/lib/puppet/indirector/ssl_rsa/file.rb deleted file mode 100644 index 435aa8f86..000000000 --- a/lib/puppet/indirector/ssl_rsa/file.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'puppet/indirector/file' -require 'puppet/indirector/ssl_rsa' - -class Puppet::Indirector::SslRsa::File < Puppet::Indirector::File - desc "Store SSL keys on disk." - - def initialize - Puppet.settings.use(:ssl) - end - - def path(name) - if name == :ca - File.join Puppet.settings[:cadir], "ca_key.pem" - else - File.join Puppet.settings[:publickeydir], name.to_s + ".pem" - end - end - - def save(key) - File.open(path(key.name), "w") { |f| f.print key.to_pem } - end - - def find(name) - return nil unless FileTest.exists?(path(name)) - OpenSSL::PKey::RSA.new(File.read(path(name))) - end - - def destroy(name) - return nil unless FileTest.exists?(path(name)) - File.unlink(path(name)) and true - end - -end diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index 7bf35ac18..ab1bc75bd 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -12,9 +12,14 @@ class Puppet::Module dirs = Puppet.settings.value(:modulepath, environment).split(":") if ENV["PUPPETLIB"] dirs = ENV["PUPPETLIB"].split(":") + dirs - else end - dirs.select do |p| + dirs.collect do |dir| + if dir !~ /^#{File::SEPARATOR}/ + File.join(Dir.getwd, dir) + else + dir + end + end.select do |p| p =~ /^#{File::SEPARATOR}/ && File::directory?(p) end end diff --git a/lib/puppet/network/client.rb b/lib/puppet/network/client.rb index 478883959..429e2563f 100644 --- a/lib/puppet/network/client.rb +++ b/lib/puppet/network/client.rb @@ -1,7 +1,6 @@ # the available clients require 'puppet' -require 'puppet/daemon' require 'puppet/network/xmlrpc/client' require 'puppet/util/subclass_loader' require 'puppet/util/methodhelper' @@ -34,7 +33,6 @@ end # provide a different interface. class Puppet::Network::Client Client = self - include Puppet::Daemon include Puppet::Util extend Puppet::Util::SubclassLoader include Puppet::Util::MethodHelper diff --git a/lib/puppet/network/client/ca.rb b/lib/puppet/network/client/ca.rb index a2704e451..5fbdfe9e3 100644 --- a/lib/puppet/network/client/ca.rb +++ b/lib/puppet/network/client/ca.rb @@ -45,7 +45,7 @@ class Puppet::Network::Client::CA < Puppet::Network::Client end unless @cert.check_private_key(key) - raise InvalidCertificate, "Certificate does not match private key. Try 'puppetca --clean %s' on the server." % Facter.value(:fqdn) + raise InvalidCertificate, "Certificate does not match private key. Try 'puppetca --clean %s' on the server." % Puppet[:certname] end # Only write the cert out if it passes validating. diff --git a/lib/puppet/network/client/master.rb b/lib/puppet/network/client/master.rb deleted file mode 100644 index 4b0cfdae3..000000000 --- a/lib/puppet/network/client/master.rb +++ /dev/null @@ -1,524 +0,0 @@ -# The client for interacting with the puppetmaster config server. -require 'sync' -require 'timeout' -require 'puppet/network/http_pool' - -class Puppet::Network::Client::Master < Puppet::Network::Client - unless defined? @@sync - @@sync = Sync.new - end - - attr_accessor :catalog - attr_reader :compile_time - - class << self - # Puppetd should only have one instance running, and we need a way - # to retrieve it. - attr_accessor :instance - include Puppet::Util - end - - def self.facts - # Retrieve the facts from the central server. - if Puppet[:factsync] - self.getfacts() - end - - down = Puppet[:downcasefacts] - - Facter.clear - - # Reload everything. - if Facter.respond_to? :loadfacts - Facter.loadfacts - elsif Facter.respond_to? :load - Facter.load - else - Puppet.warning "You should upgrade your version of Facter to at least 1.3.8" - end - - # This loads all existing facts and any new ones. We have to remove and - # reload because there's no way to unload specific facts. - loadfacts() - facts = Facter.to_hash.inject({}) do |newhash, array| - name, fact = array - if down - newhash[name] = fact.to_s.downcase - else - newhash[name] = fact.to_s - end - newhash - end - - # Add our client version to the list of facts, so people can use it - # in their manifests - facts["clientversion"] = Puppet.version.to_s - - # And add our environment as a fact. - unless facts.include?("environment") - facts["environment"] = Puppet[:environment] - end - - facts - end - - # Return the list of dynamic facts as an array of symbols - # NOTE:LAK(2008/04/10): This code is currently unused, since we now always - # recompile. - def self.dynamic_facts - # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] - x = Puppet.settings[:dynamicfacts].split(/\s*,\s*/).collect { |fact| fact.downcase } - end - - # Cache the config - def cache(text) - Puppet.info "Caching catalog at %s" % self.cachefile - confdir = ::File.dirname(Puppet[:localconfig]) - ::File.open(self.cachefile + ".tmp", "w", 0660) { |f| - f.print text - } - ::File.rename(self.cachefile + ".tmp", self.cachefile) - end - - def cachefile - unless defined? @cachefile - @cachefile = Puppet[:localconfig] + ".yaml" - end - @cachefile - end - - def clear - @catalog.clear(true) if @catalog - Puppet::Type.allclear - @catalog = nil - end - - # Initialize and load storage - def dostorage - begin - Puppet::Util::Storage.load - @compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time] - rescue => detail - if Puppet[:trace] - puts detail.backtrace - end - Puppet.err "Corrupt state file %s: %s" % [Puppet[:statefile], detail] - begin - ::File.unlink(Puppet[:statefile]) - retry - rescue => detail - raise Puppet::Error.new("Cannot remove %s: %s" % - [Puppet[:statefile], detail]) - end - end - end - - # Let the daemon run again, freely in the filesystem. Frolick, little - # daemon! - def enable - lockfile.unlock(:anonymous => true) - end - - # Stop the daemon from making any catalog runs. - def disable - lockfile.lock(:anonymous => true) - end - - # Retrieve the config from a remote server. If this fails, then - # use the cached copy. - def getconfig - dostorage() - - # Retrieve the plugins. - getplugins() if Puppet[:pluginsync] - - facts = nil - Puppet::Util.benchmark(:debug, "Retrieved facts") do - facts = self.class.facts - end - - raise Puppet::Network::ClientError.new("Could not retrieve any facts") unless facts.length > 0 - - Puppet.debug("Retrieving catalog") - - # If we can't retrieve the catalog, just return, which will either - # fail, or use the in-memory catalog. - unless marshalled_objects = get_actual_config(facts) - use_cached_config(true) - return - end - - begin - case Puppet[:catalog_format] - when "marshal": objects = Marshal.load(marshalled_objects) - when "yaml": objects = YAML.load(marshalled_objects) - else - raise "Invalid catalog format '%s'" % Puppet[:catalog_format] - end - rescue => detail - msg = "Configuration could not be translated from %s" % Puppet[:catalog_format] - msg += "; using cached catalog" if use_cached_config(true) - Puppet.warning msg - return - end - - self.setclasses(objects.classes) - - # Clear all existing objects, so we can recreate our stack. - clear() if self.catalog - - # Now convert the objects to a puppet catalog graph. - begin - @catalog = objects.to_catalog - rescue => detail - clear() - puts detail.backtrace if Puppet[:trace] - msg = "Configuration could not be instantiated: %s" % detail - msg += "; using cached catalog" if use_cached_config(true) - Puppet.warning msg - return - end - - if ! @catalog.from_cache - self.cache(marshalled_objects) - end - - # Keep the state database up to date. - @catalog.host_config = true - end - - # A simple proxy method, so it's easy to test. - def getplugins - self.class.getplugins - end - - # Just so we can specify that we are "the" instance. - def initialize(*args) - Puppet.settings.use(:main, :ssl, :puppetd) - super - - self.class.instance = self - @running = false - @splayed = false - end - - # Mark that we should restart. The Puppet module checks whether we're running, - # so this only gets called if we're in the middle of a run. - def restart - # If we're currently running, then just mark for later - Puppet.notice "Received signal to restart; waiting until run is complete" - @restart = true - end - - # Should we restart? - def restart? - if defined? @restart - @restart - else - false - end - end - - # Retrieve the cached config - def retrievecache - if FileTest.exists?(self.cachefile) - return ::File.read(self.cachefile) - else - return nil - end - end - - # The code that actually runs the catalog. - # This just passes any options on to the catalog, - # which accepts :tags and :ignoreschedules. - def run(options = {}) - got_lock = false - splay - Puppet::Util.sync(:puppetrun).synchronize(Sync::EX) do - if !lockfile.lock - Puppet.notice "Lock file %s exists; skipping catalog run" % - lockfile.lockfile - else - got_lock = true - begin - duration = thinmark do - self.getconfig - end - rescue => detail - puts detail.backtrace if Puppet[:trace] - Puppet.err "Could not retrieve catalog: %s" % detail - end - - if self.catalog - @catalog.retrieval_duration = duration - Puppet.notice "Starting catalog run" unless @local - benchmark(:notice, "Finished catalog run") do - @catalog.apply(options) - end - end - - # Now close all of our existing http connections, since there's no - # reason to leave them lying open. - Puppet::Network::HttpPool.clear_http_instances - end - - lockfile.unlock - - # Did we get HUPped during the run? If so, then restart now that we're - # done with the run. - if self.restart? - Process.kill(:HUP, $$) - end - end - ensure - # Just make sure we remove the lock file if we set it. - lockfile.unlock if got_lock and lockfile.locked? - clear() - end - - def running? - lockfile.locked? - end - - # Store the classes in the classfile, but only if we're not local. - def setclasses(ary) - if @local - return - end - unless ary and ary.length > 0 - Puppet.info "No classes to store" - return - end - begin - ::File.open(Puppet[:classfile], "w") { |f| - f.puts ary.join("\n") - } - rescue => detail - Puppet.err "Could not create class file %s: %s" % - [Puppet[:classfile], detail] - end - end - - private - - # Download files from the remote server, returning a list of all - # changed files. - def self.download(args) - hash = { - :path => args[:dest], - :recurse => true, - :source => args[:source], - :tag => "#{args[:name]}s", - :owner => Process.uid, - :group => Process.gid, - :purge => true, - :force => true, - :backup => false, - :noop => false - } - - if args[:ignore] - hash[:ignore] = args[:ignore].split(/\s+/) - end - downconfig = Puppet::Node::Catalog.new("downloading") - downconfig.add_resource Puppet::Type.type(:file).create(hash) - - Puppet.info "Retrieving #{args[:name]}s" - - files = [] - begin - Timeout::timeout(self.timeout) do - downconfig.apply do |trans| - trans.changed?.find_all do |resource| - yield resource if block_given? - files << resource[:path] - end - end - end - rescue Puppet::Error, Timeout::Error => detail - if Puppet[:debug] - puts detail.backtrace - end - Puppet.err "Could not retrieve %ss: %s" % [args[:name], detail] - end - - # Now clean up after ourselves - downconfig.clear - - return files - end - - # Retrieve facts from the central server. - def self.getfacts - # Download the new facts - path = Puppet[:factpath].split(":") - files = [] - download(:dest => Puppet[:factdest], :source => Puppet[:factsource], - :ignore => Puppet[:factsignore], :name => "fact") do |resource| - - next unless path.include?(::File.dirname(resource[:path])) - - files << resource[:path] - end - end - - # Retrieve the plugins from the central server. We only have to load the - # changed plugins, because Puppet::Type loads plugins on demand. - def self.getplugins - download(:dest => Puppet[:plugindest], :source => Puppet[:pluginsource], - :ignore => Puppet[:pluginsignore], :name => "plugin") do |resource| - - next if FileTest.directory?(resource[:path]) - path = resource[:path].sub(Puppet[:plugindest], '').sub(/^\/+/, '') - unless Puppet::Util::Autoload.loaded?(path) - next - end - - begin - Puppet.info "Reloading downloaded file %s" % path - load resource[:path] - rescue => detail - Puppet.warning "Could not reload downloaded file %s: %s" % - [resource[:path], detail] - end - end - end - - def self.loaddir(dir, type) - return unless FileTest.directory?(dir) - - Dir.entries(dir).find_all { |e| e =~ /\.rb$/ }.each do |file| - fqfile = ::File.join(dir, file) - begin - Puppet.info "Loading %s %s" % - [type, ::File.basename(file.sub(".rb",''))] - Timeout::timeout(self.timeout) do - load fqfile - end - rescue => detail - Puppet.warning "Could not load %s %s: %s" % [type, fqfile, detail] - end - end - end - - def self.loadfacts - # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] - x = Puppet[:factpath].split(":").each do |dir| - loaddir(dir, "fact") - end - end - - def self.timeout - timeout = Puppet[:configtimeout] - case timeout - when String: - if timeout =~ /^\d+$/ - timeout = Integer(timeout) - else - raise ArgumentError, "Configuration timeout must be an integer" - end - when Integer: # nothing - else - raise ArgumentError, "Configuration timeout must be an integer" - end - - return timeout - end - - loadfacts() - - # Actually retrieve the catalog, either from the server or from a - # local master. - def get_actual_config(facts) - begin - Timeout::timeout(self.class.timeout) do - return get_remote_config(facts) - end - rescue Timeout::Error - Puppet.err "Configuration retrieval timed out" - return nil - end - end - - # Retrieve a config from a remote master. - def get_remote_config(facts) - textobjects = "" - - textfacts = CGI.escape(YAML.dump(facts)) - - benchmark(:debug, "Retrieved catalog") do - # error handling for this is done in the network client - begin - textobjects = @driver.getconfig(textfacts, Puppet[:catalog_format]) - begin - textobjects = CGI.unescape(textobjects) - rescue => detail - raise Puppet::Error, "Could not CGI.unescape catalog" - end - - rescue => detail - Puppet.err "Could not retrieve catalog: %s" % detail - return nil - end - end - - return nil if textobjects == "" - - @compile_time = Time.now - Puppet::Util::Storage.cache(:configuration)[:facts] = facts - Puppet::Util::Storage.cache(:configuration)[:compile_time] = @compile_time - - return textobjects - end - - def lockfile - unless defined?(@lockfile) - @lockfile = Puppet::Util::Pidlock.new(Puppet[:puppetdlockfile]) - end - - @lockfile - end - - def splayed? - @splayed - end - - # Sleep when splay is enabled; else just return. - def splay - return unless Puppet[:splay] - return if splayed? - - time = rand(Integer(Puppet[:splaylimit]) + 1) - Puppet.info "Sleeping for %s seconds (splay is enabled)" % time - sleep(time) - @splayed = true - end - - private - - # Use our cached config, optionally specifying whether this is - # necessary because of a failure. - def use_cached_config(because_of_failure = false) - return true if self.catalog - - if because_of_failure and ! Puppet[:usecacheonfailure] - @catalog = nil - Puppet.warning "Not using cache on failed catalog" - return false - end - - return false unless oldtext = self.retrievecache - - begin - @catalog = YAML.load(oldtext).to_catalog - @catalog.from_cache = true - @catalog.host_config = true - rescue => detail - puts detail.backtrace if Puppet[:trace] - Puppet.warning "Could not load cached catalog: %s" % detail - clear - return false - end - return true - end -end diff --git a/lib/puppet/network/format.rb b/lib/puppet/network/format.rb new file mode 100644 index 000000000..21aead7cc --- /dev/null +++ b/lib/puppet/network/format.rb @@ -0,0 +1,79 @@ +require 'puppet/provider/confiner' + +# A simple class for modeling encoding formats for moving +# instances around the network. +class Puppet::Network::Format + include Puppet::Provider::Confiner + + attr_reader :name, :mime, :weight + + def initialize(name, options = {}, &block) + @name = name.to_s.downcase.intern + + if mime = options[:mime] + self.mime = mime + options.delete(:mime) + else + self.mime = "text/%s" % name + end + + if weight = options[:weight] + @weight = weight + options.delete(:weight) + else + @weight = 5 + end + + unless options.empty? + raise ArgumentError, "Unsupported option(s) %s" % options.keys + end + + instance_eval(&block) if block_given? + + @intern_method = "from_%s" % name + @render_method = "to_%s" % name + @intern_multiple_method = "from_multiple_%s" % name + @render_multiple_method = "to_multiple_%s" % name + end + + def intern(klass, text) + return klass.send(intern_method, text) if klass.respond_to?(intern_method) + raise NotImplementedError, "%s can not intern instances from %s" % [klass, mime] + end + + def intern_multiple(klass, text) + return klass.send(intern_multiple_method, text) if klass.respond_to?(intern_multiple_method) + raise NotImplementedError, "%s can not intern multiple instances from %s" % [klass, mime] + end + + def mime=(mime) + @mime = mime.to_s.downcase + end + + def render(instance) + return instance.send(render_method) if instance.respond_to?(render_method) + raise NotImplementedError, "%s can not render instances to %s" % [instance.class, mime] + end + + def render_multiple(instances) + # This method implicitly assumes that all instances are of the same type. + return instances[0].class.send(render_multiple_method, instances) if instances[0].class.respond_to?(render_multiple_method) + raise NotImplementedError, "%s can not intern multiple instances to %s" % [instances[0].class, mime] + end + + def supported?(klass) + suitable? and + klass.respond_to?(intern_method) and + klass.respond_to?(intern_multiple_method) and + klass.respond_to?(render_multiple_method) and + klass.instance_methods.include?(render_method) + end + + def to_s + "Puppet::Network::Format[%s]" % name + end + + private + + attr_reader :intern_method, :render_method, :intern_multiple_method, :render_multiple_method +end diff --git a/lib/puppet/network/format_handler.rb b/lib/puppet/network/format_handler.rb new file mode 100644 index 000000000..efeea79e3 --- /dev/null +++ b/lib/puppet/network/format_handler.rb @@ -0,0 +1,119 @@ +require 'yaml' +require 'puppet/network' +require 'puppet/network/format' + +module Puppet::Network::FormatHandler + class FormatError < Puppet::Error; end + + class FormatProtector + attr_reader :format + + def protect(method, args) + begin + Puppet::Network::FormatHandler.format(format).send(method, *args) + rescue => details + direction = method.to_s.include?("intern") ? "from" : "to" + error = FormatError.new("Could not %s %s %s: %s" % [method, direction, format, details]) + error.set_backtrace(details.backtrace) + raise error + end + end + + def initialize(format) + @format = format + end + + [:intern, :intern_multiple, :render, :render_multiple].each do |method| + define_method(method) do |*args| + protect(method, args) + end + end + end + + @formats = {} + def self.create(*args, &block) + instance = Puppet::Network::Format.new(*args) + instance.instance_eval(&block) if block_given? + + @formats[instance.name] = instance + instance + end + + def self.extended(klass) + klass.extend(ClassMethods) + + # LAK:NOTE This won't work in 1.9 ('send' won't be able to send + # private methods, but I don't know how else to do it. + klass.send(:include, InstanceMethods) + end + + def self.format(name) + @formats[name.to_s.downcase.intern] + end + + # Provide a list of all formats. + def self.formats + @formats.keys + end + + # Return a format capable of handling the provided mime type. + def self.mime(mimetype) + mimetype = mimetype.to_s.downcase + @formats.values.find { |format| format.mime == mimetype } + end + + # Use a delegator to make sure any exceptions generated by our formats are + # handled intelligently. + def self.protected_format(name) + @format_protectors ||= {} + @format_protectors[name] ||= FormatProtector.new(name) + @format_protectors[name] + end + + module ClassMethods + def format_handler + Puppet::Network::FormatHandler + end + + def convert_from(format, data) + format_handler.protected_format(format).intern(self, data) + end + + def convert_from_multiple(format, data) + format_handler.protected_format(format).intern_multiple(self, data) + end + + def render_multiple(format, instances) + format_handler.protected_format(format).render_multiple(instances) + end + + def default_format + supported_formats[0] + end + + def support_format?(name) + Puppet::Network::FormatHandler.format(name).supported?(self) + end + + def supported_formats + format_handler.formats.collect { |f| format_handler.format(f) }.find_all { |f| f.supported?(self) }.collect { |f| f.name }.sort do |a, b| + # It's an inverse sort -- higher weight formats go first. + format_handler.format(b).weight <=> format_handler.format(a).weight + end + end + end + + module InstanceMethods + def render(format = nil) + format ||= self.class.default_format + + Puppet::Network::FormatHandler.protected_format(format).render(self) + end + + def support_format?(name) + self.class.support_format?(name) + end + end +end + +require 'puppet/network/formats' diff --git a/lib/puppet/network/formats.rb b/lib/puppet/network/formats.rb new file mode 100644 index 000000000..85e8ce6f8 --- /dev/null +++ b/lib/puppet/network/formats.rb @@ -0,0 +1,77 @@ +require 'puppet/network/format_handler' + +Puppet::Network::FormatHandler.create(:yaml, :mime => "text/yaml") do + # Yaml doesn't need the class name; it's serialized. + def intern(klass, text) + YAML.load(text) + end + + # Yaml doesn't need the class name; it's serialized. + def intern_multiple(klass, text) + YAML.load(text) + end + + def render(instance) + instance.to_yaml + end + + # Yaml monkey-patches Array, so this works. + def render_multiple(instances) + instances.to_yaml + end + + # Everything's supported + def supported?(klass) + true + end +end + + +Puppet::Network::FormatHandler.create(:marshal, :mime => "text/marshal") do + # Marshal doesn't need the class name; it's serialized. + def intern(klass, text) + Marshal.load(text) + end + + # Marshal doesn't need the class name; it's serialized. + def intern_multiple(klass, text) + Marshal.load(text) + end + + def render(instance) + Marshal.dump(instance) + end + + # Marshal monkey-patches Array, so this works. + def render_multiple(instances) + Marshal.dump(instances) + end + + # Everything's supported + def supported?(klass) + true + end +end + +Puppet::Network::FormatHandler.create(:s, :mime => "text/plain") + +# A very low-weight format so it'll never get chosen automatically. +Puppet::Network::FormatHandler.create(:raw, :mime => "application/x-raw", :weight => 1) do + def intern_multiple(klass, text) + raise NotImplementedError + end + + def render_multiple(instances) + raise NotImplementedError + end + + # LAK:NOTE The format system isn't currently flexible enough to handle + # what I need to support raw formats just for individual instances (rather + # than both individual and collections), but we don't yet have enough data + # to make a "correct" design. + # So, we hack it so it works for singular but fail if someone tries it + # on plurals. + def supported?(klass) + true + end +end diff --git a/lib/puppet/network/handler/fileserver.rb b/lib/puppet/network/handler/fileserver.rb index 815d0ba82..4e00b605f 100755 --- a/lib/puppet/network/handler/fileserver.rb +++ b/lib/puppet/network/handler/fileserver.rb @@ -77,7 +77,7 @@ class Puppet::Network::Handler return "" unless metadata.exist? begin - metadata.collect_attributes + metadata.collect rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err detail @@ -498,22 +498,25 @@ class Puppet::Network::Handler @path = nil end + @files = {} + super() end def fileobj(path, links, client) obj = nil - if obj = Puppet.type(:file)[file_path(path, client)] + if obj = @files[file_path(path, client)] # This can only happen in local fileserving, but it's an # important one. It'd be nice if we didn't just set # the check params every time, but I'm not sure it's worth # the effort. obj[:check] = CHECKPARAMS else - obj = Puppet.type(:file).create( + obj = Puppet::Type.type(:file).new( :name => file_path(path, client), :check => CHECKPARAMS ) + @files[file_path(path, client)] = obj end if links == :manage @@ -530,7 +533,7 @@ class Puppet::Network::Handler # Read the contents of the file at the relative path given. def read_file(relpath, client) - File.read(file_path(relpath, client)) + File.read(file_path(relpath, client)) end # Cache this manufactured map, since if it's used it's likely diff --git a/lib/puppet/network/handler/master.rb b/lib/puppet/network/handler/master.rb index 71b633a09..7bde0af73 100644 --- a/lib/puppet/network/handler/master.rb +++ b/lib/puppet/network/handler/master.rb @@ -62,7 +62,7 @@ class Puppet::Network::Handler # Pass the facts to the fact handler Puppet::Node::Facts.new(client, facts).save unless local? - catalog = Puppet::Node::Catalog.find(client) + catalog = Puppet::Resource::Catalog.find(client) case format when "yaml": diff --git a/lib/puppet/network/handler/resource.rb b/lib/puppet/network/handler/resource.rb index f2a339751..e7ecbbdf2 100755 --- a/lib/puppet/network/handler/resource.rb +++ b/lib/puppet/network/handler/resource.rb @@ -59,7 +59,7 @@ class Puppet::Network::Handler Puppet.info "Describing %s[%s]" % [type.to_s.capitalize, name] @local = true unless client typeklass = nil - unless typeklass = Puppet.type(type) + unless typeklass = Puppet::Type.type(type) raise Puppet::Error, "Puppet type %s is unsupported" % type end @@ -68,15 +68,11 @@ class Puppet::Network::Handler retrieve ||= :all ignore ||= [] - if obj = typeklass[name] - obj[:check] = retrieve - else - begin - obj = typeklass.create(:name => name, :check => retrieve) - rescue Puppet::Error => detail - raise Puppet::Error, "%s[%s] could not be created: %s" % - [type, name, detail] - end + begin + obj = typeklass.create(:name => name, :check => retrieve) + rescue Puppet::Error => detail + raise Puppet::Error, "%s[%s] could not be created: %s" % + [type, name, detail] end unless obj @@ -128,7 +124,7 @@ class Puppet::Network::Handler def list(type, ignore = [], base = nil, format = "yaml", client = nil, clientip = nil) @local = true unless client typeklass = nil - unless typeklass = Puppet.type(type) + unless typeklass = Puppet::Type.type(type) raise Puppet::Error, "Puppet type %s is unsupported" % type end diff --git a/lib/puppet/network/handler/runner.rb b/lib/puppet/network/handler/runner.rb index c97e4791a..070cae114 100755 --- a/lib/puppet/network/handler/runner.rb +++ b/lib/puppet/network/handler/runner.rb @@ -1,3 +1,5 @@ +require 'puppet/agent/runner' + class Puppet::Network::Handler class MissingMasterError < RuntimeError; end # Cannot find the master client # A simple server for triggering a new run on a Puppet client. @@ -13,51 +15,16 @@ class Puppet::Network::Handler # Run the client configuration right now, optionally specifying # tags and whether to ignore schedules def run(tags = nil, ignoreschedules = false, fg = true, client = nil, clientip = nil) - # We need to retrieve the client - master = Puppet::Network::Client.client(:Master).instance - - unless master - raise MissingMasterError, "Could not find the master client" - end - - if Puppet::Util::Pidlock.new(Puppet[:puppetdlockfile]).locked? - Puppet.notice "Could not trigger run; already running" - return "running" - end - - if tags == "" - tags = nil - end - - if ignoreschedules == "" - ignoreschedules == nil - end - - msg = "" - if client - msg = "%s(%s) " % [client, clientip] - end - msg += "triggered run" % - if tags - msg += " with tags %s" % tags - end - - if ignoreschedules - msg += " ignoring schedules" - end + options = {} + options[:tags] = tags if tags + options[:ignoreschedules] = ignoreschedules if ignoreschedules + options[:background] = !fg - Puppet.notice msg + runner = Puppet::Agent::Runner.new(options) - # And then we need to tell it to run, with this extra info. - if fg - master.run(:tags => tags, :ignoreschedules => ignoreschedules) - else - Puppet.newthread do - master.run(:tags => tags, :ignoreschedules => ignoreschedules) - end - end + runner.run - return "success" + return runner.status end end end diff --git a/lib/puppet/network/http.rb b/lib/puppet/network/http.rb index c219859b6..3b81d38b5 100644 --- a/lib/puppet/network/http.rb +++ b/lib/puppet/network/http.rb @@ -1,4 +1,4 @@ -class Puppet::Network::HTTP +module Puppet::Network::HTTP def self.server_class_by_type(kind) case kind.to_sym when :webrick: diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index 3c14c8a40..7c7abccf5 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -1,4 +1,30 @@ +module Puppet::Network::HTTP +end + module Puppet::Network::HTTP::Handler + attr_reader :model, :server, :handler + + # Retrieve the accept header from the http request. + def accept_header(request) + raise NotImplementedError + end + + # Which format to use when serializing our response. Just picks + # the first value in the accept header, at this point. + def format_to_use(request) + unless header = accept_header(request) + raise ArgumentError, "An Accept header must be provided to pick the right format" + end + + format = nil + header.split(/,\s*/).each do |name| + next unless format = Puppet::Network::FormatHandler.format(name) + next unless format.suitable? + return name + end + + raise "No specified acceptable formats (%s) are functional on this machine" % header + end def initialize_for_puppet(args = {}) raise ArgumentError unless @server = args[:server] @@ -14,54 +40,108 @@ module Puppet::Network::HTTP::Handler return do_save(request, response) if put?(request) and singular?(request) raise ArgumentError, "Did not understand HTTP #{http_method(request)} request for '#{path(request)}'" rescue Exception => e - return do_exception(request, response, e) + return do_exception(response, e) end - private + # Are we interacting with a singular instance? + def singular?(request) + %r{/#{handler.to_s}$}.match(path(request)) + end - def model - @model + # Are we interacting with multiple instances? + def plural?(request) + %r{/#{handler.to_s}s$}.match(path(request)) + end + + # Set the response up, with the body and status. + def set_response(response, body, status = 200) + raise NotImplementedError + end + + # Set the specified format as the content type of the response. + def set_content_type(response, format) + raise NotImplementedError + end + + def do_exception(response, exception, status=400) + if exception.is_a?(Exception) + puts exception.backtrace if Puppet[:trace] + puts exception if Puppet[:trace] + end + set_content_type(response, "text/plain") + set_response(response, exception.to_s, status) end + # Execute our find. def do_find(request, response) key = request_key(request) || raise(ArgumentError, "Could not locate lookup key in request path [#{path(request)}]") args = params(request) - result = model.find(key, args).to_yaml - encode_result(request, response, result) + unless result = model.find(key, args) + return do_exception(response, "Could not find %s %s" % [model.name, key], 404) + end + + # The encoding of the result must include the format to use, + # and it needs to be used for both the rendering and as + # the content type. + format = format_to_use(request) + set_content_type(response, format) + + set_response(response, result.render(format)) end + # Execute our search. def do_search(request, response) args = params(request) - result = model.search(args).collect {|result| result.to_yaml }.to_yaml - encode_result(request, response, result) + if key = request_key(request) + result = model.search(key, args) + else + result = model.search(args) + end + if result.nil? or (result.is_a?(Array) and result.empty?) + return do_exception(response, "Could not find instances in %s with '%s'" % [model.name, args.inspect], 404) + end + + format = format_to_use(request) + set_content_type(response, format) + + set_response(response, model.render_multiple(format, result)) end + # Execute our destroy. def do_destroy(request, response) key = request_key(request) || raise(ArgumentError, "Could not locate lookup key in request path [#{path(request)}]") args = params(request) result = model.destroy(key, args) - encode_result(request, response, YAML.dump(result)) + + set_content_type(response, "yaml") + + set_response(response, result.to_yaml) end + # Execute our save. def do_save(request, response) data = body(request).to_s raise ArgumentError, "No data to save" if !data or data.empty? args = params(request) - obj = model.from_yaml(data) - result = save_object(obj, args).to_yaml - encode_result(request, response, result) + + format = format_to_use(request) + + obj = model.convert_from(format_to_use(request), data) + result = save_object(obj, args) + + set_content_type(response, "yaml") + + set_response(response, result.to_yaml) end + private + # LAK:NOTE This has to be here for testing; it's a stub-point so # we keep infinite recursion from happening. def save_object(object, args) object.save(args) end - def do_exception(request, response, exception, status=404) - encode_result(request, response, exception.to_yaml, status) - end - def find_model_for_handler(handler) Puppet::Indirector::Indirection.model(handler) || raise(ArgumentError, "Cannot locate indirection [#{handler}].") @@ -79,20 +159,8 @@ module Puppet::Network::HTTP::Handler http_method(request) == 'DELETE' end - def singular?(request) - %r{/#{@handler.to_s}$}.match(path(request)) - end - - def plural?(request) - %r{/#{@handler.to_s}s$}.match(path(request)) - end - # methods to be overridden by the including web server class - def register_handler - raise NotImplementedError - end - def http_method(request) raise NotImplementedError end @@ -112,8 +180,4 @@ module Puppet::Network::HTTP::Handler def params(request) raise NotImplementedError end - - def encode_result(request, response, result, status = 200) - raise NotImplementedError - end end diff --git a/lib/puppet/network/http/mongrel.rb b/lib/puppet/network/http/mongrel.rb index 9a4531c7a..847781cf2 100644 --- a/lib/puppet/network/http/mongrel.rb +++ b/lib/puppet/network/http/mongrel.rb @@ -16,6 +16,7 @@ class Puppet::Network::HTTP::Mongrel @protocols = args[:protocols] @handlers = args[:handlers] + @xmlrpc_handlers = args[:xmlrpc_handlers] @server = Mongrel::HttpServer.new(args[:address], args[:port]) setup_handlers @@ -38,12 +39,22 @@ class Puppet::Network::HTTP::Mongrel def setup_handlers @protocols.each do |protocol| + next if protocol == :xmlrpc klass = class_for_protocol(protocol) @handlers.each do |handler| @server.register('/' + handler.to_s, klass.new(:server => @server, :handler => handler)) @server.register('/' + handler.to_s + 's', klass.new(:server => @server, :handler => handler)) end end + + if @protocols.include?(:xmlrpc) and ! @xmlrpc_handlers.empty? + setup_xmlrpc_handlers + end + end + + # Use our existing code to provide the xmlrpc backward compatibility. + def setup_xmlrpc_handlers + @server.register('/RPC2', Puppet::Network::HTTPServer::Mongrel.new(@xmlrpc_handlers)) end def class_for_protocol(protocol) diff --git a/lib/puppet/network/http/mongrel/rest.rb b/lib/puppet/network/http/mongrel/rest.rb index 520ad67f0..45d21ea62 100644 --- a/lib/puppet/network/http/mongrel/rest.rb +++ b/lib/puppet/network/http/mongrel/rest.rb @@ -4,24 +4,28 @@ class Puppet::Network::HTTP::MongrelREST < Mongrel::HttpHandler include Puppet::Network::HTTP::Handler + ACCEPT_HEADER = "HTTP_ACCEPT".freeze # yay, zed's a crazy-man + def initialize(args={}) super() initialize_for_puppet(args) end - # Return the query params for this request. We had to expose this method for - # testing purposes. - def params(request) - Mongrel::HttpRequest.query_parse(request.params["QUERY_STRING"]).merge(client_info(request)) + def accept_header(request) + request.params[ACCEPT_HEADER] end - private - # which HTTP verb was used in this request def http_method(request) request.params[Mongrel::Const::REQUEST_METHOD] end + # Return the query params for this request. We had to expose this method for + # testing purposes. + def params(request) + Mongrel::HttpRequest.query_parse(request.params["QUERY_STRING"]).merge(client_info(request)) + end + # what path was requested? def path(request) # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] @@ -31,7 +35,7 @@ class Puppet::Network::HTTP::MongrelREST < Mongrel::HttpHandler # return the key included in the request path def request_key(request) # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] - x = request.params[Mongrel::Const::REQUEST_PATH].split('/')[2] + x = request.params[Mongrel::Const::REQUEST_PATH].split('/', 3)[2] end # return the request body @@ -39,9 +43,21 @@ class Puppet::Network::HTTP::MongrelREST < Mongrel::HttpHandler request.body end + def set_content_type(response, format) + response.header['Content-Type'] = format + end + # produce the body of the response - def encode_result(request, response, result, status = 200) - response.start(status) do |head, body| + def set_response(response, result, status = 200) + args = [status] + + # Set the 'reason' (or 'message', as it's called in Webrick), when + # we have a failure. + if status >= 300 + args << false << result + end + + response.start(*args) do |head, body| body.write(result) end end diff --git a/lib/puppet/network/http/webrick.rb b/lib/puppet/network/http/webrick.rb index 3a37e2071..972ebc2e2 100644 --- a/lib/puppet/network/http/webrick.rb +++ b/lib/puppet/network/http/webrick.rb @@ -1,8 +1,12 @@ require 'webrick' require 'webrick/https' require 'puppet/network/http/webrick/rest' +require 'puppet/network/xmlrpc/webrick_servlet' require 'thread' +require 'puppet/ssl/certificate' +require 'puppet/ssl/certificate_revocation_list' + class Puppet::Network::HTTP::WEBrick def initialize(args = {}) @listening = false @@ -22,7 +26,14 @@ class Puppet::Network::HTTP::WEBrick @protocols = args[:protocols] @handlers = args[:handlers] - @server = WEBrick::HTTPServer.new(:BindAddress => args[:address], :Port => args[:port]) + @xmlrpc_handlers = args[:xmlrpc_handlers] + + arguments = {:BindAddress => args[:address], :Port => args[:port]} + arguments.merge!(setup_logger) + arguments.merge!(setup_ssl) + + @server = WEBrick::HTTPServer.new(arguments) + setup_handlers @mutex.synchronize do @@ -48,15 +59,85 @@ class Puppet::Network::HTTP::WEBrick end end + # Configure our http log file. + def setup_logger + # Make sure the settings are all ready for us. + Puppet.settings.use(:main, :ssl, Puppet[:name]) + + if Puppet[:name] == "puppetmasterd" + file = Puppet[:masterhttplog] + else + file = Puppet[:httplog] + end + + # open the log manually to prevent file descriptor leak + file_io = ::File.open(file, "a+") + file_io.sync + file_io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) + + args = [file_io] + args << WEBrick::Log::DEBUG if Puppet::Util::Log.level == :debug + + logger = WEBrick::Log.new(*args) + return :Logger => logger, :AccessLog => [ + [logger, WEBrick::AccessLog::COMMON_LOG_FORMAT ], + [logger, WEBrick::AccessLog::REFERER_LOG_FORMAT ] + ] + end + + # Add all of the ssl cert information. + def setup_ssl + results = {} + + # Get the cached copy. We know it's been generated, too. + host = Puppet::SSL::Host.localhost + + raise Puppet::Error, "Could not retrieve certificate for %s and not running on a valid certificate authority" % host.name unless host.certificate + + results[:SSLPrivateKey] = host.key.content + results[:SSLCertificate] = host.certificate.content + results[:SSLStartImmediately] = true + results[:SSLEnable] = true + + unless Puppet::SSL::Certificate.find("ca") + raise Puppet::Error, "Could not find CA certificate" + end + + results[:SSLCACertificateFile] = Puppet[:localcacert] + results[:SSLVerifyClient] = OpenSSL::SSL::VERIFY_PEER + + results[:SSLCertificateStore] = host.ssl_store + + results + end + private def setup_handlers + # Set up the new-style protocols. @protocols.each do |protocol| + next if protocol == :xmlrpc klass = self.class.class_for_protocol(protocol) @handlers.each do |handler| @server.mount('/' + handler.to_s, klass, handler) @server.mount('/' + handler.to_s + 's', klass, handler) end end + + # And then set up xmlrpc, if configured. + if @protocols.include?(:xmlrpc) and ! @xmlrpc_handlers.empty? + @server.mount("/RPC2", xmlrpc_servlet) + end + end + + # Create our xmlrpc servlet, which provides backward compatibility. + def xmlrpc_servlet + handlers = @xmlrpc_handlers.collect { |handler| + unless hclass = Puppet::Network::Handler.handler(handler) + raise "Invalid xmlrpc handler %s" % handler + end + hclass.new({}) + } + Puppet::Network::XMLRPC::WEBrickServlet.new handlers end end diff --git a/lib/puppet/network/http/webrick/rest.rb b/lib/puppet/network/http/webrick/rest.rb index a235fb4f3..f06914365 100644 --- a/lib/puppet/network/http/webrick/rest.rb +++ b/lib/puppet/network/http/webrick/rest.rb @@ -10,7 +10,7 @@ class Puppet::Network::HTTP::WEBrickREST < WEBrick::HTTPServlet::AbstractServlet initialize_for_puppet(:server => server, :handler => handler) end - # We had to expose this method for testing purposes. + # Retrieve the request parameters, including authentication information. def params(request) result = request.query result.merge(client_information(request)) @@ -21,7 +21,9 @@ class Puppet::Network::HTTP::WEBrickREST < WEBrick::HTTPServlet::AbstractServlet process(request, response) end - private + def accept_header(request) + request["accept"] + end def http_method(request) request.request_method @@ -34,16 +36,25 @@ class Puppet::Network::HTTP::WEBrickREST < WEBrick::HTTPServlet::AbstractServlet def request_key(request) # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] - x = request.path.split('/')[2] + x = request.path.split('/', 3)[2] end def body(request) request.body end - def encode_result(request, response, result, status = 200) + # Set the specified format as the content type of the response. + def set_content_type(response, format) + response["content-type"] = format + end + + def set_response(response, result, status = 200) response.status = status - response.body = result + if status >= 200 and status < 300 + response.body = result + else + response.reason_phrase = result + end end # Retrieve node/cert/ip information from the request object. diff --git a/lib/puppet/network/http_pool.rb b/lib/puppet/network/http_pool.rb index 1227f78dc..9430457bb 100644 --- a/lib/puppet/network/http_pool.rb +++ b/lib/puppet/network/http_pool.rb @@ -1,11 +1,24 @@ -require 'puppet/sslcertificates/support' +require 'puppet/ssl/host' require 'net/https' +require 'puppet/util/cacher' -module Puppet::Network -end +module Puppet::Network; end # Manage Net::HTTP instances for keep-alive. module Puppet::Network::HttpPool + class << self + include Puppet::Util::Cacher + + private + + cached_attr(:http_cache) { Hash.new } + end + + # Use the global localhost instance. + def self.ssl_host + Puppet::SSL::Host.localhost + end + # 2008/03/23 # LAK:WARNING: Enabling this has a high propability of # causing corrupt files and who knows what else. See #1010. @@ -15,18 +28,12 @@ module Puppet::Network::HttpPool HTTP_KEEP_ALIVE end - # This handles reading in the key and such-like. - extend Puppet::SSLCertificates::Support - @http_cache = {} - # Clear our http cache, closing all connections. def self.clear_http_instances - @http_cache.each do |name, connection| + http_cache.each do |name, connection| connection.finish if connection.started? end - @http_cache.clear - @cert = nil - @key = nil + Puppet::Util::Cacher.expire end # Make sure we set the driver up when we read the cert in. @@ -44,17 +51,13 @@ module Puppet::Network::HttpPool # Use cert information from a Puppet client to set up the http object. def self.cert_setup(http) # Just no-op if we don't have certs. - return false unless (defined?(@cert) and @cert) or self.read_cert - - store = OpenSSL::X509::Store.new - store.add_file Puppet[:localcacert] - store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT + return false unless FileTest.exist?(Puppet[:hostcert]) # ssl_host.certificate - http.cert_store = store + http.cert_store = ssl_host.ssl_store http.ca_file = Puppet[:localcacert] - http.cert = self.cert + http.cert = ssl_host.certificate.content http.verify_mode = OpenSSL::SSL::VERIFY_PEER - http.key = self.key + http.key = ssl_host.key.content end # Retrieve a cached http instance of caching is enabled, else return @@ -66,11 +69,11 @@ module Puppet::Network::HttpPool # Return our cached instance if we've got a cache, as long as we're not # resetting the instance. if keep_alive? - return @http_cache[key] if ! reset and @http_cache[key] + return http_cache[key] if ! reset and http_cache[key] # Clean up old connections if we have them. - if http = @http_cache[key] - @http_cache.delete(key) + if http = http_cache[key] + http_cache.delete(key) http.finish if http.started? end end @@ -100,7 +103,7 @@ module Puppet::Network::HttpPool cert_setup(http) - @http_cache[key] = http if keep_alive? + http_cache[key] = http if keep_alive? return http end diff --git a/lib/puppet/network/http_server/mongrel.rb b/lib/puppet/network/http_server/mongrel.rb index 6b2325d29..924c11728 100644 --- a/lib/puppet/network/http_server/mongrel.rb +++ b/lib/puppet/network/http_server/mongrel.rb @@ -34,7 +34,6 @@ require 'puppet/network/xmlrpc/server' require 'puppet/network/http_server' require 'puppet/network/client_request' require 'puppet/network/handler' -require 'puppet/daemon' require 'resolv' @@ -51,7 +50,6 @@ require 'resolv' # </pre> module Puppet::Network class HTTPServer::Mongrel < ::Mongrel::HttpHandler - include Puppet::Daemon attr_reader :xmlrpc_server def initialize(handlers) @@ -64,11 +62,11 @@ module Puppet::Network # behaviour and we have to subclass Mongrel::HttpHandler so our handler # works for Mongrel. @xmlrpc_server = Puppet::Network::XMLRPCServer.new - handlers.each do |name, args| + handlers.each do |name| unless handler = Puppet::Network::Handler.handler(name) raise ArgumentError, "Invalid handler %s" % name end - @xmlrpc_server.add_handler(handler.interface, handler.new(args)) + @xmlrpc_server.add_handler(handler.interface, handler.new({})) end end diff --git a/lib/puppet/network/http_server/webrick.rb b/lib/puppet/network/http_server/webrick.rb index 568b4e798..0e835d057 100644 --- a/lib/puppet/network/http_server/webrick.rb +++ b/lib/puppet/network/http_server/webrick.rb @@ -1,5 +1,4 @@ require 'puppet' -require 'puppet/daemon' require 'webrick' require 'webrick/https' require 'fcntl' @@ -16,7 +15,6 @@ module Puppet # The old-school, pure ruby webrick server, which is the default serving # mechanism. class HTTPServer::WEBrick < WEBrick::HTTPServer - include Puppet::Daemon include Puppet::SSLCertificates::Support # Read the CA cert and CRL and populate an OpenSSL::X509::Store diff --git a/lib/puppet/network/server.rb b/lib/puppet/network/server.rb index cab14519b..de32db02f 100644 --- a/lib/puppet/network/server.rb +++ b/lib/puppet/network/server.rb @@ -1,59 +1,153 @@ require 'puppet/network/http' +require 'puppet/util/pidlock' class Puppet::Network::Server - attr_reader :server_type, :protocols, :address, :port + attr_reader :server_type, :protocols, :address, :port + + # Put the daemon into the background. + def daemonize + if pid = fork() + Process.detach(pid) + exit(0) + end + + # Get rid of console logging + Puppet::Util::Log.close(:console) + + Process.setsid + Dir.chdir("/") + begin + $stdin.reopen "/dev/null" + $stdout.reopen "/dev/null", "a" + $stderr.reopen $stdout + Puppet::Util::Log.reopen + rescue => detail + File.open("/tmp/daemonout", "w") { |f| + f.puts "Could not start %s: %s" % [Puppet[:name], detail] + } + raise "Could not start %s: %s" % [Puppet[:name], detail] + end + end + + # Create a pidfile for our daemon, so we can be stopped and others + # don't try to start. + def create_pidfile + Puppet::Util.sync(Puppet[:name]).synchronize(Sync::EX) do + unless Puppet::Util::Pidlock.new(pidfile).lock + raise "Could not create PID file: %s" % [pidfile] + end + end + end + + # Remove the pid file for our daemon. + def remove_pidfile + Puppet::Util.sync(Puppet[:name]).synchronize(Sync::EX) do + locker = Puppet::Util::Pidlock.new(pidfile) + if locker.locked? + locker.unlock or Puppet.err "Could not remove PID file %s" % [pidfile] + end + end + end + + # Provide the path to our pidfile. + def pidfile + Puppet[:pidfile] + end def initialize(args = {}) @server_type = Puppet[:servertype] or raise "No servertype configuration found." # e.g., WEBrick, Mongrel, etc. http_server_class || raise(ArgumentError, "Could not determine HTTP Server class for server type [#{@server_type}]") - @address = args[:address] || Puppet[:bindaddress] || - raise(ArgumentError, "Must specify :address or configure Puppet :bindaddress.") - @port = args[:port] || Puppet[:masterport] || - raise(ArgumentError, "Must specify :port or configure Puppet :masterport") - @protocols = [ :rest ] + + @address = args[:address] || Puppet[:bindaddress] || raise(ArgumentError, "Must specify :address or configure Puppet :bindaddress.") + @port = args[:port] || Puppet[:masterport] || raise(ArgumentError, "Must specify :port or configure Puppet :masterport") + + @protocols = [ :rest, :xmlrpc ] @listening = false @routes = {} + @xmlrpc_routes = {} self.register(args[:handlers]) if args[:handlers] + self.register_xmlrpc(args[:xmlrpc_handlers]) if args[:xmlrpc_handlers] + + # Make sure we have all of the directories we need to function. + Puppet.settings.use(:main, :ssl, Puppet[:name]) end + # Register handlers for REST networking, based on the Indirector. def register(*indirections) - raise ArgumentError, "Indirection names are required." if indirections.empty? - indirections.flatten.each { |i| @routes[i.to_sym] = true } + raise ArgumentError, "Indirection names are required." if indirections.empty? + indirections.flatten.each do |name| + Puppet::Indirector::Indirection.model(name) || raise(ArgumentError, "Cannot locate indirection '#{name}'.") + @routes[name.to_sym] = true + end end + # Unregister Indirector handlers. def unregister(*indirections) - raise "Cannot unregister indirections while server is listening." if listening? - indirections = @routes.keys if indirections.empty? + raise "Cannot unregister indirections while server is listening." if listening? + indirections = @routes.keys if indirections.empty? + + indirections.flatten.each do |i| + raise(ArgumentError, "Indirection [%s] is unknown." % i) unless @routes[i.to_sym] + end + + indirections.flatten.each do |i| + @routes.delete(i.to_sym) + end + end + + # Register xmlrpc handlers for backward compatibility. + def register_xmlrpc(*namespaces) + raise ArgumentError, "XMLRPC namespaces are required." if namespaces.empty? + namespaces.flatten.each do |name| + Puppet::Network::Handler.handler(name) || raise(ArgumentError, "Cannot locate XMLRPC handler for namespace '#{name}'.") + @xmlrpc_routes[name.to_sym] = true + end + end + + # Unregister xmlrpc handlers. + def unregister_xmlrpc(*namespaces) + raise "Cannot unregister xmlrpc handlers while server is listening." if listening? + namespaces = @xmlrpc_routes.keys if namespaces.empty? - indirections.flatten.each do |i| - raise(ArgumentError, "Indirection [%s] is unknown." % i) unless @routes[i.to_sym] - end + namespaces.flatten.each do |i| + raise(ArgumentError, "XMLRPC handler '%s' is unknown." % i) unless @xmlrpc_routes[i.to_sym] + end - indirections.flatten.each do |i| - @routes.delete(i.to_sym) - end + namespaces.flatten.each do |i| + @xmlrpc_routes.delete(i.to_sym) + end end def listening? - @listening + @listening end def listen - raise "Cannot listen -- already listening." if listening? - @listening = true - http_server.listen(:address => address, :port => port, :handlers => @routes.keys, :protocols => protocols) + raise "Cannot listen -- already listening." if listening? + @listening = true + http_server.listen(:address => address, :port => port, :handlers => @routes.keys, :xmlrpc_handlers => @xmlrpc_routes.keys, :protocols => protocols) end def unlisten - raise "Cannot unlisten -- not currently listening." unless listening? - http_server.unlisten - @listening = false + raise "Cannot unlisten -- not currently listening." unless listening? + http_server.unlisten + @listening = false end def http_server_class http_server_class_by_type(@server_type) end + def start + create_pidfile + listen + end + + def stop + unlisten + remove_pidfile + end + private def http_server diff --git a/lib/puppet/node.rb b/lib/puppet/node.rb index e10299d87..74bf8902d 100644 --- a/lib/puppet/node.rb +++ b/lib/puppet/node.rb @@ -67,10 +67,14 @@ class Puppet::Node # Merge the node facts with parameters from the node source. def fact_merge - if facts = Puppet::Node::Facts.find(name) - merge(facts.values) - else - Puppet.warning "Could not find facts for %s; you probably have a discrepancy between the node and fact names" % name + begin + if facts = Puppet::Node::Facts.find(name) + merge(facts.values) + end + rescue => detail + error = Puppet::Error.new("Could not retrieve facts for %s: %s" % [name, detail]) + error.set_backtrace(detail.backtrace) + raise error end end @@ -79,6 +83,8 @@ class Puppet::Node params.each do |name, value| @parameters[name] = value unless @parameters.include?(name) end + + @parameters["environment"] ||= self.environment if self.environment end # Calculate the list of names we might use for looking diff --git a/lib/puppet/node/facts.rb b/lib/puppet/node/facts.rb index 8ee90b4ac..dca435c7d 100755 --- a/lib/puppet/node/facts.rb +++ b/lib/puppet/node/facts.rb @@ -21,6 +21,11 @@ class Puppet::Node::Facts attr_accessor :name, :values + def add_local_facts + values["clientversion"] = Puppet.version.to_s + values["environment"] ||= Puppet.settings[:environment] + end + def initialize(name, values = {}) @name = name @values = values @@ -28,6 +33,22 @@ class Puppet::Node::Facts add_internal end + def downcase_if_necessary + return unless Puppet.settings[:downcasefacts] + + Puppet.warning "DEPRECATION NOTICE: Fact downcasing is deprecated; please disable (20080122)" + values.each do |fact, value| + values[fact] = value.downcase if value.is_a?(String) + end + end + + # Convert all fact values into strings. + def stringify + values.each do |fact, value| + values[fact] = value.to_s + end + end + private # Add internal data to the facts for storage. diff --git a/lib/puppet/parameter.rb b/lib/puppet/parameter.rb index 06dfe5b91..04b3aec30 100644 --- a/lib/puppet/parameter.rb +++ b/lib/puppet/parameter.rb @@ -2,6 +2,7 @@ require 'puppet/util/methodhelper' require 'puppet/util/log_paths' require 'puppet/util/logging' require 'puppet/util/docs' +require 'puppet/util/cacher' class Puppet::Parameter include Puppet::Util @@ -9,10 +10,223 @@ class Puppet::Parameter include Puppet::Util::LogPaths include Puppet::Util::Logging include Puppet::Util::MethodHelper + include Puppet::Util::Cacher + + # A collection of values and regexes, used for specifying + # what values are allowed in a given parameter. + class ValueCollection + class Value + attr_reader :name, :options, :event + attr_accessor :block, :call, :method, :required_features + + # Add an alias for this value. + def alias(name) + @aliases << convert(name) + end + + # Return all aliases. + def aliases + @aliases.dup + end + + # Store the event that our value generates, if it does so. + def event=(value) + @event = convert(value) + end + + def initialize(name) + if name.is_a?(Regexp) + @name = name + else + # Convert to a string and then a symbol, so things like true/false + # still show up as symbols. + @name = convert(name) + end + + @aliases = [] + + @call = :instead + end + + # Does a provided value match our value? + def match?(value) + if regex? + return true if name =~ value.to_s + else + return true if name == convert(value) + return @aliases.include?(convert(value)) + end + end + + # Is our value a regex? + def regex? + @name.is_a?(Regexp) + end + + private + + # A standard way of converting all of our values, so we're always + # comparing apples to apples. + def convert(value) + if value == '' + # We can't intern an empty string, yay. + value + else + value.to_s.to_sym + end + end + end + + def aliasvalue(name, other) + other = other.to_sym + unless value = match?(other) + raise Puppet::DevError, "Cannot alias nonexistent value %s" % other + end + + value.alias(name) + end + + # Return a doc string for all of the values in this parameter/property. + def doc + unless defined?(@doc) + @doc = "" + unless values.empty? + @doc += " Valid values are " + @doc += @strings.collect do |value| + if aliases = value.aliases and ! aliases.empty? + "``%s`` (also called ``%s``)" % [value.name, aliases.join(", ")] + else + "``%s``" % value.name + end + end.join(", ") + "." + end + + unless regexes.empty? + @doc += " Values can match ``" + regexes.join("``, ``") + "``." + end + end + + @doc + end + + # Does this collection contain any value definitions? + def empty? + @values.empty? + end + + def initialize + # We often look values up by name, so a hash makes more sense. + @values = {} + + # However, we want to retain the ability to match values in order, + # but we always prefer directly equality (i.e., strings) over regex matches. + @regexes = [] + @strings = [] + end + + # Can we match a given value? + def match?(test_value) + # First look for normal values + if value = @strings.find { |v| v.match?(test_value) } + return value + end + + # Then look for a regex match + @regexes.find { |v| v.match?(test_value) } + end + + # If the specified value is allowed, then munge appropriately. + def munge(value) + return value if empty? + + if instance = match?(value) + if instance.regex? + return value + else + return instance.name + end + else + return value + end + end + + # Define a new valid value for a property. You must provide the value itself, + # usually as a symbol, or a regex to match the value. + # + # The first argument to the method is either the value itself or a regex. + # The second argument is an option hash; valid options are: + # * <tt>:event</tt>: The event that should be returned when this value is set. + # * <tt>:call</tt>: When to call any associated block. The default value + # is ``instead``, which means to call the value instead of calling the + # provider. You can also specify ``before`` or ``after``, which will + # call both the block and the provider, according to the order you specify + # (the ``first`` refers to when the block is called, not the provider). + def newvalue(name, options = {}, &block) + value = Value.new(name) + @values[value.name] = value + if value.regex? + @regexes << value + else + @strings << value + end + + options.each { |opt, arg| value.send(opt.to_s + "=", arg) } + if block_given? + value.block = block + else + value.call = options[:call] || :none + end + + if block_given? and ! value.regex? + value.method ||= "set_" + value.name.to_s + end + + value + end + + # Define one or more new values for our parameter. + def newvalues(*names) + names.each { |name| newvalue(name) } + end + + def regexes + @regexes.collect { |r| r.name.inspect } + end + + # Verify that the passed value is valid. + def validate(value) + return if empty? + + unless @values.detect { |name, v| v.match?(value) } + str = "Invalid value %s. " % [value.inspect] + + unless values.empty? + str += "Valid values are %s. " % values.join(", ") + end + + unless regexes.empty? + str += "Valid values match %s." % regexes.join(", ") + end + + raise ArgumentError, str + end + end + + # Return a single value instance. + def value(name) + @values[name] + end + + # Return the list of valid values. + def values + @strings.collect { |s| s.name } + end + end + class << self include Puppet::Util include Puppet::Util::Docs - attr_reader :validater, :munger, :name, :default, :required_features + attr_reader :validater, :munger, :name, :default, :required_features, :value_collection attr_accessor :metaparam # Define the default value for a given parameter or parameter. This @@ -36,36 +250,7 @@ class Puppet::Parameter @doc ||= "" unless defined? @addeddocvals - unless values.empty? - if @aliasvalues.empty? - @doc += " Valid values are ``" + - values.join("``, ``") + "``." - else - @doc += " Valid values are " - - @doc += values.collect do |value| - ary = @aliasvalues.find do |name, val| - val == value - end - if ary - "``%s`` (also called ``%s``)" % [value, ary[0]] - else - "``#{value}``" - end - end.join(", ") + "." - end - end - - if defined? @parameterregexes and ! @parameterregexes.empty? - regs = @parameterregexes - if @parameterregexes.is_a? Hash - regs = @parameterregexes.keys - end - unless regs.empty? - @doc += " Values can also match ``" + - regs.collect { |r| r.inspect }.join("``, ``") + "``." - end - end + @doc += value_collection.doc if f = self.required_features @doc += " Requires features %s." % f.flatten.collect { |f| f.to_s }.join(" ") @@ -88,9 +273,7 @@ class Puppet::Parameter end def initvars - @parametervalues = [] - @aliasvalues = {} - @parameterregexes = [] + @value_collection = ValueCollection.new end # This is how we munge the value. Basically, this is our @@ -101,23 +284,6 @@ class Puppet::Parameter # class's context, not the instance's, thus the two methods, # instead of just one. define_method(:unsafe_munge, &block) - - define_method(:munge) do |*args| - begin - ret = unsafe_munge(*args) - rescue Puppet::Error => detail - Puppet.debug "Reraising %s" % detail - raise - rescue => detail - raise Puppet::DevError, "Munging failed for value %s in class %s: %s" % - [args.inspect, self.name, detail], detail.backtrace - end - - if self.shadow - self.shadow.munge(*args) - end - ret - end end # Mark whether we're the namevar. @@ -140,6 +306,11 @@ class Puppet::Parameter @required = true end + # Specify features that are required for this parameter to work. + def required_features=(*args) + @required_features = args.flatten.collect { |a| a.to_s.downcase.intern } + end + # Is this parameter required? Defaults to false. def required? if defined? @required @@ -151,88 +322,16 @@ class Puppet::Parameter # Verify that we got a good value def validate(&block) - #@validater = block define_method(:unsafe_validate, &block) - - define_method(:validate) do |*args| - begin - unsafe_validate(*args) - rescue ArgumentError, Puppet::Error, TypeError - raise - rescue => detail - raise Puppet::DevError, - "Validate method failed for class %s: %s" % - [self.name, detail], detail.backtrace - end - end - end - - # Does the value match any of our regexes? - def match?(value) - value = value.to_s unless value.is_a? String - @parameterregexes.find { |r| - r = r[0] if r.is_a? Array # Properties use a hash here - r =~ value - } end # Define a new value for our parameter. def newvalues(*names) - names.each { |name| - name = name.intern if name.is_a? String - - case name - when Symbol - if @parametervalues.include?(name) - Puppet.warning "%s already has a value for %s" % - [name, name] - end - @parametervalues << name - when Regexp - if @parameterregexes.include?(name) - Puppet.warning "%s already has a value for %s" % - [name, name] - end - @parameterregexes << name - else - raise ArgumentError, "Invalid value %s of type %s" % - [name, name.class] - end - } + @value_collection.newvalues(*names) end def aliasvalue(name, other) - other = symbolize(other) - unless @parametervalues.include?(other) - raise Puppet::DevError, - "Cannot alias nonexistent value %s" % other - end - - @aliasvalues[name] = other - end - - def alias(name) - @aliasvalues[name] - end - - def regexes - return @parameterregexes.dup - end - - def required_features=(*args) - @required_features = args.flatten.collect { |a| a.to_s.downcase.intern } - end - - # Return the list of valid values. - def values - #[@aliasvalues.keys, @parametervalues.keys].flatten - if @parametervalues.is_a? Array - return @parametervalues.dup - elsif @parametervalues.is_a? Hash - return @parametervalues.keys - else - return [] - end + @value_collection.aliasvalue(name, other) end end @@ -252,12 +351,15 @@ class Puppet::Parameter attr_accessor :resource # LAK 2007-05-09: Keep the @parent around for backward compatibility. attr_accessor :parent - attr_reader :shadow def devfail(msg) self.fail(Puppet::DevError, msg) end + def expirer + resource.catalog + end + def fail(*args) type = nil if args[0].is_a?(Class) @@ -289,17 +391,12 @@ class Puppet::Parameter raise Puppet::DevError, "No resource set for %s" % self.class.name end - if ! self.metaparam? and klass = Puppet::Type.metaparamclass(self.class.name) - setup_shadow(klass) - end - set_options(options) end # Log a message using the resource's log level. def log(msg) unless @resource[:loglevel] - p @resource self.devfail "Parent %s has no loglevel" % @resource.name end @@ -344,73 +441,45 @@ class Puppet::Parameter end # If the specified value is allowed, then munge appropriately. - munge do |value| - if self.class.values.empty? and self.class.regexes.empty? - # This parameter isn't using defined values to do its work. - return value - end - - # We convert to a string and then a symbol so that things like - # booleans work as we expect. - intern = value.to_s.intern - - # If it's a valid value, always return it as a symbol. - if self.class.values.include?(intern) - retval = intern - elsif other = self.class.alias(intern) - retval = other - elsif ary = self.class.match?(value) - retval = value - else - # If it passed the validation but is not a registered value, - # we just return it as is. - retval = value - end + # If the developer uses a 'munge' hook, this method will get overridden. + def unsafe_munge(value) + self.class.value_collection.munge(value) + end - retval + # A wrapper around our munging that makes sure we raise useful exceptions. + def munge(value) + begin + ret = unsafe_munge(value) + rescue Puppet::Error => detail + Puppet.debug "Reraising %s" % detail + raise + rescue => detail + raise Puppet::DevError, "Munging failed for value %s in class %s: %s" % [value.inspect, self.name, detail], detail.backtrace + end + ret end # Verify that the passed value is valid. - validate do |value| - vals = self.class.values - regs = self.class.regexes - - # this is true on properties - regs = regs.keys if regs.is_a?(Hash) - - # This parameter isn't using defined values to do its work. - return if vals.empty? and regs.empty? - - newval = value - newval = value.to_s.intern unless value.is_a?(Symbol) - - name = newval - - unless vals.include?(newval) or name = self.class.alias(newval) or name = self.class.match?(value) # We match the string, not the symbol - str = "Invalid '%s' value %s. " % - [self.class.name, value.inspect] - - unless vals.empty? - str += "Valid values are %s. " % vals.join(", ") - end - - unless regs.empty? - str += "Valid values match %s." % regs.collect { |r| - r.to_s - }.join(", ") - end + # If the developer uses a 'validate' hook, this method will get overridden. + def unsafe_validate(value) + self.class.value_collection.validate(value) + end - raise ArgumentError, str + # A protected validation method that only ever raises useful exceptions. + def validate(value) + begin + unsafe_validate(value) + rescue ArgumentError => detail + fail detail.to_s + rescue Puppet::Error, TypeError + raise + rescue => detail + raise Puppet::DevError, "Validate method failed for class %s: %s" % [self.name, detail], detail.backtrace end - - # Now check for features. - name = name[0] if name.is_a?(Array) # This is true for regexes. - validate_features_per_value(name) if is_a?(Puppet::Property) end def remove @resource = nil - @shadow = nil end attr_reader :value @@ -419,48 +488,18 @@ class Puppet::Parameter # late-binding (e.g., users might not exist when the value is assigned # but might when it is asked for). def value=(value) - if respond_to?(:validate) - validate(value) - end + validate(value) - if respond_to?(:munge) - value = munge(value) - end - @value = value - end - - def inspect - s = "Parameter(%s = %s" % [self.name, self.value || "nil"] - if defined? @resource - s += ", @resource = %s)" % @resource - else - s += ")" - end + @value = munge(value) end # Retrieve the resource's provider. Some types don't have providers, in which # case we return the resource object itself. def provider - @resource.provider || @resource - end - - # If there's a shadowing metaparam, instantiate it now. - # This allows us to create a property or parameter with the - # same name as a metaparameter, and the metaparam will only be - # stored as a shadow. - def setup_shadow(klass) - @shadow = klass.new(:resource => self.resource) + @resource.provider end def to_s s = "Parameter(%s)" % self.name end - - # Make sure that we've got all of the required features for a given value. - def validate_features_per_value(value) - if features = self.class.value_option(value, :required_features) - raise ArgumentError, "Provider must have features '%s' to set '%s' to '%s'" % [features, self.class.name, value] unless provider.satisfies?(features) - end - end end - diff --git a/lib/puppet/parser/ast/resource_defaults.rb b/lib/puppet/parser/ast/resource_defaults.rb index 4919817fb..ed83d3573 100644 --- a/lib/puppet/parser/ast/resource_defaults.rb +++ b/lib/puppet/parser/ast/resource_defaults.rb @@ -12,7 +12,7 @@ class Puppet::Parser::AST # object type. def evaluate(scope) # Use a resource reference to canonize the type - ref = Puppet::ResourceReference.new(@type, "whatever") + ref = Puppet::Resource::Reference.new(@type, "whatever") type = ref.type params = @params.safeevaluate(scope) diff --git a/lib/puppet/parser/collector.rb b/lib/puppet/parser/collector.rb index bcba9528e..9423db26b 100644 --- a/lib/puppet/parser/collector.rb +++ b/lib/puppet/parser/collector.rb @@ -35,7 +35,7 @@ class Puppet::Parser::Collector @scope = scope # Canonize the type - @type = Puppet::ResourceReference.new(type, "whatever").type + @type = Puppet::Resource::Reference.new(type, "whatever").type @equery = equery @vquery = vquery diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb index d67b3d275..7dcd50270 100644 --- a/lib/puppet/parser/compiler.rb +++ b/lib/puppet/parser/compiler.rb @@ -2,7 +2,7 @@ # Copyright (c) 2007. All rights reserved. require 'puppet/node' -require 'puppet/node/catalog' +require 'puppet/resource/catalog' require 'puppet/util/errors' # Maintain a graph of scopes, along with a bunch of data @@ -10,7 +10,7 @@ require 'puppet/util/errors' class Puppet::Parser::Compiler include Puppet::Util include Puppet::Util::Errors - attr_reader :parser, :node, :facts, :collections, :catalog, :node_scope + attr_reader :parser, :node, :facts, :collections, :catalog, :node_scope, :resources # Add a collection to the global list. def add_collection(coll) @@ -31,6 +31,8 @@ class Puppet::Parser::Compiler # Store a resource in our resource table. def add_resource(scope, resource) + @resources << resource + # Note that this will fail if the resource is not unique. @catalog.add_resource(resource) @@ -204,11 +206,6 @@ class Puppet::Parser::Compiler @resource_overrides[resource.ref] end - # Return a list of all resources. - def resources - @catalog.vertices - end - # The top scope is usually the top-level scope, but if we're using AST nodes, # then it is instead the node's scope. def topscope @@ -310,6 +307,7 @@ class Puppet::Parser::Compiler @main_resource = Puppet::Parser::Resource.new(:type => "class", :title => :main, :scope => @topscope, :source => @main) @topscope.resource = @main_resource + @resources << @main_resource @catalog.add_resource(@main_resource) @main_resource.evaluate @@ -366,7 +364,7 @@ class Puppet::Parser::Compiler # Make sure all of our resources and such have done any last work # necessary. def finish - @catalog.vertices.each do |resource| + resources.each do |resource| # Add in any resource overrides. if overrides = resource_overrides(resource) overrides.each do |over| @@ -412,8 +410,11 @@ class Puppet::Parser::Compiler @scope_graph = Puppet::SimpleGraph.new # For maintaining the relationship between scopes and their resources. - @catalog = Puppet::Node::Catalog.new(@node.name) + @catalog = Puppet::Resource::Catalog.new(@node.name) @catalog.version = @parser.version + + # local resource array to maintain resource ordering + @resources = [] end # Set the node's parameters into the top-scope as variables. @@ -436,7 +437,7 @@ class Puppet::Parser::Compiler # We used to have hooks here for forking and saving, but I don't # think it's worth retaining at this point. - store_to_active_record(@node, @catalog.vertices) + store_to_active_record(@node, resources) end # Do the actual storage. @@ -459,7 +460,7 @@ class Puppet::Parser::Compiler # Return an array of all of the unevaluated resources. These will be definitions, # which need to get evaluated into native resources. def unevaluated_resources - ary = @catalog.vertices.reject { |resource| resource.builtin? or resource.evaluated? } + ary = resources.reject { |resource| resource.builtin? or resource.evaluated? } if ary.empty? return nil diff --git a/lib/puppet/parser/functions.rb b/lib/puppet/parser/functions.rb index b1cd0d083..b9e49131c 100644 --- a/lib/puppet/parser/functions.rb +++ b/lib/puppet/parser/functions.rb @@ -92,7 +92,7 @@ module Functions #ret += "%s\n%s\n" % [name, hash[:type]] ret += "%s\n%s\n" % [name, "-" * name.to_s.length] if hash[:doc] - ret += hash[:doc].gsub(/\n\s*/, ' ') + ret += Puppet::Util::Docs.scrub(hash[:doc]) else ret += "Undocumented.\n" end diff --git a/lib/puppet/parser/functions/versioncmp.rb b/lib/puppet/parser/functions/versioncmp.rb new file mode 100644 index 000000000..62df38ffc --- /dev/null +++ b/lib/puppet/parser/functions/versioncmp.rb @@ -0,0 +1,10 @@ +require 'puppet/util/package' + +Puppet::Parser::Functions::newfunction(:versioncmp, :doc => "Compares two versions.") do |args| + + unless args.length == 2 + raise Puppet::ParseError, "versioncmp should have 2 arguments" + end + + return Puppet::Util::Package.versioncmp(args[0], args[1]) +end diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index 423c34a4e..c728b54a2 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -26,10 +26,10 @@ class Puppet::Parser::Interpreter def compile(node) raise Puppet::ParseError, "Could not parse configuration; cannot compile on node %s" % node.name unless env_parser = parser(node.environment) begin - return Puppet::Parser::Compiler.new(node, env_parser).compile + return Puppet::Parser::Compiler.new(node, env_parser).compile.to_resource rescue => detail - puts detail.backtrace if Puppet[:trace] - raise Puppet::Error, detail.to_s + " on node %s" % node.name + puts detail.backtrace if Puppet[:trace] + raise Puppet::Error, detail.to_s + " on node %s" % node.name end end diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index 2fdd78ddf..d4c7a1f0f 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -176,7 +176,7 @@ class Puppet::Parser::Resource db_resource.file = self.file end - updated_params = @params.inject({}) do |hash, ary| + updated_params = @params.reject { |name, param| param.value == :undef }.inject({}) do |hash, ary| hash[ary[0].to_s] = ary[1] hash end @@ -270,6 +270,7 @@ class Puppet::Parser::Resource db_resource.save @params.each { |name, param| + next if param.value == :undef param.to_rails(db_resource) } @@ -278,69 +279,57 @@ class Puppet::Parser::Resource return db_resource end - def to_s - self.ref - end + # Create a Puppet::Resource instance from this parser resource. + # We plan, at some point, on not needing to do this conversion, but + # it's sufficient for now. + def to_resource + result = Puppet::Resource.new(type, title) - # Translate our object to a transportable object. - def to_trans - return nil if virtual? - - if builtin? - to_transobject - else - to_transbucket - end - end - - def to_transbucket - bucket = Puppet::TransBucket.new([]) - - bucket.type = self.type - bucket.name = self.title - - # TransBuckets don't support parameters, which is why they're being deprecated. - return bucket - end - - # Convert this resource to a RAL resource. We hackishly go via the - # transportable stuff. - def to_type - to_trans.to_type - end - - def to_transobject - # Now convert to a transobject - obj = Puppet::TransObject.new(@ref.title, @ref.type) to_hash.each do |p, v| - if v.is_a?(Reference) - v = v.to_ref + if v.is_a?(Puppet::Parser::Resource::Reference) + v = Puppet::Resource::Reference.new(v.type, v.title) elsif v.is_a?(Array) - v = v.collect { |av| - if av.is_a?(Reference) - av = av.to_ref + v = v.collect do |av| + if av.is_a?(Puppet::Parser::Resource::Reference) + av = Puppet::Resource::Reference.new(av.type, av.title) end av - } + end end # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. - obj[p.to_s] = if v.is_a?(Array) and v.length == 1 + result[p] = if v.is_a?(Array) and v.length == 1 v[0] else v end end - obj.file = self.file - obj.line = self.line + result.file = self.file + result.line = self.line + result.tag(*self.tags) + + return result + end + + def to_s + self.ref + end + + # Translate our object to a transportable object. + def to_trans + return nil if virtual? - obj.tags = self.tags + return to_resource.to_trans + end - return obj + # Convert this resource to a RAL resource. We hackishly go via the + # transportable stuff. + def to_ral + to_resource.to_ral end private @@ -364,6 +353,7 @@ class Puppet::Parser::Resource # LAK:NOTE Relationship metaparams get treated specially -- we stack them, instead of # overriding. next if @params[name] and not self.class.relationship_parameter?(name) + next if @params[name] and @params[name].value == :undef # Skip metaparams for which we get no value. next unless val = scope.lookupvar(name.to_s, false) and val != :undefined diff --git a/lib/puppet/parser/resource/reference.rb b/lib/puppet/parser/resource/reference.rb index cb505d606..e552b51fe 100644 --- a/lib/puppet/parser/resource/reference.rb +++ b/lib/puppet/parser/resource/reference.rb @@ -1,7 +1,7 @@ -require 'puppet/resource_reference' +require 'puppet/resource/reference' # A reference to a resource. Mostly just the type and title. -class Puppet::Parser::Resource::Reference < Puppet::ResourceReference +class Puppet::Parser::Resource::Reference < Puppet::Resource::Reference include Puppet::Util::MethodHelper include Puppet::Util::Errors diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index 4acdf41c9..77e7b0cfd 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -52,11 +52,11 @@ class Puppet::Parser::Scope if value.is_a?(String) if value =~ /^-?\d+(:?\.\d+|(:?\.\d+)?e\d+)$/ return value.to_f - elsif value =~ /^0x\d+/i + elsif value =~ /^0x[0-9a-f]+$/i return value.to_i(16) - elsif value =~ /^0\d+/i + elsif value =~ /^0[0-7]+$/ return value.to_i(8) - elsif value =~ /^-?\d+/ + elsif value =~ /^-?\d+$/ return value.to_i else return nil diff --git a/lib/puppet/pgraph.rb b/lib/puppet/pgraph.rb deleted file mode 100644 index 55ad7d2c1..000000000 --- a/lib/puppet/pgraph.rb +++ /dev/null @@ -1,121 +0,0 @@ -# Created by Luke A. Kanies on 2006-11-24. -# Copyright (c) 2006. All rights reserved. - -require 'puppet/relationship' -require 'puppet/simple_graph' - -# This class subclasses a graph class in order to handle relationships -# among resources. -class Puppet::PGraph < Puppet::SimpleGraph - include Puppet::Util - - def add_edge(*args) - @reversal = nil - super - end - - def add_vertex(*args) - @reversal = nil - super - end - - # Which resources a given resource depends upon. - def dependents(resource) - tree_from_vertex(resource).keys - end - - # Which resources depend upon the given resource. - def dependencies(resource) - # Cache the reversal graph, because it's somewhat expensive - # to create. - unless defined? @reversal and @reversal - @reversal = reversal - end - # Strangely, it's significantly faster to search a reversed - # tree in the :out direction than to search a normal tree - # in the :in direction. - @reversal.tree_from_vertex(resource, :out).keys - end - - # Determine all of the leaf nodes below a given vertex. - def leaves(vertex, direction = :out) - tree = tree_from_vertex(vertex, direction) - l = tree.keys.find_all { |c| adjacent(c, :direction => direction).empty? } - return l - end - - # Collect all of the edges that the passed events match. Returns - # an array of edges. - def matching_edges(events, base = nil) - events.collect do |event| - source = base || event.source - - unless vertex?(source) - Puppet.warning "Got an event from invalid vertex %s" % source.ref - next - end - # Get all of the edges that this vertex should forward events - # to, which is the same thing as saying all edges directly below - # This vertex in the graph. - adjacent(source, :direction => :out, :type => :edges).find_all do |edge| - edge.match?(event.name) - end - end.compact.flatten - end - - # Take container information from another graph and use it - # to replace any container vertices with their respective leaves. - # This creates direct relationships where there were previously - # indirect relationships through the containers. - def splice!(other, type) - # We have to get the container list via a topological sort on the - # configuration graph, because otherwise containers that contain - # other containers will add those containers back into the - # graph. We could get a similar affect by only setting relationships - # to container leaves, but that would result in many more - # relationships. - containers = other.topsort.find_all { |v| v.is_a?(type) and vertex?(v) } - containers.each do |container| - # Get the list of children from the other graph. - children = other.adjacent(container, :direction => :out) - - # Just remove the container if it's empty. - if children.empty? - remove_vertex!(container) - next - end - - # First create new edges for each of the :in edges - [:in, :out].each do |dir| - edges = adjacent(container, :direction => dir, :type => :edges) - edges.each do |edge| - children.each do |child| - if dir == :in - s = edge.source - t = child - else - s = child - t = edge.target - end - - add_edge(s, t, edge.label) - end - - # Now get rid of the edge, so remove_vertex! works correctly. - remove_edge!(edge) - end - end - remove_vertex!(container) - end - end - - # A different way of walking a tree, and a much faster way than the - # one that comes with GRATR. - def tree_from_vertex(start, direction = :out) - predecessor={} - walk(start, direction) do |parent, child| - predecessor[child] = parent - end - predecessor - end -end diff --git a/lib/puppet/property.rb b/lib/puppet/property.rb index e6d0704e6..f8a17ac07 100644 --- a/lib/puppet/property.rb +++ b/lib/puppet/property.rb @@ -4,8 +4,7 @@ require 'puppet' require 'puppet/parameter' -module Puppet -class Property < Puppet::Parameter +class Puppet::Property < Puppet::Parameter # Because 'should' uses an array, we have a special method for handling # it. We also want to keep copies of the original values, so that @@ -54,39 +53,26 @@ class Property < Puppet::Parameter end # Look up a value's name, so we can find options and such. - def self.value_name(value) - if value != '' and name = symbolize(value) and @parametervalues.include?(name) - return name - elsif ary = self.match?(value) - return ary[0] - else - return nil + def self.value_name(name) + if value = value_collection.match?(name) + value.name end end # Retrieve an option set when a value was defined. def self.value_option(name, option) - option = option.to_sym - if hash = @parameteroptions[name] - hash[option] - else - nil + if value = value_collection.value(name) + value.send(option) end end - # Create the value management variables. - def self.initvars - @parametervalues = {} - @aliasvalues = {} - @parameterregexes = {} - @parameteroptions = {} - end - # Define a new valid value for a property. You must provide the value itself, # usually as a symbol, or a regex to match the value. # # The first argument to the method is either the value itself or a regex. # The second argument is an option hash; valid options are: + # * <tt>:method</tt>: The name of the method to define. Defaults to 'set_<value>'. + # * <tt>:required_features</tt>: A list of features this value requires. # * <tt>:event</tt>: The event that should be returned when this value is set. # * <tt>:call</tt>: When to call any associated block. The default value # is ``instead``, which means to call the value instead of calling the @@ -94,52 +80,12 @@ class Property < Puppet::Parameter # call both the block and the provider, according to the order you specify # (the ``first`` refers to when the block is called, not the provider). def self.newvalue(name, options = {}, &block) - name = name.intern if name.is_a? String - - @parameteroptions[name] = {} - paramopts = @parameteroptions[name] + value = value_collection.newvalue(name, options, &block) - # Symbolize everything - options.each do |opt, val| - paramopts[symbolize(opt)] = symbolize(val) - end - - # By default, call the block instead of the provider. - if block_given? - paramopts[:call] ||= :instead - else - paramopts[:call] ||= :none - end - # If there was no block given, we still want to store the information - # for validation, but we won't be defining a method - block ||= true - - case name - when Symbol - if @parametervalues.include?(name) - Puppet.warning "%s reassigning value %s" % [self.name, name] - end - @parametervalues[name] = block - - if block_given? - method = "set_" + name.to_s - settor = paramopts[:settor] || (self.name.to_s + "=") - define_method(method, &block) - paramopts[:method] = method - end - when Regexp - # The regexes are handled in parameter.rb. This value is used - # for validation. - @parameterregexes[name] = block - - # This is used for looking up the block for execution. - if block_given? - paramopts[:block] = block - end - else - raise ArgumentError, "Invalid value %s of type %s" % - [name, name.class] + if value.method and value.block + define_method(value.method, &value.block) end + value end # Call the provider method. @@ -175,9 +121,9 @@ class Property < Puppet::Parameter elsif block = self.class.value_option(name, :block) # FIXME It'd be better here to define a method, so that # the blocks could return values. - # If the regex was defined with no associated block, then just pass - # through and the correct event will be passed back. event = self.instance_eval(&block) + else + devfail "Could not find method for value '%s'" % name end return event, name end @@ -231,18 +177,14 @@ class Property < Puppet::Parameter return event end + attr_reader :shadow + # initialize our property def initialize(hash = {}) super - end - - def inspect - str = "Property('%s', " % self.name - if defined? @should and @should - str += "@should = '%s')" % @should.join(", ") - else - str += "@should = nil)" + if ! self.metaparam? and klass = Puppet::Type.metaparamclass(self.class.name) + setup_shadow(klass) end end @@ -308,6 +250,13 @@ class Property < Puppet::Parameter self.class.array_matching == :all end + # Execute our shadow's munge code, too, if we have one. + def munge(value) + self.shadow.munge(value) if self.shadow + + super + end + # each property class must define the name() method, and property instances # do not change that name # this implicitly means that a given object can only have one property @@ -342,29 +291,39 @@ class Property < Puppet::Parameter # Set a name for looking up associated options like the event. name = self.class.value_name(value) - call = self.class.value_option(name, :call) + call = self.class.value_option(name, :call) || :none - # If we're supposed to call the block first or instead, call it now - if call == :before or call == :instead + if call == :instead event, tmp = call_valuemethod(name, value) - end - unless call == :instead + elsif call == :none if @resource.provider call_provider(value) else # They haven't provided a block, and our parent does not have # a provider, so we have no idea how to handle this. - self.fail "%s cannot handle values of type %s" % - [self.class.name, value.inspect] + self.fail "%s cannot handle values of type %s" % [self.class.name, value.inspect] end - end - if call == :after - event, tmp = call_valuemethod(name, value) + else + # LAK:NOTE 20081031 This is a change in behaviour -- you could + # previously specify :call => [;before|:after], which would call + # the setter *in addition to* the block. I'm convinced this + # was never used, and it makes things unecessarily complicated. + # If you want to specify a block and still call the setter, then + # do so in the block. + devfail "Cannot use obsolete :call value '%s' for property '%s'" % [call, self.class.name] end return event(name, event) end + # If there's a shadowing metaparam, instantiate it now. + # This allows us to create a property or parameter with the + # same name as a metaparameter, and the metaparam will only be + # stored as a shadow. + def setup_shadow(klass) + @shadow = klass.new(:resource => self.resource) + end + # Only return the first value def should if defined? @should @@ -390,18 +349,8 @@ class Property < Puppet::Parameter @shouldorig = values - if self.respond_to?(:validate) - values.each { |val| - validate(val) - } - end - if self.respond_to?(:munge) - @should = values.collect { |val| - self.munge(val) - } - else - @should = values - end + values.each { |val| validate(val) } + @should = values.collect { |val| self.munge(val) } end def should_to_s(newvalue) @@ -413,10 +362,7 @@ class Property < Puppet::Parameter end end - # The default 'sync' method only selects among a list of registered # values. def sync - self.devfail("No values defined for %s" % self.class.name) unless self.class.values - if value = self.should set(value) else @@ -432,7 +378,7 @@ class Property < Puppet::Parameter if @resource.respond_to? :tags @tags = @resource.tags end - @tags << self.name + @tags << self.name.to_s end @tags end @@ -441,6 +387,20 @@ class Property < Puppet::Parameter return "%s(%s)" % [@resource.name,self.name] end + # Verify that the passed value is valid. + # If the developer uses a 'validate' hook, this method will get overridden. + def unsafe_validate(value) + super + validate_features_per_value(value) + end + + # Make sure that we've got all of the required features for a given value. + def validate_features_per_value(value) + if features = self.class.value_option(self.class.value_name(value), :required_features) + raise ArgumentError, "Provider must have features '%s' to set '%s' to '%s'" % [features.collect { |f| f.to_s }.join(", "), self.class.name, value] unless provider.satisfies?(features) + end + end + # Just return any should value we might have. def value self.should @@ -545,5 +505,3 @@ class Property < Puppet::Parameter end end end -end - diff --git a/lib/puppet/provider.rb b/lib/puppet/provider.rb index c02e15029..ce195c086 100644 --- a/lib/puppet/provider.rb +++ b/lib/puppet/provider.rb @@ -260,7 +260,7 @@ class Puppet::Provider # use the hash here for later events. @property_hash = resource elsif resource - @resource = resource if resource + @resource = resource # LAK 2007-05-09: Keep the model stuff around for backward compatibility @model = resource @property_hash = {} diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb index 8d4f6d55a..2457840d1 100644 --- a/lib/puppet/provider/augeas/augeas.rb +++ b/lib/puppet/provider/augeas/augeas.rb @@ -22,9 +22,9 @@ require 'augeas' if Puppet.features.augeas? Puppet::Type.type(:augeas).provide(:augeas) do #class Puppet::Provider::Augeas < Puppet::Provider include Puppet::Util - - confine :true => Puppet.features.augeas? - + + confine :true => Puppet.features.augeas? + has_features :parse_commands, :need_to_run?,:execute_changes # Extracts an 2 dimensional array of commands which are in the @@ -38,10 +38,27 @@ Puppet::Type.type(:augeas).provide(:augeas) do if data.is_a?(String) data.each_line do |line| cmd_array = Array.new() - tokens = line.split(" ") - cmd = tokens.shift() - file = tokens.shift() - other = tokens.join(" ") + single = line.index("'") + double = line.index('"') + tokens = nil + delim = " " + if ((single != nil) or (double != nil)) + single = 99999 if single == nil + double = 99999 if double == nil + delim = '"' if double < single + delim = "'" if single < double + end + tokens = line.split(delim) + # If the length of tokens is 2, thn that means the pattern was + # command file "some text", therefore we need to re-split + # the first line + if tokens.length == 2 + tokens = (tokens[0].split(" ")) << tokens[1] + end + cmd = tokens.shift().strip() + delim = "" if delim == " " + file = tokens.shift().strip() + other = tokens.join(" ").strip() cmd_array << cmd if !cmd.nil? cmd_array << file if !file.nil? cmd_array << other if other != "" @@ -63,7 +80,7 @@ Puppet::Type.type(:augeas).provide(:augeas) do debug("Opening augeas with root #{root}, lens path #{load_path}, flags #{flags}") Augeas.open(root, load_path,flags) end - + # Used by the need_to_run? method to process get filters. Returns # true if there is a match, false if otherwise # Assumes a syntax of get /files/path [COMPARATOR] value @@ -93,8 +110,8 @@ Puppet::Type.type(:augeas).provide(:augeas) do end end return_value - end - + end + # Used by the need_to_run? method to process match filters. Returns # true if there is a match, false if otherwise def process_match(cmd_array) @@ -131,8 +148,8 @@ Puppet::Type.type(:augeas).provide(:augeas) do end end return_value - end - + end + # Determines if augeas acutally needs to run. def need_to_run? return_value = true @@ -152,8 +169,8 @@ Puppet::Type.type(:augeas).provide(:augeas) do end end return_value - end - + end + # Actually execute the augeas changes. def execute_changes aug = open_augeas @@ -204,7 +221,7 @@ Puppet::Type.type(:augeas).provide(:augeas) do fail("Save failed with return code #{success}") end - return :executed - end - + return :executed + end + end diff --git a/lib/puppet/provider/mailalias/aliases.rb b/lib/puppet/provider/mailalias/aliases.rb index 8b5c45617..f9217123e 100755 --- a/lib/puppet/provider/mailalias/aliases.rb +++ b/lib/puppet/provider/mailalias/aliases.rb @@ -17,6 +17,14 @@ Puppet::Type.type(:mailalias).provide(:aliases, record end + def process(line) + ret = {} + records = line.split(':',2) + ret[:name] = records[0].strip() + ret[:recipient] = records[1].strip() + ret + end + def to_line(record) dest = record[:recipient].collect do |d| # Quote aliases that have non-alpha chars diff --git a/lib/puppet/provider/package/rug.rb b/lib/puppet/provider/package/rug.rb index 1e1d6763f..b68ec30c5 100644 --- a/lib/puppet/provider/package/rug.rb +++ b/lib/puppet/provider/package/rug.rb @@ -1,4 +1,4 @@ -Puppet.type(:package).provide :rug, :parent => :rpm do +Puppet::Type.type(:package).provide :rug, :parent => :rpm do desc "Support for suse ``rug`` package manager." has_feature :versionable diff --git a/lib/puppet/provider/package/up2date.rb b/lib/puppet/provider/package/up2date.rb index e2a9d06f1..d8a12652f 100644 --- a/lib/puppet/provider/package/up2date.rb +++ b/lib/puppet/provider/package/up2date.rb @@ -1,4 +1,4 @@ -Puppet.type(:package).provide :up2date, :parent => :rpm, :source => :rpm do +Puppet::Type.type(:package).provide :up2date, :parent => :rpm, :source => :rpm do desc "Support for Red Hat's proprietary ``up2date`` package update mechanism." diff --git a/lib/puppet/provider/ssh_authorized_key/parsed.rb b/lib/puppet/provider/ssh_authorized_key/parsed.rb index 77af58ef5..5604ba32a 100644 --- a/lib/puppet/provider/ssh_authorized_key/parsed.rb +++ b/lib/puppet/provider/ssh_authorized_key/parsed.rb @@ -40,25 +40,55 @@ Puppet::Type.type(:ssh_authorized_key).provide(:parsed, # This was done in the type class but path expansion was failing for # not yet existing users, the only workaround I found was to move that # in the provider. - if user = @resource.should(:user) - target = File.expand_path("~%s/.ssh/authorized_keys" % user) - @property_hash[:target] = target - @resource[:target] = target - end + @resource[:target] = target super end + def target + if user + File.expand_path("~%s/.ssh/authorized_keys" % user) + elsif target = @resource.should(:target) + target + end + end + + def user + @resource.should(:user) + end + + def dir_perm + # Determine correct permission for created directory and file + # we can afford more restrictive permissions when the user is known + if target + if user + 0700 + else + 0755 + end + end + end + + def file_perm + if target + if user + 0600 + else + 0644 + end + end + end + def flush # As path expansion had to be moved in the provider, we cannot generate new file # resources and thus have to chown and chmod here. It smells hackish. - + # Create target's parent directory if nonexistant - if target = @property_hash[:target] - dir = File.dirname(@property_hash[:target]) + if target + dir = File.dirname(target) if not File.exist? dir Puppet.debug("Creating directory %s which did not exist" % dir) - Dir.mkdir(dir, 0700) + Dir.mkdir(dir, dir_perm) end end @@ -66,9 +96,19 @@ Puppet::Type.type(:ssh_authorized_key).provide(:parsed, super # Ensure correct permissions - if target and user = @property_hash[:user] - File.chown(Puppet::Util.uid(user), nil, dir) - File.chown(Puppet::Util.uid(user), nil, @property_hash[:target]) + if target and user + uid = Puppet::Util.uid(user) + + if uid + File.chown(uid, nil, dir) + File.chown(uid, nil, target) + else + raise Puppet::Error, "Specified user does not exist" + end + end + + if target + File.chmod(file_perm, target) end end diff --git a/lib/puppet/reports/store.rb b/lib/puppet/reports/store.rb index dfc992820..0c7f8cea9 100644 --- a/lib/puppet/reports/store.rb +++ b/lib/puppet/reports/store.rb @@ -12,7 +12,7 @@ Puppet::Reports.register_report(:store) do def mkclientdir(client, dir) config = Puppet::Util::Settings.new - config.setdefaults("reportclient-#{client}", + config.setdefaults("reportclient-#{client}".to_sym, "client-#{client}-dir" => { :default => dir, :mode => 0750, :desc => "Client dir for %s" % client, @@ -21,7 +21,7 @@ Puppet::Reports.register_report(:store) do } ) - config.use("reportclient-#{client}") + config.use("reportclient-#{client}".to_sym) end def process diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb new file mode 100644 index 000000000..add32b7cf --- /dev/null +++ b/lib/puppet/resource.rb @@ -0,0 +1,200 @@ +require 'puppet' +require 'puppet/util/tagging' +require 'puppet/resource/reference' + +# The simplest resource class. Eventually it will function as the +# base class for all resource-like behaviour. +class Puppet::Resource + include Puppet::Util::Tagging + include Enumerable + attr_accessor :type, :title, :file, :line, :catalog, :implicit + + # Proxy these methods to the parameters hash. It's likely they'll + # be overridden at some point, but this works for now. + %w{has_key? keys length delete empty? <<}.each do |method| + define_method(method) do |*args| + @parameters.send(method, *args) + end + end + + # Set a given parameter. Converts all passed names + # to lower-case symbols. + def []=(param, value) + @parameters[parameter_name(param)] = value + end + + # Return a given parameter's value. Converts all passed names + # to lower-case symbols. + def [](param) + @parameters[parameter_name(param)] + end + + # Compatibility method. + def builtin? + builtin_type? + end + + # Is this a builtin resource type? + def builtin_type? + @reference.builtin_type? + end + + # Iterate over each param/value pair, as required for Enumerable. + def each + @parameters.each { |p,v| yield p, v } + end + + # Create our resource. + def initialize(type, title, parameters = {}) + @reference = Puppet::Resource::Reference.new(type, title) + @parameters = {} + + parameters.each do |param, value| + self[param] = value + end + + tag(@reference.type) + tag(@reference.title) if valid_tag?(@reference.title) + end + + # Provide a reference to our resource in the canonical form. + def ref + @reference.to_s + end + + # Get our title information from the reference, since it will canonize it for us. + def title + @reference.title + end + + # Get our type information from the reference, since it will canonize it for us. + def type + @reference.type + end + + # Produce a simple hash of our parameters. + def to_hash + result = @parameters.dup + unless result.include?(namevar) + result[namevar] = title + end + if result.has_key?(nil) + raise "wtf? %s" % namevar.inspect + end + result + end + + def to_s + return ref + end + + # Convert our resource to Puppet code. + def to_manifest + "%s { '%s':\n%s\n}" % [self.type.to_s.downcase, self.title, + @parameters.collect { |p, v| + if v.is_a? Array + " #{p} => [\'#{v.join("','")}\']" + else + " #{p} => \'#{v}\'" + end + }.join(",\n") + ] + end + + def to_ref + ref + end + + # Convert our resource to a RAL resource instance. Creates component + # instances for resource types that don't exist. + def to_ral + if typeklass = Puppet::Type.type(self.type) + return typeklass.new(self) + else + return Puppet::Type::Component.new(self) + end + end + + # Translate our object to a backward-compatible transportable object. + def to_trans + if @reference.builtin_type? + result = to_transobject + else + result = to_transbucket + end + + result.file = self.file + result.line = self.line + + return result + end + + # Create an old-style TransObject instance, for builtin resource types. + def to_transobject + # Now convert to a transobject + result = Puppet::TransObject.new(@reference.title, @reference.type) + to_hash.each do |p, v| + if v.is_a?(Puppet::Resource::Reference) + v = v.to_trans_ref + elsif v.is_a?(Array) + v = v.collect { |av| + if av.is_a?(Puppet::Resource::Reference) + av = av.to_trans_ref + end + av + } + end + + # If the value is an array with only one value, then + # convert it to a single value. This is largely so that + # the database interaction doesn't have to worry about + # whether it returns an array or a string. + result[p.to_s] = if v.is_a?(Array) and v.length == 1 + v[0] + else + v + end + end + + result.tags = self.tags + + return result + end + + private + + # Produce a canonical method name. + def parameter_name(param) + param = param.to_s.downcase.to_sym + if param == :name and n = namevar() + param = namevar + end + param + end + + # The namevar for our resource type. If the type doesn't exist, + # always use :name. + def namevar + if t = resource_type + t.namevar + else + :name + end + end + + # Retrieve the resource type. + def resource_type + Puppet::Type.type(type) + end + + # Create an old-style TransBucket instance, for non-builtin resource types. + def to_transbucket + bucket = Puppet::TransBucket.new([]) + + bucket.type = self.type + bucket.name = self.title + + # TransBuckets don't support parameters, which is why they're being deprecated. + return bucket + end +end diff --git a/lib/puppet/node/catalog.rb b/lib/puppet/resource/catalog.rb index 17927388a..78f6a6f79 100644 --- a/lib/puppet/node/catalog.rb +++ b/lib/puppet/resource/catalog.rb @@ -1,18 +1,23 @@ require 'puppet/indirector' -require 'puppet/pgraph' +require 'puppet/simple_graph' require 'puppet/transaction' +require 'puppet/util/cacher' + require 'puppet/util/tagging' # This class models a node catalog. It is the thing # meant to be passed from server to client, and it contains all # of the information in the catalog, including the resources # and the relationships between them. -class Puppet::Node::Catalog < Puppet::PGraph +class Puppet::Resource::Catalog < Puppet::SimpleGraph + class DuplicateResourceError < Puppet::Error; end + extend Puppet::Indirector indirects :catalog, :terminus_class => :compiler include Puppet::Util::Tagging + include Puppet::Util::Cacher::Expirer # The host name this is a catalog for. attr_accessor :name @@ -27,10 +32,6 @@ class Puppet::Node::Catalog < Puppet::PGraph # How we should extract the catalog for sending to the client. attr_reader :extraction_format - # We need the ability to set this externally, so we can yaml-dump the - # catalog. - attr_accessor :edgelist_class - # Whether this is a host catalog, which behaves very differently. # In particular, reports are sent, graphs are made, and state is # stored in the state database. If this is set incorrectly, then you often @@ -38,11 +39,6 @@ class Puppet::Node::Catalog < Puppet::PGraph # that the host catalog needs. attr_accessor :host_config - # Whether this graph is another catalog's relationship graph. - # We don't want to accidentally create a relationship graph for another - # relationship graph. - attr_accessor :is_relationship_graph - # Whether this catalog was retrieved from the cache, which affects # whether it is written back out again. attr_accessor :from_cache @@ -58,16 +54,21 @@ class Puppet::Node::Catalog < Puppet::PGraph end # Add one or more resources to our graph and to our resource table. + # This is actually a relatively complicated method, because it handles multiple + # aspects of Catalog behaviour: + # * Add the resource to the resource table + # * Add the resource to the resource graph + # * Add the resource to the relationship graph + # * Add any aliases that make sense for the resource (e.g., name != title) def add_resource(*resources) resources.each do |resource| unless resource.respond_to?(:ref) - raise ArgumentError, "Can only add objects that respond to :ref" + raise ArgumentError, "Can only add objects that respond to :ref, not instances of %s" % resource.class end - - fail_unless_unique(resource) - + end.find_all { |resource| fail_or_skip_unless_unique(resource) }.each do |resource| ref = resource.ref + @transient_resources << resource if applying? @resource_table[ref] = resource # If the name and title differ, set up an alias @@ -76,9 +77,15 @@ class Puppet::Node::Catalog < Puppet::PGraph self.alias(resource, resource.name) if resource.isomorphic? end - resource.catalog = self if resource.respond_to?(:catalog=) and ! is_relationship_graph + resource.catalog = self if resource.respond_to?(:catalog=) add_vertex(resource) + + if @relationship_graph + @relationship_graph.add_vertex(resource) + end + + yield(resource) if block_given? end end @@ -99,6 +106,7 @@ class Puppet::Node::Catalog < Puppet::PGraph raise(ArgumentError, "Cannot alias %s to %s; resource %s already exists" % [resource.ref, name, newref]) end @resource_table[newref] = resource + @aliases[resource.ref] ||= [] @aliases[resource.ref] << newref end @@ -111,6 +119,10 @@ class Puppet::Node::Catalog < Puppet::PGraph def apply(options = {}) @applying = true + # Expire all of the resource data -- this ensures that all + # data we're operating against is entirely current. + expire() + Puppet::Util::Storage.load if host_config? transaction = Puppet::Transaction.new(self) @@ -156,7 +168,7 @@ class Puppet::Node::Catalog < Puppet::PGraph @resource_table.clear if defined?(@relationship_graph) and @relationship_graph - @relationship_graph.clear(false) + @relationship_graph.clear @relationship_graph = nil end end @@ -165,48 +177,25 @@ class Puppet::Node::Catalog < Puppet::PGraph @classes.dup end - # Create an implicit resource, meaning that it will lose out - # to any explicitly defined resources. This method often returns - # nil. - # The quirk of this method is that it's not possible to create - # an implicit resource before an explicit resource of the same name, - # because all explicit resources are created before any generate() - # methods are called on the individual resources. Thus, this - # method can safely just check if an explicit resource already exists - # and toss this implicit resource if so. - def create_implicit_resource(type, options) - unless options.include?(:implicit) - options[:implicit] = true - end - - # This will return nil if an equivalent explicit resource already exists. - # When resource classes no longer retain references to resource instances, - # this will need to be modified to catch that conflict and discard - # implicit resources. - if resource = create_resource(type, options) - resource.implicit = true - - return resource - else - return nil - end - end - # Create a new resource and register it in the catalog. def create_resource(type, options) unless klass = Puppet::Type.type(type) raise ArgumentError, "Unknown resource type %s" % type end - return unless resource = klass.create(options) + return unless resource = klass.new(options) - @transient_resources << resource if applying? add_resource(resource) - if @relationship_graph - @relationship_graph.add_resource(resource) unless @relationship_graph.resource(resource.ref) - end resource end + def expired?(ts) + if applying? + return super + else + return true + end + end + # Make sure we support the requested extraction format. def extraction_format=(value) unless respond_to?("extract_to_%s" % value) @@ -221,7 +210,8 @@ class Puppet::Node::Catalog < Puppet::PGraph end # Create the traditional TransBuckets and TransObjects from our catalog - # graph. This will hopefully be deprecated soon. + # graph. LAK:NOTE(20081211): This is a pre-0.25 backward compatibility method. + # It can be removed as soon as xmlrpc is killed. def extract_to_transportable top = nil current = nil @@ -297,7 +287,7 @@ class Puppet::Node::Catalog < Puppet::PGraph @applying = false @relationship_graph = nil - @aliases = Hash.new { |hash, key| hash[key] = [] } + @aliases = {} if block_given? yield(self) @@ -315,23 +305,19 @@ class Puppet::Node::Catalog < Puppet::PGraph # And filebuckets if bucket = Puppet::Type.type(:filebucket).mkdefaultbucket - add_resource(bucket) + add_resource(bucket) unless resource(bucket.ref) end end # Create a graph of all of the relationships in our catalog. def relationship_graph - raise(Puppet::DevError, "Tried get a relationship graph for a relationship graph") if self.is_relationship_graph - unless defined? @relationship_graph and @relationship_graph # It's important that we assign the graph immediately, because # the debug messages below use the relationships in the # relationship graph to determine the path to the resources # spitting out the messages. If this is not set, # then we get into an infinite loop. - @relationship_graph = Puppet::Node::Catalog.new - @relationship_graph.host_config = host_config? - @relationship_graph.is_relationship_graph = true + @relationship_graph = Puppet::SimpleGraph.new # First create the dependency graph self.vertices.each do |vertex| @@ -343,7 +329,7 @@ class Puppet::Node::Catalog < Puppet::PGraph # Lastly, add in any autorequires @relationship_graph.vertices.each do |vertex| - vertex.autorequire.each do |edge| + vertex.autorequire(self).each do |edge| unless @relationship_graph.edge?(edge.source, edge.target) # don't let automatic relationships conflict with manual ones. unless @relationship_graph.edge?(edge.target, edge.source) vertex.debug "Autorequiring %s" % [edge.source] @@ -354,13 +340,12 @@ class Puppet::Node::Catalog < Puppet::PGraph end end end - - @relationship_graph.write_graph(:relationships) + @relationship_graph.write_graph(:relationships) if host_config? # Then splice in the container information @relationship_graph.splice!(self, Puppet::Type::Component) - @relationship_graph.write_graph(:expanded_relationships) + @relationship_graph.write_graph(:expanded_relationships) if host_config? end @relationship_graph end @@ -371,8 +356,10 @@ class Puppet::Node::Catalog < Puppet::PGraph def remove_resource(*resources) resources.each do |resource| @resource_table.delete(resource.ref) - @aliases[resource.ref].each { |res_alias| @resource_table.delete(res_alias) } - @aliases[resource.ref].clear + if aliases = @aliases[resource.ref] + aliases.each { |res_alias| @resource_table.delete(res_alias) } + @aliases.delete(resource.ref) + end remove_vertex!(resource) if vertex?(resource) @relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource) resource.remove @@ -384,18 +371,14 @@ class Puppet::Node::Catalog < Puppet::PGraph # Always create a resource reference, so that it always canonizes how we # are referring to them. if title - ref = Puppet::ResourceReference.new(type, title).to_s + ref = Puppet::Resource::Reference.new(type, title).to_s else # If they didn't provide a title, then we expect the first # argument to be of the form 'Class[name]', which our # Reference class canonizes for us. - ref = Puppet::ResourceReference.new(nil, type).to_s - end - if resource = @resource_table[ref] - return resource - elsif defined?(@relationship_graph) and @relationship_graph - @relationship_graph.resource(ref) + ref = Puppet::Resource::Reference.new(nil, type).to_s end + @resource_table[ref] end # Return an array of all resources. @@ -405,39 +388,31 @@ class Puppet::Node::Catalog < Puppet::PGraph # Convert our catalog into a RAL catalog. def to_ral - to_catalog :to_type + to_catalog :to_ral + end + + # Convert our catalog into a catalog of Puppet::Resource instances. + def to_resource + to_catalog :to_resource end - # Turn our parser catalog into a transportable catalog. - def to_transportable - to_catalog :to_transobject + # Store the classes in the classfile. + def write_class_file + begin + ::File.open(Puppet[:classfile], "w") do |f| + f.puts classes.join("\n") + end + rescue => detail + Puppet.err "Could not create class file %s: %s" % [Puppet[:classfile], detail] + end end # Produce the graph files if requested. def write_graph(name) # We only want to graph the main host catalog. return unless host_config? - - return unless Puppet[:graph] - - Puppet.settings.use(:graphing) - file = File.join(Puppet[:graphdir], "%s.dot" % name.to_s) - File.open(file, "w") { |f| - f.puts to_dot("name" => name.to_s.capitalize) - } - end - - # LAK:NOTE We cannot yaml-dump the class in the edgelist_class, because classes cannot be - # dumped by default, nor does yaml-dumping # the edge-labels work at this point (I don't - # know why). - # Neither of these matters right now, but I suppose it could at some point. - # We also have to have the vertex_dict dumped after the resource table, because yaml can't - # seem to handle the output of yaml-dumping the vertex_dict. - def to_yaml_properties - props = instance_variables.reject { |v| %w{@edgelist_class @edge_labels @vertex_dict}.include?(v) } - props << "@vertex_dict" - props + super end private @@ -448,12 +423,28 @@ class Puppet::Node::Catalog < Puppet::PGraph @transient_resources.clear @relationship_graph = nil end + + # Expire any cached data the resources are keeping. + expire() end # Verify that the given resource isn't defined elsewhere. - def fail_unless_unique(resource) + def fail_or_skip_unless_unique(resource) # Short-curcuit the common case, - return unless existing_resource = @resource_table[resource.ref] + return resource unless existing_resource = @resource_table[resource.ref] + + if resource.implicit? + resource.debug "Generated resource conflicts with explicit resource; ignoring generated resource" + return nil + elsif old = resource(resource.ref) and old.implicit? + # The existing resource is implicit; remove it and replace it with + # the new one. + old.debug "Replacing with new resource" + remove_resource(old) + return resource + end + + # If we've gotten this far, it's a real conflict # Either it's a defined type, which are never # isomorphic, or it's a non-isomorphic type, so @@ -469,7 +460,7 @@ class Puppet::Node::Catalog < Puppet::PGraph msg << "; cannot redefine" end - raise ArgumentError.new(msg) + raise DuplicateResourceError.new(msg) end # An abstracted method for converting one catalog into another type of catalog. @@ -478,6 +469,8 @@ class Puppet::Node::Catalog < Puppet::PGraph def to_catalog(convert) result = self.class.new(self.name) + result.version = self.version + map = {} vertices.each do |resource| next if resource.respond_to?(:virtual?) and resource.virtual? @@ -486,15 +479,22 @@ class Puppet::Node::Catalog < Puppet::PGraph #Aliases aren't working in the ral catalog because the current instance of the resource #has a reference to the catalog being converted. . . So, give it a reference to the new one #problem solved. . . - if resource.is_a?(Puppet::TransObject) + if resource.is_a?(Puppet::Resource) + resource = resource.dup + resource.catalog = result + elsif resource.is_a?(Puppet::TransObject) resource = resource.dup resource.catalog = result elsif resource.is_a?(Puppet::Parser::Resource) - resource = resource.to_transobject + resource = resource.to_resource resource.catalog = result end - newres = resource.send(convert) + if resource.is_a?(Puppet::Resource) and convert.to_s == "to_resource" + newres = resource + else + newres = resource.send(convert) + end # We can't guarantee that resources don't munge their names # (like files do with trailing slashes), so we have to keep track diff --git a/lib/puppet/resource_reference.rb b/lib/puppet/resource/reference.rb index 44b518816..750c10e41 100644 --- a/lib/puppet/resource_reference.rb +++ b/lib/puppet/resource/reference.rb @@ -3,34 +3,50 @@ # Copyright (c) 2007. All rights reserved. require 'puppet' +require 'puppet/resource' # A simple class to canonize how we refer to and retrieve # resources. -class Puppet::ResourceReference +class Puppet::Resource::Reference attr_reader :type attr_accessor :title, :catalog - def initialize(type, title) - # This will set @type if it looks like a resource reference. - self.title = title + def ==(ref) + return false unless ref.is_a?(Puppet::Resource::Reference) + return true if ref.type == self.type and ref.title == self.title + return false + end + + def builtin_type? + builtin_type ? true : false + end - # Don't override whatever was done by setting the title. - self.type = type if self.type.nil? + def initialize(argtype, argtitle = nil) + if argtitle.nil? + if argtype.is_a?(Puppet::Type) + self.title = argtype.title + self.type = argtype.class.name + else + self.title = argtype + if self.title == argtype + raise ArgumentError, "No title provided and title '%s' is not a valid resource reference" % argtype.inspect + end + end + else + # This will set @type if it looks like a resource reference. + self.title = argtitle + + # Don't override whatever was done by setting the title. + self.type ||= argtype + end @builtin_type = nil end # Find our resource. def resolve - if catalog - return catalog.resource(to_s) - end - # If it's builtin, then just ask for it directly from the type. - if t = builtin_type - t[@title] - else # Else, look for a component with the full reference as the name. - Puppet::Type::Component[to_s] - end + return catalog.resource(to_s) if catalog + return nil end # If the title has square brackets, treat it like a reference and @@ -54,6 +70,18 @@ class Puppet::ResourceReference end end + # Convert to the reference format that TransObject uses. Yay backward + # compatibility. + def to_trans_ref + # We have to return different cases to provide backward compatibility + # from 0.24.x to 0.23.x. + if builtin_type? + return [type.to_s.downcase, title.to_s] + else + return [type.to_s, title.to_s] + end + end + # Convert to the standard way of referring to resources. def to_s "%s[%s]" % [@type, @title] @@ -61,16 +89,12 @@ class Puppet::ResourceReference private - def builtin_type? - builtin_type ? true : false - end - def builtin_type if @builtin_type.nil? if @type =~ /::/ @builtin_type = false elsif klass = Puppet::Type.type(@type.to_s.downcase) - @builtin_type = klass + @builtin_type = true else @builtin_type = false end diff --git a/lib/puppet/simple_graph.rb b/lib/puppet/simple_graph.rb index 48f393f77..b9ea0f394 100644 --- a/lib/puppet/simple_graph.rb +++ b/lib/puppet/simple_graph.rb @@ -19,8 +19,7 @@ class Puppet::SimpleGraph def initialize(vertex) @vertex = vertex - @adjacencies = {:in => Hash.new { |h,k| h[k] = [] }, :out => Hash.new { |h,k| h[k] = [] }} - #@adjacencies = {:in => [], :out => []} + @adjacencies = {:in => {}, :out => {}} end # Find adjacent vertices or edges. @@ -35,7 +34,7 @@ class Puppet::SimpleGraph # Add an edge to our list. def add_edge(direction, edge) - @adjacencies[direction][other_vertex(direction, edge)] << edge + opposite_adjacencies(direction, edge) << edge end # Return all known edges. @@ -45,7 +44,7 @@ class Puppet::SimpleGraph # Test whether we share an edge with a given vertex. def has_edge?(direction, vertex) - return true if @adjacencies[direction][vertex].length > 0 + return true if vertex_adjacencies(direction, vertex).length > 0 return false end @@ -74,12 +73,29 @@ class Puppet::SimpleGraph # Remove an edge from our list. Assumes that we've already checked # that the edge is valid. def remove_edge(direction, edge) - @adjacencies[direction][other_vertex(direction, edge)].delete(edge) + opposite_adjacencies(direction, edge).delete(edge) end def to_s vertex.to_s end + + private + + # These methods exist so we don't need a Hash with a default proc. + + # Look up the adjacencies for a vertex at the other end of an + # edge. + def opposite_adjacencies(direction, edge) + opposite_vertex = other_vertex(direction, edge) + vertex_adjacencies(direction, opposite_vertex) + end + + # Look up the adjacencies for a given vertex. + def vertex_adjacencies(direction, vertex) + @adjacencies[direction][vertex] ||= [] + @adjacencies[direction][vertex] + end end def initialize @@ -94,11 +110,55 @@ class Puppet::SimpleGraph @edges.clear end + # Which resources a given resource depends upon. + def dependents(resource) + tree_from_vertex(resource).keys + end + + # Which resources depend upon the given resource. + def dependencies(resource) + # Cache the reversal graph, because it's somewhat expensive + # to create. + unless defined? @reversal and @reversal + @reversal = reversal + end + # Strangely, it's significantly faster to search a reversed + # tree in the :out direction than to search a normal tree + # in the :in direction. + @reversal.tree_from_vertex(resource, :out).keys + end + # Whether our graph is directed. Always true. Used to produce dot files. def directed? true end + # Determine all of the leaf nodes below a given vertex. + def leaves(vertex, direction = :out) + tree = tree_from_vertex(vertex, direction) + l = tree.keys.find_all { |c| adjacent(c, :direction => direction).empty? } + return l + end + + # Collect all of the edges that the passed events match. Returns + # an array of edges. + def matching_edges(events, base = nil) + events.collect do |event| + source = base || event.source + + unless vertex?(source) + Puppet.warning "Got an event from invalid vertex %s" % source.ref + next + end + # Get all of the edges that this vertex should forward events + # to, which is the same thing as saying all edges directly below + # This vertex in the graph. + adjacent(source, :direction => :out, :type => :edges).find_all do |edge| + edge.match?(event.name) + end + end.compact.flatten + end + # Return a reversed version of this graph. def reversal result = self.class.new @@ -154,6 +214,7 @@ class Puppet::SimpleGraph # Add a new vertex to the graph. def add_vertex(vertex) + @reversal = nil return false if vertex?(vertex) setup_vertex(vertex) true # don't return the VertexWrapper instance. @@ -180,6 +241,7 @@ class Puppet::SimpleGraph # Add a new edge. The graph user has to create the edge instance, # since they have to specify what kind of edge it is. def add_edge(source, target = nil, label = nil) + @reversal = nil if target edge = Puppet::Relationship.new(source, target, label) else @@ -250,6 +312,52 @@ class Puppet::SimpleGraph # induced_subgraph(gv).write_to_graphic_file('jpg', name) # end # end + + # Take container information from another graph and use it + # to replace any container vertices with their respective leaves. + # This creates direct relationships where there were previously + # indirect relationships through the containers. + def splice!(other, type) + # We have to get the container list via a topological sort on the + # configuration graph, because otherwise containers that contain + # other containers will add those containers back into the + # graph. We could get a similar affect by only setting relationships + # to container leaves, but that would result in many more + # relationships. + containers = other.topsort.find_all { |v| v.is_a?(type) and vertex?(v) } + containers.each do |container| + # Get the list of children from the other graph. + children = other.adjacent(container, :direction => :out) + + # Just remove the container if it's empty. + if children.empty? + remove_vertex!(container) + next + end + + # First create new edges for each of the :in edges + [:in, :out].each do |dir| + edges = adjacent(container, :direction => dir, :type => :edges) + edges.each do |edge| + children.each do |child| + if dir == :in + s = edge.source + t = child + else + s = child + t = edge.target + end + + add_edge(s, t, edge.label) + end + + # Now get rid of the edge, so remove_vertex! works correctly. + remove_edge!(edge) + end + end + remove_vertex!(container) + end + end def to_yaml_properties instance_variables @@ -263,6 +371,16 @@ class Puppet::SimpleGraph end end + # A different way of walking a tree, and a much faster way than the + # one that comes with GRATR. + def tree_from_vertex(start, direction = :out) + predecessor={} + walk(start, direction) do |parent, child| + predecessor[child] = parent + end + predecessor + end + # LAK:FIXME This is just a paste of the GRATR code with slight modifications. # Return a DOT::DOTDigraph for directed graphs or a DOT::DOTSubgraph for an @@ -304,6 +422,14 @@ class Puppet::SimpleGraph system('dotty', dotfile) end + # Just walk the tree and pass each edge. + def walk(source, direction, &block) + adjacent(source, :direction => direction).each do |target| + yield source, target + walk(target, direction, &block) + end + end + # Use +dot+ to create a graphical representation of the graph. Returns the # filename of the graphics file. def write_to_graphic_file (fmt='png', dotfile='graph') @@ -315,4 +441,16 @@ class Puppet::SimpleGraph system( "dot -T#{fmt} #{src} -o #{dot}" ) dot end + + # Produce the graph files if requested. + def write_graph(name) + return unless Puppet[:graph] + + Puppet.settings.use(:graphing) + + file = File.join(Puppet[:graphdir], "%s.dot" % name.to_s) + File.open(file, "w") { |f| + f.puts to_dot("name" => name.to_s.capitalize) + } + end end diff --git a/lib/puppet/ssl.rb b/lib/puppet/ssl.rb new file mode 100644 index 000000000..1a3e8d13d --- /dev/null +++ b/lib/puppet/ssl.rb @@ -0,0 +1,7 @@ +# Just to make the constants work out. +require 'puppet' +require 'openssl' + +module Puppet::SSL # :nodoc: + require 'puppet/ssl/host' +end diff --git a/lib/puppet/ssl/base.rb b/lib/puppet/ssl/base.rb new file mode 100644 index 000000000..d67861f4b --- /dev/null +++ b/lib/puppet/ssl/base.rb @@ -0,0 +1,62 @@ +require 'puppet/ssl' + +# The base class for wrapping SSL instances. +class Puppet::SSL::Base + # For now, use the YAML separator. + SEPARATOR = "\n---\n" + + def self.from_multiple_s(text) + text.split(SEPARATOR).collect { |inst| from_s(inst) } + end + + def self.to_multiple_s(instances) + instances.collect { |inst| inst.to_s }.join(SEPARATOR) + end + + def self.wraps(klass) + @wrapped_class = klass + end + + def self.wrapped_class + raise(Puppet::DevError, "%s has not declared what class it wraps" % self) unless defined?(@wrapped_class) + @wrapped_class + end + + attr_accessor :name, :content + + # Is this file for the CA? + def ca? + name == Puppet::SSL::Host.ca_name + end + + def generate + raise Puppet::DevError, "%s did not override 'generate'" % self.class + end + + def initialize(name) + @name = name.to_s.downcase + end + + # Read content from disk appropriately. + def read(path) + @content = wrapped_class.new(File.read(path)) + end + + # Convert our thing to pem. + def to_s + return "" unless content + content.to_pem + end + + # Provide the full text of the thing we're dealing with. + def to_text + return "" unless content + content.to_text + end + + private + + def wrapped_class + self.class.wrapped_class + end +end diff --git a/lib/puppet/ssl/certificate.rb b/lib/puppet/ssl/certificate.rb new file mode 100644 index 000000000..f9297f380 --- /dev/null +++ b/lib/puppet/ssl/certificate.rb @@ -0,0 +1,34 @@ +require 'puppet/ssl/base' + +# Manage certificates themselves. This class has no +# 'generate' method because the CA is responsible +# for turning CSRs into certificates; we can only +# retrieve them from the CA (or not, as is often +# the case). +class Puppet::SSL::Certificate < Puppet::SSL::Base + # This is defined from the base class + wraps OpenSSL::X509::Certificate + + extend Puppet::Indirector + indirects :certificate, :terminus_class => :file + + # Convert a string into an instance. + def self.from_s(string) + instance = wrapped_class.new(string) + name = instance.subject.to_s.sub(/\/CN=/i, '').downcase + result = new(name) + result.content = instance + result + end + + # Because of how the format handler class is included, this + # can't be in the base class. + def self.supported_formats + [:s] + end + + def expiration + return nil unless content + return content.not_after + end +end diff --git a/lib/puppet/ssl/certificate_authority.rb b/lib/puppet/ssl/certificate_authority.rb new file mode 100644 index 000000000..08feff0ac --- /dev/null +++ b/lib/puppet/ssl/certificate_authority.rb @@ -0,0 +1,289 @@ +require 'puppet/ssl/host' +require 'puppet/ssl/certificate_request' +require 'puppet/util/cacher' + +# The class that knows how to sign certificates. It creates +# a 'special' SSL::Host whose name is 'ca', thus indicating +# that, well, it's the CA. There's some magic in the +# indirector/ssl_file terminus base class that does that +# for us. +# This class mostly just signs certs for us, but +# it can also be seen as a general interface into all of the +# SSL stuff. +class Puppet::SSL::CertificateAuthority + require 'puppet/ssl/certificate_factory' + require 'puppet/ssl/inventory' + require 'puppet/ssl/certificate_revocation_list' + + require 'puppet/ssl/certificate_authority/interface' + + class << self + include Puppet::Util::Cacher + + cached_attr(:singleton_instance) { new } + end + + def self.ca? + return false unless Puppet[:ca] + return false unless Puppet[:name] == "puppetmasterd" + return true + end + + # If this process can function as a CA, then return a singleton + # instance. + def self.instance + return nil unless ca? + + singleton_instance + end + + attr_reader :name, :host + + # Create and run an applicator. I wanted to build an interface where you could do + # something like 'ca.apply(:generate).to(:all) but I don't think it's really possible. + def apply(method, options) + unless options[:to] + raise ArgumentError, "You must specify the hosts to apply to; valid values are an array or the symbol :all" + end + applier = Interface.new(method, options[:to]) + + applier.apply(self) + end + + # If autosign is configured, then autosign all CSRs that match our configuration. + def autosign + return unless auto = autosign? + + store = nil + if auto != true + store = autosign_store(auto) + end + + Puppet::SSL::CertificateRequest.search("*").each do |csr| + sign(csr.name) if auto == true or store.allowed?(csr.name, "127.1.1.1") + end + end + + # Do we autosign? This returns true, false, or a filename. + def autosign? + auto = Puppet[:autosign] + return false if ['false', false].include?(auto) + return true if ['true', true].include?(auto) + + raise ArgumentError, "The autosign configuration '%s' must be a fully qualified file" % auto unless auto =~ /^\// + if FileTest.exist?(auto) + return auto + else + return false + end + end + + # Create an AuthStore for autosigning. + def autosign_store(file) + auth = Puppet::Network::AuthStore.new + File.readlines(file).each do |line| + next if line =~ /^\s*#/ + next if line =~ /^\s*$/ + auth.allow(line.chomp) + end + + auth + end + + # Retrieve (or create, if necessary) the certificate revocation list. + def crl + unless defined?(@crl) + unless @crl = Puppet::SSL::CertificateRevocationList.find("ca") + @crl = Puppet::SSL::CertificateRevocationList.new("ca") + @crl.generate(host.certificate.content, host.key.content) + @crl.save + end + end + @crl + end + + # Delegate this to our Host class. + def destroy(name) + Puppet::SSL::Host.destroy(name) + end + + # Generate a new certificate. + def generate(name) + raise ArgumentError, "A Certificate already exists for %s" % name if Puppet::SSL::Certificate.find(name) + host = Puppet::SSL::Host.new(name) + + host.generate_certificate_request + + sign(name) + end + + # Generate our CA certificate. + def generate_ca_certificate + generate_password unless password? + + host.generate_key unless host.key + + # Create a new cert request. We do this + # specially, because we don't want to actually + # save the request anywhere. + request = Puppet::SSL::CertificateRequest.new(host.name) + request.generate(host.key) + + # Create a self-signed certificate. + @certificate = sign(host.name, :ca, request) + + # And make sure we initialize our CRL. + crl() + end + + def initialize + Puppet.settings.use :main, :ssl, :ca + + @name = Puppet[:certname] + + @host = Puppet::SSL::Host.new(Puppet::SSL::Host.ca_name) + + setup() + end + + # Retrieve (or create, if necessary) our inventory manager. + def inventory + unless defined?(@inventory) + @inventory = Puppet::SSL::Inventory.new + end + @inventory + end + + # Generate a new password for the CA. + def generate_password + pass = "" + 20.times { pass += (rand(74) + 48).chr } + + begin + Puppet.settings.write(:capass) { |f| f.print pass } + rescue Errno::EACCES => detail + raise Puppet::Error, "Could not write CA password: %s" % detail.to_s + end + + @password = pass + + return pass + end + + # List all signed certificates. + def list + Puppet::SSL::Certificate.search("*").collect { |c| c.name } + end + + # Read the next serial from the serial file, and increment the + # file so this one is considered used. + def next_serial + serial = nil + + # This is slightly odd. If the file doesn't exist, our readwritelock creates + # it, but with a mode we can't actually read in some cases. So, use + # a default before the lock. + unless FileTest.exist?(Puppet[:serial]) + serial = 0x0 + end + + Puppet.settings.readwritelock(:serial) { |f| + if FileTest.exist?(Puppet[:serial]) + serial ||= File.read(Puppet.settings[:serial]).chomp.hex + end + + # We store the next valid serial, not the one we just used. + f << "%04X" % (serial + 1) + } + + return serial + end + + # Does the password file exist? + def password? + FileTest.exist? Puppet[:capass] + end + + # Print a given host's certificate as text. + def print(name) + if cert = Puppet::SSL::Certificate.find(name) + return cert.to_text + else + return nil + end + end + + # Revoke a given certificate. + def revoke(name) + raise ArgumentError, "Cannot revoke certificates when the CRL is disabled" unless crl + + if cert = Puppet::SSL::Certificate.find(name) + serial = cert.content.serial + elsif ! serial = inventory.serial(name) + raise ArgumentError, "Could not find a serial number for %s" % name + end + crl.revoke(serial, host.key.content) + end + + # This initializes our CA so it actually works. This should be a private + # method, except that you can't any-instance stub private methods, which is + # *awesome*. This method only really exists to provide a stub-point during + # testing. + def setup + generate_ca_certificate unless @host.certificate + end + + # Sign a given certificate request. + def sign(hostname, cert_type = :server, self_signing_csr = nil) + # This is a self-signed certificate + if self_signing_csr + csr = self_signing_csr + issuer = csr.content + else + unless csr = Puppet::SSL::CertificateRequest.find(hostname) + raise ArgumentError, "Could not find certificate request for %s" % hostname + end + issuer = host.certificate.content + end + + cert = Puppet::SSL::Certificate.new(hostname) + cert.content = Puppet::SSL::CertificateFactory.new(cert_type, csr.content, issuer, next_serial).result + cert.content.sign(host.key.content, OpenSSL::Digest::SHA1.new) + + Puppet.notice "Signed certificate request for %s" % hostname + + # Add the cert to the inventory before we save it, since + # otherwise we could end up with it being duplicated, if + # this is the first time we build the inventory file. + inventory.add(cert) + + # Save the now-signed cert. This should get routed correctly depending + # on the certificate type. + cert.save + + # And remove the CSR if this wasn't self signed. + Puppet::SSL::CertificateRequest.destroy(csr.name) unless self_signing_csr + + return cert + end + + # Verify a given host's certificate. + def verify(name) + unless cert = Puppet::SSL::Certificate.find(name) + raise ArgumentError, "Could not find a certificate for %s" % name + end + store = OpenSSL::X509::Store.new + store.add_file Puppet[:cacert] + store.add_crl crl.content if self.crl + store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT + + unless store.verify(cert.content) + raise "Certificate for %s failed verification" % name + end + end + + # List the waiting certificate requests. + def waiting? + Puppet::SSL::CertificateRequest.search("*").collect { |r| r.name } + end +end diff --git a/lib/puppet/ssl/certificate_authority/interface.rb b/lib/puppet/ssl/certificate_authority/interface.rb new file mode 100644 index 000000000..b355e21f0 --- /dev/null +++ b/lib/puppet/ssl/certificate_authority/interface.rb @@ -0,0 +1,110 @@ +# This class is basically a hidden class that knows how to act +# on the CA. It's only used by the 'puppetca' executable, and its +# job is to provide a CLI-like interface to the CA class. +class Puppet::SSL::CertificateAuthority::Interface + INTERFACE_METHODS = [:destroy, :list, :revoke, :generate, :sign, :print, :verify] + + class InterfaceError < ArgumentError; end + + attr_reader :method, :subjects + + # Actually perform the work. + def apply(ca) + unless subjects or method == :list + raise ArgumentError, "You must provide hosts or :all when using %s" % method + end + + begin + if respond_to?(method) + return send(method, ca) + end + + (subjects == :all ? ca.list : subjects).each do |host| + ca.send(method, host) + end + rescue InterfaceError + raise + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not call %s: %s" % [method, detail] + end + end + + def generate(ca) + raise InterfaceError, "It makes no sense to generate all hosts; you must specify a list" if subjects == :all + + subjects.each do |host| + ca.generate(host) + end + end + + def initialize(method, subjects) + self.method = method + self.subjects = subjects + end + + # List the hosts. + def list(ca) + unless subjects + puts ca.waiting?.join("\n") + return nil + end + + signed = ca.list + requests = ca.waiting? + + if subjects == :all + hosts = [signed, requests].flatten + else + hosts = subjects + end + + hosts.uniq.sort.each do |host| + if signed.include?(host) + puts "+ " + host + else + puts host + end + end + end + + # Set the method to apply. + def method=(method) + raise ArgumentError, "Invalid method %s to apply" % method unless INTERFACE_METHODS.include?(method) + @method = method + end + + # Print certificate information. + def print(ca) + (subjects == :all ? ca.list : subjects).each do |host| + if value = ca.print(host) + puts value + else + Puppet.err "Could not find certificate for %s" % host + end + end + end + + # Sign a given certificate. + def sign(ca) + list = subjects == :all ? ca.waiting? : subjects + raise InterfaceError, "No waiting certificate requests to sign" if list.empty? + list.each do |host| + ca.sign(host) + end + end + + # Set the list of hosts we're operating on. Also supports keywords. + def subjects=(value) + unless value == :all or value.is_a?(Array) + raise ArgumentError, "Subjects must be an array or :all; not %s" % value + end + + if value.is_a?(Array) and value.empty? + value = nil + end + + @subjects = value + end +end + diff --git a/lib/puppet/ssl/certificate_factory.rb b/lib/puppet/ssl/certificate_factory.rb new file mode 100644 index 000000000..41155fd41 --- /dev/null +++ b/lib/puppet/ssl/certificate_factory.rb @@ -0,0 +1,145 @@ +require 'puppet/ssl' + +# The tedious class that does all the manipulations to the +# certificate to correctly sign it. Yay. +class Puppet::SSL::CertificateFactory + # How we convert from various units to the required seconds. + UNITMAP = { + "y" => 365 * 24 * 60 * 60, + "d" => 24 * 60 * 60, + "h" => 60 * 60, + "s" => 1 + } + + attr_reader :name, :cert_type, :csr, :issuer, :serial + + def initialize(cert_type, csr, issuer, serial) + @cert_type, @csr, @issuer, @serial = cert_type, csr, issuer, serial + + @name = @csr.subject + end + + # Actually generate our certificate. + def result + @cert = OpenSSL::X509::Certificate.new + + @cert.version = 2 # X509v3 + @cert.subject = @csr.subject + @cert.issuer = @issuer.subject + @cert.public_key = @csr.public_key + @cert.serial = @serial + + build_extensions() + + set_ttl + + @cert + end + + private + + # This is pretty ugly, but I'm not really sure it's even possible to do + # it any other way. + def build_extensions + @ef = OpenSSL::X509::ExtensionFactory.new + + @ef.subject_certificate = @cert + + if @issuer.is_a?(OpenSSL::X509::Request) # It's a self-signed cert + @ef.issuer_certificate = @cert + else + @ef.issuer_certificate = @issuer + end + + @subject_alt_name = [] + @key_usage = nil + @ext_key_usage = nil + @extensions = [] + + method = "add_#{@cert_type.to_s}_extensions" + + begin + send(method) + rescue NoMethodError + raise ArgumentError, "%s is an invalid certificate type" % @cert_type + end + + @extensions << @ef.create_extension("nsComment", "Puppet Ruby/OpenSSL Generated Certificate") + @extensions << @ef.create_extension("basicConstraints", @basic_constraint, true) + @extensions << @ef.create_extension("subjectKeyIdentifier", "hash") + @extensions << @ef.create_extension("keyUsage", @key_usage.join(",")) if @key_usage + @extensions << @ef.create_extension("extendedKeyUsage", @ext_key_usage.join(",")) if @ext_key_usage + @extensions << @ef.create_extension("subjectAltName", @subject_alt_name.join(",")) if ! @subject_alt_name.empty? + + @cert.extensions = @extensions + + # for some reason this _must_ be the last extension added + @extensions << @ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always") if @cert_type == :ca + end + + # TTL for new certificates in seconds. If config param :ca_ttl is set, + # use that, otherwise use :ca_days for backwards compatibility + def ttl + ttl = Puppet.settings[:ca_ttl] + + return ttl unless ttl.is_a?(String) + + raise ArgumentError, "Invalid ca_ttl #{ttl}" unless ttl =~ /^(\d+)(y|d|h|s)$/ + + return $1.to_i * UNITMAP[$2] + end + + def set_ttl + # Make the certificate valid as of yesterday, because + # so many people's clocks are out of sync. + from = Time.now - (60*60*24) + @cert.not_before = from + @cert.not_after = from + ttl + end + + # Woot! We're a CA. + def add_ca_extensions + @basic_constraint = "CA:TRUE" + @key_usage = %w{cRLSign keyCertSign} + end + + # We're a terminal CA, probably not self-signed. + def add_terminalsubca_extensions + @basic_constraint = "CA:TRUE,pathlen:0" + @key_usage = %w{cRLSign keyCertSign} + end + + # We're a normal server. + def add_server_extensions + @basic_constraint = "CA:FALSE" + dnsnames = Puppet[:certdnsnames] + name = @name.to_s.sub(%r{/CN=},'') + if dnsnames != "" + dnsnames.split(':').each { |d| @subject_alt_name << 'DNS:' + d } + @subject_alt_name << 'DNS:' + name # Add the fqdn as an alias + elsif name == Facter.value(:fqdn) # we're a CA server, and thus probably the server + @subject_alt_name << 'DNS:' + "puppet" # Add 'puppet' as an alias + @subject_alt_name << 'DNS:' + name # Add the fqdn as an alias + @subject_alt_name << 'DNS:' + name.sub(/^[^.]+./, "puppet.") # add puppet.domain as an alias + end + @key_usage = %w{digitalSignature keyEncipherment} + @ext_key_usage = %w{serverAuth clientAuth emailProtection} + end + + # Um, no idea. + def add_ocsp_extensions + @basic_constraint = "CA:FALSE" + @key_usage = %w{nonRepudiation digitalSignature} + @ext_key_usage = %w{serverAuth OCSPSigning} + end + + # Normal client. + def add_client_extensions + @basic_constraint = "CA:FALSE" + @key_usage = %w{nonRepudiation digitalSignature keyEncipherment} + @ext_key_usage = %w{clientAuth emailProtection} + + @extensions << @ef.create_extension("nsCertType", "client,email") + end +end + diff --git a/lib/puppet/ssl/certificate_request.rb b/lib/puppet/ssl/certificate_request.rb new file mode 100644 index 000000000..6a0464a33 --- /dev/null +++ b/lib/puppet/ssl/certificate_request.rb @@ -0,0 +1,51 @@ +require 'puppet/ssl/base' + +# Manage certificate requests. +class Puppet::SSL::CertificateRequest < Puppet::SSL::Base + wraps OpenSSL::X509::Request + + extend Puppet::Indirector + indirects :certificate_request, :terminus_class => :file + + # Convert a string into an instance. + def self.from_s(string) + instance = wrapped_class.new(string) + name = instance.subject.to_s.sub(/\/CN=/i, '').downcase + result = new(name) + result.content = instance + result + end + + # Because of how the format handler class is included, this + # can't be in the base class. + def self.supported_formats + [:s] + end + + # How to create a certificate request with our system defaults. + def generate(key) + Puppet.info "Creating a new SSL certificate request for %s" % name + + # Support either an actual SSL key, or a Puppet key. + key = key.content if key.is_a?(Puppet::SSL::Key) + + csr = OpenSSL::X509::Request.new + csr.version = 0 + csr.subject = OpenSSL::X509::Name.new([["CN", name]]) + csr.public_key = key.public_key + csr.sign(key, OpenSSL::Digest::MD5.new) + + raise Puppet::Error, "CSR sign verification failed; you need to clean the certificate request for %s on the server" % name unless csr.verify(key.public_key) + + @content = csr + end + + def save(args = {}) + super() + + # Try to autosign the CSR. + if ca = Puppet::SSL::CertificateAuthority.instance + ca.autosign + end + end +end diff --git a/lib/puppet/ssl/certificate_revocation_list.rb b/lib/puppet/ssl/certificate_revocation_list.rb new file mode 100644 index 000000000..f3c1a348a --- /dev/null +++ b/lib/puppet/ssl/certificate_revocation_list.rb @@ -0,0 +1,86 @@ +require 'puppet/ssl/base' +require 'puppet/indirector' + +# Manage the CRL. +class Puppet::SSL::CertificateRevocationList < Puppet::SSL::Base + wraps OpenSSL::X509::CRL + + extend Puppet::Indirector + indirects :certificate_revocation_list, :terminus_class => :file + + # Convert a string into an instance. + def self.from_s(string) + instance = wrapped_class.new(string) + result = new('foo') # The name doesn't matter + result.content = instance + result + end + + # Because of how the format handler class is included, this + # can't be in the base class. + def self.supported_formats + [:s] + end + + # Knows how to create a CRL with our system defaults. + def generate(cert, cakey) + Puppet.info "Creating a new certificate revocation list" + @content = wrapped_class.new + @content.issuer = cert.subject + @content.version = 1 + + # Init the CRL number. + crlNum = OpenSSL::ASN1::Integer(0) + @content.extensions = [OpenSSL::X509::Extension.new("crlNumber", crlNum)] + + # Set last/next update + @content.last_update = Time.now + # Keep CRL valid for 5 years + @content.next_update = Time.now + 5 * 365*24*60*60 + + @content.sign(cakey, OpenSSL::Digest::SHA1.new) + + @content + end + + # The name doesn't actually matter; there's only one CRL. + # We just need the name so our Indirector stuff all works more easily. + def initialize(fakename) + raise Puppet::Error, "Cannot manage the CRL when :cacrl is set to false" if [false, "false"].include?(Puppet[:cacrl]) + + @name = "crl" + end + + # Revoke the certificate with serial number SERIAL issued by this + # CA, then write the CRL back to disk. The REASON must be one of the + # OpenSSL::OCSP::REVOKED_* reasons + def revoke(serial, cakey, reason = OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE) + Puppet.notice "Revoked certificate with serial %s" % serial + time = Time.now + + # Add our revocation to the CRL. + revoked = OpenSSL::X509::Revoked.new + revoked.serial = serial + revoked.time = time + enum = OpenSSL::ASN1::Enumerated(reason) + ext = OpenSSL::X509::Extension.new("CRLReason", enum) + revoked.add_extension(ext) + @content.add_revoked(revoked) + + # Increment the crlNumber + e = @content.extensions.find { |e| e.oid == 'crlNumber' } + ext = @content.extensions.reject { |e| e.oid == 'crlNumber' } + crlNum = OpenSSL::ASN1::Integer(e ? e.value.to_i + 1 : 0) + ext << OpenSSL::X509::Extension.new("crlNumber", crlNum) + @content.extensions = ext + + # Set last/next update + @content.last_update = time + # Keep CRL valid for 5 years + @content.next_update = time + 5 * 365*24*60*60 + + @content.sign(cakey, OpenSSL::Digest::SHA1.new) + + save + end +end diff --git a/lib/puppet/ssl/host.rb b/lib/puppet/ssl/host.rb new file mode 100644 index 000000000..a65490c40 --- /dev/null +++ b/lib/puppet/ssl/host.rb @@ -0,0 +1,258 @@ +require 'puppet/ssl' +require 'puppet/ssl/key' +require 'puppet/ssl/certificate' +require 'puppet/ssl/certificate_request' +require 'puppet/ssl/certificate_revocation_list' +require 'puppet/util/cacher' + +# The class that manages all aspects of our SSL certificates -- +# private keys, public keys, requests, etc. +class Puppet::SSL::Host + # Yay, ruby's strange constant lookups. + Key = Puppet::SSL::Key + Certificate = Puppet::SSL::Certificate + CertificateRequest = Puppet::SSL::CertificateRequest + CertificateRevocationList = Puppet::SSL::CertificateRevocationList + + attr_reader :name + attr_accessor :ca + + attr_writer :key, :certificate, :certificate_request + + class << self + include Puppet::Util::Cacher + + cached_attr(:localhost) do + result = new() + result.generate unless result.certificate + result.key # Make sure it's read in + result + end + end + + CA_NAME = "ca" + # This is the constant that people will use to mark that a given host is + # a certificate authority. + def self.ca_name + CA_NAME + end + + class << self + attr_reader :ca_location + end + + # Configure how our various classes interact with their various terminuses. + def self.configure_indirection(terminus, cache = nil) + Certificate.terminus_class = terminus + CertificateRequest.terminus_class = terminus + CertificateRevocationList.terminus_class = terminus + + if cache + # This is weird; we don't actually cache our keys, we + # use what would otherwise be the cache as our normal + # terminus. + Key.terminus_class = cache + else + Key.terminus_class = terminus + end + + if cache + Certificate.cache_class = cache + CertificateRequest.cache_class = cache + CertificateRevocationList.cache_class = cache + else + # Make sure we have no cache configured. puppetmasterd + # switches the configurations around a bit, so it's important + # that we specify the configs for absolutely everything, every + # time. + Certificate.cache_class = nil + CertificateRequest.cache_class = nil + CertificateRevocationList.cache_class = nil + end + end + + CA_MODES = { + # Our ca is local, so we use it as the ultimate source of information + # And we cache files locally. + :local => [:ca, :file], + # We're a remote CA client. + :remote => [:rest, :file], + # We are the CA, so we don't have read/write access to the normal certificates. + :only => [:ca], + # We have no CA, so we just look in the local file store. + :none => [:file] + } + + # Specify how we expect to interact with our certificate authority. + def self.ca_location=(mode) + raise ArgumentError, "CA Mode can only be %s" % CA_MODES.collect { |m| m.to_s }.join(", ") unless CA_MODES.include?(mode) + + @ca_location = mode + + configure_indirection(*CA_MODES[@ca_location]) + end + + # Remove all traces of a given host + def self.destroy(name) + [Key, Certificate, CertificateRequest].inject(false) do |result, klass| + if klass.destroy(name) + result = true + end + result + end + end + + # Search for more than one host, optionally only specifying + # an interest in hosts with a given file type. + # This just allows our non-indirected class to have one of + # indirection methods. + def self.search(options = {}) + classes = [Key, CertificateRequest, Certificate] + if klass = options[:for] + classlist = [klass].flatten + else + classlist = [Key, CertificateRequest, Certificate] + end + + # Collect the results from each class, flatten them, collect all of the names, make the name list unique, + # then create a Host instance for each one. + classlist.collect { |klass| klass.search }.flatten.collect { |r| r.name }.uniq.collect do |name| + new(name) + end + end + + # Is this a ca host, meaning that all of its files go in the CA location? + def ca? + ca + end + + def key + return nil unless @key ||= Key.find(name) + @key + end + + # This is the private key; we can create it from scratch + # with no inputs. + def generate_key + @key = Key.new(name) + @key.generate + begin + @key.save + rescue + @key = nil + raise + end + true + end + + def certificate_request + return nil unless @certificate_request ||= CertificateRequest.find(name) + @certificate_request + end + + # Our certificate request requires the key but that's all. + def generate_certificate_request + generate_key unless key + @certificate_request = CertificateRequest.new(name) + @certificate_request.generate(key.content) + begin + @certificate_request.save + rescue + @certificate_request = nil + raise + end + + return true + end + + def certificate + unless @certificate + # get the CA cert first, since it's required for the normal cert + # to be of any use. + return nil unless Certificate.find("ca") unless ca? + @certificate = Certificate.find(name) + end + @certificate + end + + # Generate all necessary parts of our ssl host. + def generate + generate_key unless key + generate_certificate_request unless certificate_request + + # If we can get a CA instance, then we're a valid CA, and we + # should use it to sign our request; else, just try to read + # the cert. + if ! certificate() and ca = Puppet::SSL::CertificateAuthority.instance + ca.sign(self.name) + end + end + + def initialize(name = nil) + @name = (name || Puppet[:certname]).downcase + @key = @certificate = @certificate_request = nil + @ca = (name == self.class.ca_name) + end + + # Extract the public key from the private key. + def public_key + key.content.public_key + end + + # Create/return a store that uses our SSL info to validate + # connections. + def ssl_store(purpose = OpenSSL::X509::PURPOSE_ANY) + unless defined?(@ssl_store) and @ssl_store + @ssl_store = OpenSSL::X509::Store.new + @ssl_store.purpose = purpose + + # Use the file path here, because we don't want to cause + # a lookup in the middle of setting our ssl connection. + @ssl_store.add_file(Puppet[:localcacert]) + + # If there's a CRL, add it to our store. + if crl = Puppet::SSL::CertificateRevocationList.find("ca") + @ssl_store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK + @ssl_store.add_crl(crl.content) + end + return @ssl_store + end + @ssl_store + end + + # Attempt to retrieve a cert, if we don't already have one. + def wait_for_cert(time) + return if certificate + begin + generate + + return if certificate + rescue StandardError => detail + Puppet.err "Could not request certificate: %s" % detail.to_s + if time < 1 + puts "Exiting; failed to retrieve certificate and watiforcert is disabled" + exit(1) + else + sleep(time) + end + retry + end + + if time < 1 + puts "Exiting; no certificate found and waitforcert is disabled" + exit(1) + end + + while true do + sleep time + begin + break if certificate + Puppet.notice "Did not receive certificate" + rescue StandardError => detail + Puppet.err "Could not request certificate: %s" % detail.to_s + end + end + end +end + +require 'puppet/ssl/certificate_authority' diff --git a/lib/puppet/ssl/inventory.rb b/lib/puppet/ssl/inventory.rb new file mode 100644 index 000000000..38cbf46e9 --- /dev/null +++ b/lib/puppet/ssl/inventory.rb @@ -0,0 +1,52 @@ +require 'puppet/ssl' +require 'puppet/ssl/certificate' + +# Keep track of all of our known certificates. +class Puppet::SSL::Inventory + attr_reader :path + + # Add a certificate to our inventory. + def add(cert) + cert = cert.content if cert.is_a?(Puppet::SSL::Certificate) + + # Create our file, if one does not already exist. + rebuild unless FileTest.exist?(@path) + + Puppet.settings.write(:cert_inventory, "a") do |f| + f.print format(cert) + end + end + + # Format our certificate for output. + def format(cert) + iso = '%Y-%m-%dT%H:%M:%S%Z' + return "0x%04x %s %s %s\n" % [cert.serial, cert.not_before.strftime(iso), cert.not_after.strftime(iso), cert.subject] + end + + def initialize + @path = Puppet[:cert_inventory] + end + + # Rebuild the inventory from scratch. This should happen if + # the file is entirely missing or if it's somehow corrupted. + def rebuild + Puppet.notice "Rebuilding inventory file" + + Puppet.settings.write(:cert_inventory) do |f| + f.print "# Inventory of signed certificates\n# SERIAL NOT_BEFORE NOT_AFTER SUBJECT\n" + end + + Puppet::SSL::Certificate.search("*").each { |cert| add(cert) } + end + + # Find the serial number for a given certificate. + def serial(name) + return nil unless FileTest.exist?(@path) + + File.readlines(@path).each do |line| + next unless line =~ /^(\S+).+\/CN=#{name}$/ + + return Integer($1) + end + end +end diff --git a/lib/puppet/ssl/key.rb b/lib/puppet/ssl/key.rb new file mode 100644 index 000000000..d91df03f6 --- /dev/null +++ b/lib/puppet/ssl/key.rb @@ -0,0 +1,56 @@ +require 'puppet/ssl/base' +require 'puppet/indirector' + +# Manage private and public keys as a pair. +class Puppet::SSL::Key < Puppet::SSL::Base + wraps OpenSSL::PKey::RSA + + extend Puppet::Indirector + indirects :key, :terminus_class => :file + + # Because of how the format handler class is included, this + # can't be in the base class. + def self.supported_formats + [:s] + end + + attr_accessor :password_file + + # Knows how to create keys with our system defaults. + def generate + Puppet.info "Creating a new SSL key for %s" % name + @content = OpenSSL::PKey::RSA.new(Puppet[:keylength].to_i) + end + + def initialize(name) + super + + if ca? + @password_file = Puppet[:capass] + else + @password_file = Puppet[:passfile] + end + end + + def password + return nil unless password_file and FileTest.exist?(password_file) + + ::File.read(password_file) + end + + # Optionally support specifying a password file. + def read(path) + return super unless password_file + + #@content = wrapped_class.new(::File.read(path), password) + @content = wrapped_class.new(::File.read(path), password) + end + + def to_s + if pass = password + @content.export(OpenSSL::Cipher::DES.new(:EDE3, :CBC), pass) + else + return super + end + end +end diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index f3defb7a2..30f86a9d7 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -154,13 +154,13 @@ class Transaction # contained resources might never get cleaned up. def cleanup if defined? @generated - relationship_graph.remove_resource(*@generated) + catalog.remove_resource(*@generated) end end # Copy an important relationships from the parent to the newly-generated # child resource. - def copy_relationships(resource, children) + def make_parent_child_relationship(resource, children) depthfirst = resource.depthfirst? children.each do |gen_child| @@ -169,7 +169,7 @@ class Transaction else edge = [resource, gen_child] end - relationship_graph.add_resource(gen_child) unless relationship_graph.resource(gen_child.ref) + relationship_graph.add_vertex(gen_child) unless relationship_graph.edge?(edge[1], edge[0]) relationship_graph.add_edge(*edge) @@ -188,24 +188,7 @@ class Transaction # See if the resource generates new resources at evaluation time. def eval_generate(resource) - if resource.respond_to?(:eval_generate) - begin - children = resource.eval_generate - rescue => detail - if Puppet[:trace] - puts detail.backtrace - end - resource.err "Failed to generate additional resources during transaction: %s" % - detail - return nil - end - - if children - children.each { |child| child.finish } - @generated += children - return children - end - end + generate_additional_resources(resource, :eval_generate) end # Evaluate a single resource. @@ -245,13 +228,6 @@ class Transaction end end - # Create a child/parent relationship. We do this after everything else because - # we want explicit relationships to be able to override automatic relationships, - # including this one. - if children - copy_relationships(resource, children) - end - # A bit of hackery here -- if skipcheck is true, then we're the # top-level resource. If that's the case, then make sure all of # the changes list this resource as a proxy. This is really only @@ -355,39 +331,34 @@ class Transaction return skip end - # Collect any dynamically generated resources. + # A general method for recursively generating new resources from a + # resource. + def generate_additional_resources(resource, method) + return [] unless resource.respond_to?(method) + begin + made = resource.send(method) + rescue => detail + resource.err "Failed to generate additional resources using '%s': %s" % [method, detail] + end + return [] unless made + made = [made] unless made.is_a?(Array) + made.uniq! + made.each do |res| + @catalog.add_resource(res) { |r| r.finish } + end + make_parent_child_relationship(resource, made) + made + end + + # Collect any dynamically generated resources. This method is called + # before the transaction starts. def generate list = @catalog.vertices - - # Store a list of all generated resources, so that we can clean them up - # after the transaction closes. - @generated = [] - newlist = [] while ! list.empty? list.each do |resource| - if resource.respond_to?(:generate) - begin - made = resource.generate - rescue => detail - resource.err "Failed to generate additional resources: %s" % - detail - end - next unless made - unless made.is_a?(Array) - made = [made] - end - made.uniq! - made.each do |res| - @catalog.add_resource(res) - res.catalog = catalog - newlist << res - @generated << res - res.finish - end - end + newlist += generate_additional_resources(resource, :generate) end - list.clear list = newlist newlist = [] end @@ -429,10 +400,10 @@ class Transaction # this should only be called by a Puppet::Type::Component resource now # and it should only receive an array def initialize(resources) - if resources.is_a?(Puppet::Node::Catalog) + if resources.is_a?(Puppet::Resource::Catalog) @catalog = resources - elsif resources.is_a?(Puppet::PGraph) - raise "Transactions should get catalogs now, not PGraph" + elsif resources.is_a?(Puppet::SimpleGraph) + raise "Transactions should get catalogs now, not SimpleGraph" else raise "Transactions require catalogs" end @@ -534,23 +505,13 @@ class Transaction if Puppet[:report] begin - reportclient().report(report) + report.save() rescue => detail Puppet.err "Reporting failed: %s" % detail end end end - def reportclient - unless defined? @reportclient - @reportclient = Puppet::Network::Client.report.new( - :Server => Puppet[:reportserver] - ) - end - - @reportclient - end - # Roll all completed changes back. def rollback @targets.clear diff --git a/lib/puppet/transaction/report.rb b/lib/puppet/transaction/report.rb index 89da7ed9c..e5b8650bb 100644 --- a/lib/puppet/transaction/report.rb +++ b/lib/puppet/transaction/report.rb @@ -11,7 +11,13 @@ class Puppet::Transaction::Report indirects :report, :terminus_class => :processor attr_accessor :logs, :metrics, :time, :host - + + # This is necessary since Marshall doesn't know how to + # dump hash with default proc (see below @records) + def self.default_format + :yaml + end + def <<(msg) @logs << msg return self diff --git a/lib/puppet/transportable.rb b/lib/puppet/transportable.rb index 41c51fde6..052f6bab1 100644 --- a/lib/puppet/transportable.rb +++ b/lib/puppet/transportable.rb @@ -1,5 +1,5 @@ require 'puppet' -require 'puppet/resource_reference' +require 'puppet/resource/reference' require 'yaml' module Puppet @@ -36,7 +36,7 @@ module Puppet def ref unless defined? @ref - @ref = Puppet::ResourceReference.new(@type, @name) + @ref = Puppet::Resource::Reference.new(@type, @name) end @ref.to_s end @@ -78,6 +78,14 @@ module Puppet ] end + # Create a normalized resource from our TransObject. + def to_resource + result = Puppet::Resource.new(type, name, @params.dup) + result.tag(*tags) + + result + end + def to_yaml_properties instance_variables.reject { |v| %w{@ref}.include?(v) } end @@ -86,12 +94,8 @@ module Puppet ref end - def to_type - if typeklass = Puppet::Type.type(self.type) - return typeklass.create(self) - else - return to_component - end + def to_ral + to_resource.to_ral end end @@ -181,7 +185,7 @@ module Puppet # Create a resource graph from our structure. def to_catalog(clear_on_failure = true) - catalog = Puppet::Node::Catalog.new(Facter.value("hostname")) + catalog = Puppet::Resource::Catalog.new(Facter.value("hostname")) # This should really use the 'delve' method, but this # whole class is going away relatively soon, hopefully, @@ -189,13 +193,13 @@ module Puppet delver = proc do |obj| obj.catalog = catalog unless container = catalog.resource(obj.to_ref) - container = obj.to_type + container = obj.to_ral catalog.add_resource container end obj.each do |child| child.catalog = catalog unless resource = catalog.resource(child.to_ref) - resource = child.to_type + resource = child.to_ral catalog.add_resource resource end @@ -221,11 +225,11 @@ module Puppet def to_ref unless defined? @ref if self.type and self.name - @ref = Puppet::ResourceReference.new(self.type, self.name) + @ref = Puppet::Resource::Reference.new(self.type, self.name) elsif self.type and ! self.name # This is old-school node types - @ref = Puppet::ResourceReference.new("node", self.type) + @ref = Puppet::Resource::Reference.new("node", self.type) elsif ! self.type and self.name - @ref = Puppet::ResourceReference.new("component", self.name) + @ref = Puppet::Resource::Reference.new("component", self.name) else @ref = nil end @@ -233,18 +237,14 @@ module Puppet @ref.to_s if @ref end - def to_type - Puppet.debug("TransBucket '%s' has no type" % @name) unless defined? @type + def to_ral + to_resource.to_ral + end - # Nodes have the same name and type - trans = TransObject.new(to_ref, :component) - if defined? @parameters - @parameters.each { |param,value| - Puppet.debug "Defining %s on %s" % [param, to_ref] - trans[param] = value - } - end - return Puppet::Type::Component.create(trans) + # Create a normalized resource from our TransObject. + def to_resource + params = defined?(@parameters) ? @parameters.dup : {} + Puppet::Resource.new(type, name, params) end def param(param,value) diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index b57c74b95..706ea1386 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -9,7 +9,8 @@ require 'puppet/metatype/manager' require 'puppet/util/errors' require 'puppet/util/log_paths' require 'puppet/util/logging' -require 'puppet/resource_reference' +require 'puppet/resource/reference' +require 'puppet/util/cacher' # see the bottom of the file for the rest of the inclusions @@ -19,6 +20,7 @@ class Type include Puppet::Util::Errors include Puppet::Util::LogPaths include Puppet::Util::Logging + include Puppet::Util::Cacher ############################### # Code related to resource type attributes. @@ -33,12 +35,11 @@ class Type properties() end - # All parameters, in the appropriate order. The namevar comes first, - # then the properties, then the params and metaparams in the order they - # were specified in the files. + # All parameters, in the appropriate order. The namevar comes first, then + # the provider, then the properties, and finally the params and metaparams + # in the order they were specified in the files. def self.allattrs - # now get all of the arguments, in a specific order - # Cache this, since it gets called so many times + # Cache this, since it gets called multiple times namevar = self.namevar order = [namevar] @@ -50,7 +51,7 @@ class Type self.metaparams].flatten.reject { |param| # we don't want our namevar in there multiple times param == namevar - } + } order.flatten! @@ -95,10 +96,6 @@ class Type when @validproperties.include?(attr): :property when @paramhash.include?(attr): :param when @@metaparamhash.include?(attr): :meta - else - raise Puppet::DevError, - "Invalid attribute '%s' for class '%s'" % - [attr, self.name] end end @@ -122,30 +119,6 @@ class Type end end - # A similar function but one that yields the class and type. - # This is mainly so that setdefaults doesn't call quite so many functions. - def self.eachattr(*ary) - if ary.empty? - ary = nil - end - - # We have to do this in a specific order, so that defaults are - # created in that order (e.g., providers should be set up before - # anything else). - allattrs.each do |name| - next unless ary.nil? or ary.include?(name) - if obj = @properties.find { |p| p.name == name } - yield obj, :property - elsif obj = @parameters.find { |p| p.name == name } - yield obj, :param - elsif obj = @@metaparams.find { |p| p.name == name } - yield obj, :meta - else - raise Puppet::DevError, "Could not find parameter %s" % name - end - end - end - def self.eachmetaparam @@metaparams.each { |p| yield p.name } end @@ -184,8 +157,6 @@ class Type end end end - - # If this param handles relationships, store that information end # Is the parameter in question a meta-parameter? @@ -274,15 +245,6 @@ class Type param.isnamevar if options[:namevar] - # These might be enabled later. -# define_method(name) do -# @parameters[name].value -# end -# -# define_method(name.to_s + "=") do |value| -# newparam(param, value) -# end - if param.isnamevar? @namevar = param.name end @@ -347,14 +309,6 @@ class Type @properties << prop end -# define_method(name) do -# @parameters[name].should -# end -# -# define_method(name.to_s + "=") do |value| -# newproperty(name, :should => value) -# end - return prop end @@ -423,38 +377,6 @@ class Type end end - # fix any namevar => param translations - def argclean(oldhash) - # This duplication is here because it might be a transobject. - hash = oldhash.dup.to_hash - - if hash.include?(:resource) - hash.delete(:resource) - end - namevar = self.class.namevar - - # Do a simple translation for those cases where they've passed :name - # but that's not our namevar - if hash.include? :name and namevar != :name - if hash.include? namevar - raise ArgumentError, "Cannot provide both name and %s" % namevar - end - hash[namevar] = hash[:name] - hash.delete(:name) - end - - # Make sure we have a name, one way or another - unless hash.include? namevar - if defined? @title and @title - hash[namevar] = @title - else - raise Puppet::Error, "Was not passed a namevar or title" - end - end - - return hash - end - # Return either the attribute alias or the attribute. def attr_alias(name) name = symbolize(name) @@ -489,7 +411,7 @@ class Type name = attr_alias(name) unless self.class.validattr?(name) - raise TypeError.new("Invalid parameter %s(%s)" % [name, name.inspect]) + fail("Invalid parameter %s(%s)" % [name, name.inspect]) end if name == :name @@ -512,7 +434,7 @@ class Type name = attr_alias(name) unless self.class.validattr?(name) - raise TypeError.new("Invalid parameter %s" % [name]) + fail("Invalid parameter %s" % [name]) end if name == :name @@ -551,6 +473,12 @@ class Type } end + # Let the catalog determine whether a given cached value is + # still valid or has expired. + def expirer + catalog + end + # retrieve the 'should' value for a specified property def should(name) name = attr_alias(name) @@ -601,10 +529,7 @@ class Type # return the value of a parameter def parameter(name) - unless name.is_a? Symbol - name = name.intern - end - return @parameters[name].value + @parameters[name.to_sym] end # Is the named property defined? @@ -615,8 +540,9 @@ class Type return @parameters.include?(name) end - # return an actual type by name; to return the value, use 'inst[name]' - # FIXME this method should go away + # Return an actual property instance by name; to return the value, use 'resource[param]' + # LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method, + # this one should probably go away at some point. def property(name) if obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property) return obj @@ -625,36 +551,21 @@ class Type end end -# def set(name, value) -# send(name.to_s + "=", value) -# end -# -# def get(name) -# send(name) -# end - # For any parameters or properties that have defaults and have not yet been # set, set them now. This method can be handed a list of attributes, # and if so it will only set defaults for those attributes. - def setdefaults(*ary) - #self.class.eachattr(*ary) { |klass, type| - self.class.eachattr(*ary) { |klass, type| - # not many attributes will have defaults defined, so we short-circuit - # those away - next unless klass.method_defined?(:default) - next if @parameters[klass.name] - - next unless obj = self.newattr(klass) - - # We have to check for nil values, not "truth", so we allow defaults - # to false. - value = obj.default and ! value.nil? - if ! value.nil? - obj.value = value - else - @parameters.delete(obj.name) - end - } + def set_default(attr) + return unless klass = self.class.attrclass(attr) + return unless klass.method_defined?(:default) + return if @parameters.include?(klass.name) + + return unless parameter = newattr(klass.name) + + if value = parameter.default and ! value.nil? + parameter.value = value + else + @parameters.delete(parameter.name) + end end # Convert our object to a hash. This just includes properties. @@ -706,7 +617,7 @@ class Type ############################### # Code related to the closure-like behaviour of the resource classes. - attr_writer :implicit + attr_accessor :implicit # Is this type's name isomorphic with the object? That is, if the # name conflicts, does it necessarily mean that the objects conflict? @@ -756,6 +667,9 @@ class Type ############################### # Code related to the container behaviour. + + # this is a retarded hack method to get around the difference between + # component children and file children def self.depthfirst? if defined? @depthfirst return @depthfirst @@ -768,19 +682,6 @@ class Type self.class.depthfirst? end - # Add a hook for testing for recursion. - def parentof?(child) - if (self == child) - debug "parent is equal to child" - return true - elsif defined? @parent and @parent.parentof?(child) - debug "My parent is parent of child" - return true - else - return false - end - end - # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) @@ -790,7 +691,6 @@ class Type obj.remove end @parameters.clear - self.class.delete(self) @parent = nil @@ -814,13 +714,6 @@ class Type raise Puppet::Error, "Provider %s is not functional on this platform" % provider.class.name end end - #Puppet.err "Evaluating %s" % self.path.join(":") - unless defined? @evalcount - self.err "No evalcount defined on '%s' of type '%s'" % - [self.title,self.class] - @evalcount = 0 - end - @evalcount += 1 # this only operates on properties, not properties + children # it's important that we call retrieve() on the type instance, @@ -977,18 +870,15 @@ class Type # Code related to managing resource instances. require 'puppet/transportable' - # Make 'new' private, so people have to use create instead. - class << self - private :new - end - # retrieve a named instance of the current type def self.[](name) + raise "Global resource access is deprecated" @objects[name] || @aliases[name] end # add an instance by name to the class list of instances def self.[]=(name,object) + raise "Global resource storage is deprecated" newobj = nil if object.is_a?(Puppet::Type) newobj = object @@ -1020,6 +910,7 @@ class Type # Create an alias. We keep these in a separate hash so that we don't encounter # the objects multiple times when iterating over them. def self.alias(name, obj) + raise "Global resource aliasing is deprecated" if @objects.include?(name) unless @objects[name] == obj raise Puppet::Error.new( @@ -1043,6 +934,7 @@ class Type # remove all of the instances of a single type def self.clear + raise "Global resource removal is deprecated" if defined? @objects @objects.each do |name, obj| obj.remove(true) @@ -1057,96 +949,14 @@ class Type # Force users to call this, so that we can merge objects if # necessary. def self.create(args) - # Don't modify the original hash; instead, create a duplicate and modify it. - # We have to dup and use the ! so that it stays a TransObject if it is - # one. - hash = args.dup - symbolizehash!(hash) - - # If we're the base class, then pass the info on appropriately - if self == Puppet::Type - type = nil - if hash.is_a? Puppet::TransObject - type = hash.type - else - # If we're using the type to determine object type, then delete it - if type = hash[:type] - hash.delete(:type) - end - end - - # If they've specified a type and called on the base, then - # delegate to the subclass. - if type - if typeklass = self.type(type) - return typeklass.create(hash) - else - raise Puppet::Error, "Unknown type %s" % type - end - else - raise Puppet::Error, "No type found for %s" % hash.inspect - end - end - - # Handle this new object being implicit - implicit = hash[:implicit] || false - if hash.include?(:implicit) - hash.delete(:implicit) - end - - name = nil - unless hash.is_a? Puppet::TransObject - hash = self.hash2trans(hash) - end - - # XXX This will have to change when transobjects change to using titles - title = hash.name - - # if the object already exists - if self.isomorphic? and retobj = self[title] - # if only one of our objects is implicit, then it's easy to see - # who wins -- the non-implicit one. - if retobj.implicit? and ! implicit - Puppet.notice "Removing implicit %s" % retobj.title - # Remove all of the objects, but do not remove their subscriptions. - retobj.remove(false) - - # now pass through and create the new object - elsif implicit - Puppet.debug "Ignoring implicit %s[%s]" % [self.name, title] - return nil - else - raise Puppet::Error, "%s is already being managed" % retobj.ref - end - end - - # create it anew - # if there's a failure, destroy the object if it got that far, but raise - # the error. - begin - obj = new(hash) - rescue => detail - Puppet.err "Could not create %s: %s" % [title, detail.to_s] - if obj - obj.remove(true) - elsif obj = self[title] - obj.remove(true) - end - raise - end - - if implicit - obj.implicit = true - end - - # Store the object by title - self[obj.title] = obj - - return obj + # LAK:DEP Deprecation notice added 12/17/2008 + Puppet.warning "Puppet::Type.create is deprecated; use Puppet::Type.new" + new(args) end # remove a specified object def self.delete(resource) + raise "Global resource removal is deprecated" return unless defined? @objects if @objects.include?(resource.title) @objects.delete(resource.title) @@ -1167,6 +977,7 @@ class Type # iterate across each of the type's instances def self.each + raise "Global resource iteration is deprecated" return unless defined? @objects @objects.each { |name,instance| yield instance @@ -1175,55 +986,10 @@ class Type # does the type have an object with the given name? def self.has_key?(name) + raise "Global resource access is deprecated" return @objects.has_key?(name) end - # Convert a hash to a TransObject. - def self.hash2trans(hash) - title = nil - if hash.include? :title - title = hash[:title] - hash.delete(:title) - elsif hash.include? self.namevar - title = hash[self.namevar] - hash.delete(self.namevar) - - if hash.include? :name - raise ArgumentError, "Cannot provide both name and %s to %s" % - [self.namevar, self.name] - end - elsif hash[:name] - title = hash[:name] - hash.delete :name - end - - if catalog = hash[:catalog] - hash.delete(:catalog) - end - - raise(Puppet::Error, "You must specify a title for objects of type %s" % self.to_s) unless title - - if hash.include? :type - unless self.validattr? :type - hash.delete :type - end - end - - # okay, now make a transobject out of hash - begin - trans = Puppet::TransObject.new(title, self.name.to_s) - trans.catalog = catalog if catalog - hash.each { |param, value| - trans[param] = value - } - rescue => detail - raise Puppet::Error, "Could not create %s: %s" % - [name, detail] - end - - return trans - end - # Retrieve all known instances. Either requires providers or must be overridden. def self.instances unless defined?(@providers) and ! @providers.empty? @@ -1234,10 +1000,6 @@ class Type provider_instances = {} providers_by_source.collect do |provider| provider.instances.collect do |instance| - # First try to get the resource if it already exists - # Skip instances that map to a managed resource with a different provider - next if resource = self[instance.name] and resource.provider.class != instance.class - # We always want to use the "first" provider instance we find, unless the resource # is already managed and has a different provider set if other = provider_instances[instance.name] @@ -1247,12 +1009,7 @@ class Type end provider_instances[instance.name] = instance - if resource - resource.provider = instance - resource - else - create(:name => instance.name, :provider => instance, :check => :all) - end + create(:name => instance.name, :provider => instance, :check => :all) end end.flatten.compact end @@ -1269,6 +1026,47 @@ class Type end.compact end + # Convert a simple hash into a Resource instance. This is a convenience method, + # so people can create RAL resources with a hash and get the same behaviour + # as we get internally when we use Resource instances. + # This should only be used directly from Ruby -- it's not used when going through + # normal Puppet usage. + def self.hash2resource(hash) + hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result } + + if title = hash[:title] + hash.delete(:title) + else + if self.namevar != :name + if hash.include?(:name) and hash.include?(self.namevar) + raise Puppet::Error, "Cannot provide both name and %s to resources of type %s" % [self.namevar, self.name] + end + if title = hash[self.namevar] + hash.delete(self.namevar) + end + end + + unless title ||= hash[:name] + raise Puppet::Error, "You must specify a name or title for resources" + end + end + + + # Now create our resource. + resource = Puppet::Resource.new(self.name, title) + [:catalog, :implicit].each do |attribute| + if value = hash[attribute] + hash.delete(attribute) + resource.send(attribute.to_s + "=", value) + end + end + + hash.each do |param, value| + resource[param] = value + end + return resource + end + # Create the path for logging and such. def pathbuilder if p = parent @@ -1362,47 +1160,6 @@ class Type } end end - - # We've got four relationship metaparameters, so this method is used - # to reduce code duplication between them. - def munge_relationship(param, values) - # We need to support values passed in as an array or as a - # resource reference. - result = [] - - # 'values' could be an array or a reference. If it's an array, - # it could be an array of references or an array of arrays. - if values.is_a?(Puppet::Type) - result << [values.class.name, values.title] - else - unless values.is_a?(Array) - devfail "Relationships must be resource references" - end - if values[0].is_a?(String) or values[0].is_a?(Symbol) - # we're a type/title array reference - values[0] = symbolize(values[0]) - result << values - else - # we're an array of stuff - values.each do |value| - if value.is_a?(Puppet::Type) - result << [value.class.name, value.title] - elsif value.is_a?(Array) - value[0] = symbolize(value[0]) - result << value - else - devfail "Invalid relationship %s" % value.inspect - end - end - end - end - - if existing = self[param] - result = existing + result - end - - result - end newmetaparam(:loglevel) do desc "Sets the level that information will be logged. @@ -1476,9 +1233,6 @@ class Type next end - # LAK:FIXME Old-school, add the alias to the class. - @resource.class.alias(other, @resource) - # Newschool, add it to the catalog. @resource.catalog.alias(@resource, other) end @@ -1519,16 +1273,22 @@ class Type @subclasses << sub end - def munge(rels) - @resource.munge_relationship(self.class.name, rels) + def munge(references) + references = [references] unless references.is_a?(Array) + references.collect do |ref| + if ref.is_a?(Puppet::Resource::Reference) + ref + else + Puppet::Resource::Reference.new(ref) + end + end end def validate_relationship - @value.each do |value| - unless @resource.catalog.resource(*value) + @value.each do |ref| + unless @resource.catalog.resource(ref.to_s) description = self.class.direction == :in ? "dependency" : "dependent" - fail Puppet::Error, "Could not find %s %s[%s] for %s" % - [description, value[0].to_s.capitalize, value[1], resource.ref] + fail "Could not find %s %s for %s" % [description, ref.to_s, resource.ref] end end end @@ -1542,26 +1302,23 @@ class Type # which resource is applied first and which resource is considered # to be the event generator. def to_edges - @value.collect do |value| - # we just have a name and a type, and we need to convert it - # to an object... - tname, name = value - reference = Puppet::ResourceReference.new(tname, name) + @value.collect do |reference| + reference.catalog = resource.catalog # Either of the two retrieval attempts could have returned # nil. - unless object = reference.resolve + unless related_resource = reference.resolve self.fail "Could not retrieve dependency '%s' of %s" % [reference, @resource.ref] end # Are we requiring them, or vice versa? See the method docs # for futher info on this. if self.class.direction == :in - source = object + source = related_resource target = @resource else source = @resource - target = object + target = related_resource end if method = self.class.callback @@ -1569,12 +1326,12 @@ class Type :event => self.class.events, :callback => method } - self.debug("subscribes to %s" % [object.ref]) + self.debug("subscribes to %s" % [related_resource.ref]) else # If there's no callback, there's no point in even adding # a label. subargs = nil - self.debug("requires %s" % [object.ref]) + self.debug("requires %s" % [related_resource.ref]) end rel = Puppet::Relationship.new(source, target, subargs) @@ -1740,47 +1497,6 @@ class Type return @defaultprovider end - # Convert a hash, as provided by, um, a provider, into an instance of self. - def self.hash2obj(hash) - obj = nil - - namevar = self.namevar - unless hash.include?(namevar) and hash[namevar] - raise Puppet::DevError, "Hash was not passed with namevar" - end - - # if the obj already exists with that name... - if obj = self[hash[namevar]] - # We're assuming here that objects with the same name - # are the same object, which *should* be the case, assuming - # we've set up our naming stuff correctly everywhere. - - # Mark found objects as present - hash.each { |param, value| - if property = obj.property(param) - elsif val = obj[param] - obj[param] = val - else - # There is a value on disk, but it should go away - obj[param] = :absent - end - } - else - # create a new obj, since no existing one seems to - # match - obj = self.create(namevar => hash[namevar]) - - # We can't just pass the hash in at object creation time, - # because it sets the should value, not the is value. - hash.delete(namevar) - hash.each { |param, value| - obj[param] = value unless obj.add_property_parameter(param) - } - end - - return obj - end - # Retrieve a provider by name. def self.provider(name) name = Puppet::Util.symbolize(name) @@ -1960,11 +1676,14 @@ class Type # Figure out of there are any objects we can automatically add as # dependencies. - def autorequire + def autorequire(rel_catalog = nil) + rel_catalog ||= catalog + raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog + reqs = [] self.class.eachautorequire { |type, block| # Ignore any types we can't find, although that would be a bit odd. - next unless typeobj = Puppet.type(type) + next unless typeobj = Puppet::Type.type(type) # Retrieve the list of names from the block. next unless list = self.instance_eval(&block) @@ -1978,15 +1697,15 @@ class Type # Support them passing objects directly, to save some effort. unless dep.is_a? Puppet::Type # Skip autorequires that we aren't managing - unless dep = typeobj[dep] + unless dep = rel_catalog.resource(type, dep) next end end - + reqs << Puppet::Relationship.new(dep, self) } } - + return reqs end @@ -1999,61 +1718,6 @@ class Type end end.flatten.reject { |r| r.nil? } end - - # Does this resource have a relationship with the other? We have to - # check each object for both directions of relationship. - def requires?(other) - them = [other.class.name, other.title] - me = [self.class.name, self.title] - self.class.relationship_params.each do |param| - case param.direction - when :in: return true if v = self[param.name] and v.include?(them) - when :out: return true if v = other[param.name] and v.include?(me) - end - end - return false - end - - # we've received an event - # we only support local events right now, so we can pass actual - # objects around, including the transaction object - # the assumption here is that container objects will pass received - # methods on to contained objects - # i.e., we don't trigger our children, our refresh() method calls - # refresh() on our children - def trigger(event, source) - trans = event.transaction - if @callbacks.include?(source) - [:ALL_EVENTS, event.event].each { |eventname| - if method = @callbacks[source][eventname] - if trans.triggered?(self, method) > 0 - next - end - if self.respond_to?(method) - self.send(method) - end - - trans.triggered(self, method) - end - } - end - end - - # Unsubscribe from a given object, possibly with a specific event. - def unsubscribe(object, event = nil) - # First look through our own relationship params - [:require, :subscribe].each do |param| - if values = self[param] - newvals = values.reject { |d| - d == [object.class.name, object.title] - } - if newvals.length != values.length - self.delete(param) - self[param] = newvals - end - end - end - end ############################### # All of the scheduling code. @@ -2062,9 +1726,14 @@ class Type # the instantiation phase, so that the schedule can be anywhere in the # file. def schedule + unless catalog + warning "Cannot schedule without a schedule-containing catalog" + return nil + end + unless defined? @schedule if name = self[:schedule] - if sched = Puppet.type(:schedule)[name] + if sched = catalog.resource(:schedule, name) @schedule = sched else self.fail "Could not find schedule %s" % name @@ -2227,147 +1896,84 @@ class Type public - def initvars - @evalcount = 0 - @tags = [] - - # callbacks are per object and event - @callbacks = Hash.new { |chash, key| - chash[key] = {} - } - - # properties and parameters are treated equivalently from the outside: - # as name-value pairs (using [] and []=) - # internally, however, parameters are merely a hash, while properties - # point to Property objects - # further, the lists of valid properties and parameters are defined - # at the class level - unless defined? @parameters - @parameters = {} - end - - # keeping stats for the total number of changes, and how many were - # completely sync'ed - # this isn't really sufficient either, because it adds lots of special - # cases such as failed changes - # it also doesn't distinguish between changes from the current transaction - # vs. changes over the process lifetime - @totalchanges = 0 - @syncedchanges = 0 - @failedchanges = 0 - - @inited = true - end + attr_reader :original_parameters # initialize the type instance - def initialize(hash) - unless defined? @inited - self.initvars - end - namevar = self.class.namevar - - orighash = hash - - # If we got passed a transportable object, we just pull a bunch of info - # directly from it. This is the main object instantiation mechanism. - if hash.is_a?(Puppet::TransObject) - # XXX This will need to change when transobjects change to titles. - self.title = hash.name - - #self[:name] = hash[:name] - [:file, :line, :tags, :catalog].each { |getter| - if hash.respond_to?(getter) - setter = getter.to_s + "=" - if val = hash.send(getter) - self.send(setter, val) - end - end - } + def initialize(resource) + raise Puppet::DevError, "Got TransObject instead of Resource or hash" if resource.is_a?(Puppet::TransObject) + resource = self.class.hash2resource(resource) unless resource.is_a?(Puppet::Resource) - hash = hash.to_hash + # The list of parameter/property instances. + @parameters = {} + + # Set the title first, so any failures print correctly. + if resource.type.to_s.downcase.to_sym == self.class.name + self.title = resource.title else - if hash[:title] - @title = hash[:title] - hash.delete(:title) - end + # This should only ever happen for components + self.title = resource.ref end - # Before anything else, set our parent if it was included - if hash.include?(:parent) - @parent = hash[:parent] - hash.delete(:parent) + [:file, :line, :catalog, :implicit].each do |getter| + setter = getter.to_s + "=" + if val = resource.send(getter) + self.send(setter, val) + end end - # Munge up the namevar stuff so we only have one value. - hash = self.argclean(hash) + @tags = resource.tags - # Let's do the name first, because some things need to happen once - # we have the name but before anything else + @original_parameters = resource.to_hash - attrs = self.class.allattrs + set_name(@original_parameters) - if hash.include?(namevar) - #self.send(namevar.to_s + "=", hash[namevar]) - self[namevar] = hash[namevar] - hash.delete(namevar) - if attrs.include?(namevar) - attrs.delete(namevar) - else - self.devfail "My namevar isn't a valid attribute...?" - end - else - self.devfail "I was not passed a namevar" - end + set_default(:provider) - # If the name and title differ, set up an alias - if self.name != self.title - if obj = self.class[self.name] - if self.class.isomorphic? - raise Puppet::Error, "%s already exists with name %s" % - [obj.title, self.name] - end - else - self.class.alias(self.name, self) - end - end + set_parameters(@original_parameters) - if hash.include?(:provider) - self[:provider] = hash[:provider] - hash.delete(:provider) - else - setdefaults(:provider) - end + self.validate if self.respond_to?(:validate) + end - # This is all of our attributes except the namevar. - attrs.each { |attr| - if hash.include?(attr) - begin + private + + # Set our resource's name. + def set_name(hash) + n = self.class.namevar + self[n] = hash[n] + hash.delete(n) + end + + # Set all of the parameters from a hash, in the appropriate order. + def set_parameters(hash) + # Use the order provided by allattrs, but add in any + # extra attributes from the resource so we get failures + # on invalid attributes. + no_values = [] + (self.class.allattrs + hash.keys).uniq.each do |attr| + begin + # Set any defaults immediately. This is mostly done so + # that the default provider is available for any other + # property validation. + if hash.has_key?(attr) self[attr] = hash[attr] - rescue ArgumentError, Puppet::Error, TypeError - raise - rescue => detail - error = Puppet::DevError.new( "Could not set %s on %s: %s" % [attr, self.class.name, detail]) - error.set_backtrace(detail.backtrace) - raise error + else + no_values << attr end - hash.delete attr + rescue ArgumentError, Puppet::Error, TypeError + raise + rescue => detail + error = Puppet::DevError.new( "Could not set %s on %s: %s" % [attr, self.class.name, detail]) + error.set_backtrace(detail.backtrace) + raise error end - } - - # Set all default values. - self.setdefaults - - if hash.length > 0 - self.debug hash.inspect - self.fail("Class %s does not accept argument(s) %s" % - [self.class.name, hash.keys.join(" ")]) end - - if self.respond_to?(:validate) - self.validate + no_values.each do |attr| + set_default(attr) end end + public + # Set up all of our autorequires. def finish # Scheduling has to be done when the whole config is instantiated, so @@ -2395,14 +2001,6 @@ class Type #@cache[name] = value end -# def set(name, value) -# send(name.to_s + "=", value) -# end -# -# def get(name) -# send(name) -# end - # For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. def name diff --git a/lib/puppet/type/component.rb b/lib/puppet/type/component.rb index 356205089..724e2985a 100644 --- a/lib/puppet/type/component.rb +++ b/lib/puppet/type/component.rb @@ -1,127 +1,52 @@ -# the object allowing us to build complex structures -# this thing contains everything else, including itself - require 'puppet' require 'puppet/type' require 'puppet/transaction' -require 'puppet/pgraph' Puppet::Type.newtype(:component) do include Enumerable - attr_accessor :children newparam(:name) do desc "The name of the component. Generally optional." isnamevar end - newparam(:type) do - desc "The type that this component maps to. Generally some kind of - class from the language." - - defaultto "component" + # Override how parameters are handled so that we support the extra + # parameters that are used with defined resource types. + def [](param) + return super if self.class.validattr?(param) + @extra_parameters[param.to_sym] end - # Remove a child from the component. - def delete(child) - if @children.include?(child) - @children.delete(child) - return true - else - return super - end - end - - # Recurse deeply through the tree, but only yield types, not properties. - def delve(&block) - self.each do |obj| - if obj.is_a?(self.class) - obj.delve(&block) - end - end - block.call(self) - end - - # Return each child in turn. - def each - @children.each { |child| yield child } - end - - # Do all of the polishing off, mostly doing autorequires and making - # dependencies. This will get run once on the top-level component, - # and it will do everything necessary. - def finalize - started = {} - finished = {} - - # First do all of the finish work, which mostly involves - self.delve do |object| - # Make sure we don't get into loops - if started.has_key?(object) - debug "Already finished %s" % object.title - next - else - started[object] = true - end - unless finished.has_key?(object) - object.finish - finished[object] = true - end - end - - @finalized = true - end - - def finalized? - if defined? @finalized - return @finalized - else - return false - end + # Override how parameters are handled so that we support the extra + # parameters that are used with defined resource types. + def []=(param, value) + return super if self.class.validattr?(param) + @extra_parameters[param.to_sym] = value end # Initialize a new component def initialize(*args) - @children = [] - super - - @reference = Puppet::ResourceReference.new(:component, @title) - - unless self.class[@reference.to_s] - self.class.alias(@reference.to_s, self) - end - end - - def initvars + @extra_parameters = {} super - @children = [] - end - # Add a hook for testing for recursion. - def parentof?(child) - if super(child) - return true - elsif @children.include?(child) - debug "child is already in children array" - return true - else - return false + if catalog and ! catalog.resource(ref) + catalog.alias(self, ref) end end # Component paths are special because they function as containers. def pathbuilder - if @reference.type == "Class" + if reference.type == "Class" # 'main' is the top class, so we want to see '//' instead of # its name. - if @reference.title == "main" + if reference.title == "main" myname = "" else - myname = @reference.title + myname = reference.title end else - myname = @reference.to_s + myname = reference.to_s end if p = self.parent return [p.pathbuilder, myname] @@ -131,12 +56,16 @@ Puppet::Type.newtype(:component) do end def ref - @reference.to_s + reference.to_s end # We want our title to just be the whole reference, rather than @title. def title - @reference.to_s + ref + end + + def title=(str) + @reference = Puppet::Resource::Reference.new(str) end def refresh @@ -149,6 +78,10 @@ Puppet::Type.newtype(:component) do end def to_s - @reference.to_s + reference.to_s end + + private + + attr_reader :reference end diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index b6396b0bf..3d721ac69 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -28,7 +28,7 @@ module Puppet validate do |value| unless value =~ /^#{File::SEPARATOR}/ - raise Puppet::Error, "File paths must be fully qualified" + raise Puppet::Error, "File paths must be fully qualified, not '%s'" % value end end end @@ -78,9 +78,8 @@ module Puppet munge do |value| # I don't really know how this is happening. - if value.is_a?(Array) - value = value.shift - end + value = value.shift if value.is_a?(Array) + case value when false, "false", :false: false @@ -92,7 +91,7 @@ module Puppet # We can't depend on looking this up right now, # we have to do it after all of the objects # have been instantiated. - if bucketobj = Puppet::Type.type(:filebucket)[value] + if resource.catalog and bucketobj = resource.catalog.resource(:filebucket, value) @resource.bucket = bucketobj.bucket bucketobj.title else @@ -156,8 +155,6 @@ module Puppet engine, so shell metacharacters are fully supported, e.g. ``[a-z]*``. Matches that would descend into the directory structure are ignored, e.g., ``*/*``." - - defaultto false validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false @@ -243,6 +240,9 @@ module Puppet CREATORS.each do |param| count += 1 if self.should(param) end + if @parameters.include?(:source) + count += 1 + end if count > 1 self.fail "You cannot specify more than one of %s" % CREATORS.collect { |p| p.to_s}.join(", ") end @@ -268,7 +268,7 @@ module Puppet obj[:check] = :all files << obj else - files << self.create( + files << self.new( :name => path, :check => :all ) end @@ -278,11 +278,6 @@ module Puppet @depthfirst = false - - def argument?(arg) - @arghash.include?(arg) - end - # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) @@ -301,33 +296,32 @@ module Puppet return asuser end + # Does the file currently exist? Just checks for whether + # we have a stat + def exist? + stat ? true : false + end + # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish - # Let's cache these values, since there should really only be - # a couple of these buckets - @@filebuckets ||= {} - # Look up our bucket, if there is one if bucket = self.bucket case bucket when String: - if obj = @@filebuckets[bucket] - # This sets the @value on :backup, too - self.bucket = obj + if catalog and obj = catalog.resource(:filebucket, bucket) + self.bucket = obj.bucket elsif bucket == "puppet" obj = Puppet::Network::Client.client(:Dipper).new( :Path => Puppet[:clientbucketdir] ) self.bucket = obj - @@filebuckets[bucket] = obj - elsif obj = Puppet::Type.type(:filebucket).bucket(bucket) - @@filebuckets[bucket] = obj - self.bucket = obj else - self.fail "Could not find filebucket %s" % bucket + self.fail "Could not find filebucket '%s'" % bucket end when Puppet::Network::Client.client(:Dipper): # things are hunky-dorey + when Puppet::Type::Filebucket # things are hunky-dorey + self.bucket = bucket.bucket else self.fail "Invalid bucket type %s" % bucket.class end @@ -337,7 +331,23 @@ module Puppet # Create any children via recursion or whatever. def eval_generate - recurse() + return [] unless self.recurse? + + recurse + #recurse.reject do |resource| + # catalog.resource(:file, resource[:path]) + #end.each do |child| + # catalog.add_resource child + # catalog.relationship_graph.add_edge self, child + #end + end + + def flush + # We want to make sure we retrieve metadata anew on each transaction. + @parameters.each do |name, param| + param.flush if param.respond_to?(:flush) + end + @stat = nil end # Deal with backups. @@ -432,28 +442,12 @@ module Puppet end when "link": return true else - self.notice "Cannot backup files of type %s" % - File.stat(file).ftype + self.notice "Cannot backup files of type %s" % File.stat(file).ftype return false end end - def handleignore(children) - return children unless self[:ignore] - self[:ignore].each { |ignore| - ignored = [] - Dir.glob(File.join(self[:path],ignore), File::FNM_DOTMATCH) { - |match| ignored.push(File.basename(match)) - } - children = children - ignored - } - return children - end - def initialize(hash) - # Store a copy of the arguments for later. - tmphash = hash.to_hash - # Used for caching clients @clients = {} @@ -464,194 +458,31 @@ module Puppet @title.sub!(/\/$/, "") unless @title == "/" - # Clean out as many references to any file paths as possible. - # This was the source of many, many bugs. - @arghash = tmphash - @arghash.delete(self.class.namevar) - - [:source, :parent].each do |param| - if @arghash.include?(param) - @arghash.delete(param) - end - end - @stat = nil end - - # Build a recursive map of a link source - def linkrecurse(recurse) - target = @parameters[:target].should - - method = :lstat - if self[:links] == :follow - method = :stat - end - - targetstat = nil - unless FileTest.exist?(target) - return - end - # Now stat our target - targetstat = File.send(method, target) - unless targetstat.ftype == "directory" - return - end - - # Now that we know our corresponding target is a directory, - # change our type - self[:ensure] = :directory - - unless FileTest.readable? target - self.notice "Cannot manage %s: permission denied" % self.name - return - end - - children = Dir.entries(target).reject { |d| d =~ /^\.+$/ } - - # Get rid of ignored children - if @parameters.include?(:ignore) - children = handleignore(children) - end - - added = [] - children.each do |file| - Dir.chdir(target) do - longname = File.join(target, file) - - # Files know to create directories when recursion - # is enabled and we're making links - args = { - :recurse => recurse, - :ensure => longname - } - - if child = self.newchild(file, true, args) - added << child - end - end - end - - added - end - - # Build up a recursive map of what's around right now - def localrecurse(recurse) - unless FileTest.exist?(self[:path]) and self.stat.directory? - #self.info "%s is not a directory; not recursing" % - # self[:path] - return - end - - unless FileTest.readable? self[:path] - self.notice "Cannot manage %s: permission denied" % self.name - return - end - - children = Dir.entries(self[:path]) - - #Get rid of ignored children - if @parameters.include?(:ignore) - children = handleignore(children) - end - - added = [] - children.each { |file| - file = File.basename(file) - next if file =~ /^\.\.?$/ # skip . and .. - options = {:recurse => recurse} - - if child = self.newchild(file, true, options) - added << child - end - } - - added - end # Create a new file or directory object as a child to the current # object. - def newchild(path, local, hash = {}) - raise(Puppet::DevError, "File recursion cannot happen without a catalog") unless catalog - - # make local copy of arguments - args = symbolize_options(@arghash) - - # There's probably a better way to do this, but we don't want - # to pass this info on. - if v = args[:ensure] - v = symbolize(v) - args.delete(:ensure) - end - - if path =~ %r{^#{File::SEPARATOR}} - self.devfail( - "Must pass relative paths to PFile#newchild()" - ) - else - path = File.join(self[:path], path) - end + def newchild(path) + full_path = File.join(self[:path], path) - args[:path] = path + # Add some new values to our original arguments -- these are the ones + # set at initialization. We specifically want to exclude any param + # values set by the :source property or any default values. + # LAK:NOTE This is kind of silly, because the whole point here is that + # the values set at initialization should live as long as the resource + # but values set by default or by :source should only live for the transaction + # or so. Unfortunately, we don't have a straightforward way to manage + # the different lifetimes of this data, so we kludge it like this. + # The right-side hash wins in the merge. + options = @original_parameters.merge(:path => full_path, :implicit => true).reject { |param, value| value.nil? } - unless hash.include?(:recurse) - if args.include?(:recurse) - if args[:recurse].is_a?(Integer) - args[:recurse] -= 1 # reduce the level of recursion - end - end - - end - - hash.each { |key,value| - args[key] = value - } - - child = nil - - # The child might already exist because 'localrecurse' runs - # before 'sourcerecurse'. I could push the override stuff into - # a separate method or something, but the work is the same other - # than this last bit, so it doesn't really make sense. - if child = catalog.resource(:file, path) - unless child.parent.object_id == self.object_id - self.debug "Not managing more explicit file %s" % - path - return nil - end - - # This is only necessary for sourcerecurse, because we might have - # created the object with different 'should' values than are - # set remotely. - unless local - args.each { |var,value| - next if var == :path - next if var == :name - - # behave idempotently - unless child.should(var) == value - child[var] = value - end - } - end - return nil - else # create it anew - #notice "Creating new file with args %s" % args.inspect - args[:parent] = self - begin - # This method is used by subclasses of :file, so use the class name rather than hard-coding - # :file. - return nil unless child = catalog.create_implicit_resource(self.class.name, args) - rescue => detail - self.notice "Cannot manage: %s" % [detail] - return nil - end + # These should never be passed to our children. + [:parent, :ensure, :recurse, :target].each do |param| + options.delete(param) if options.include?(param) end - # LAK:FIXME This shouldn't be necessary, but as long as we're - # modeling the relationship graph specifically, it is. - catalog.relationship_graph.add_edge self, child - - return child + return self.class.new(options) end # Files handle paths specially, because they just lengthen their @@ -681,70 +512,118 @@ module Puppet @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") end - # Recurse into the directory. This basically just calls 'localrecurse' - # and maybe 'sourcerecurse', returning the collection of generated - # files. + # Recursively generate a list of file resources, which will + # be used to copy remote files, manage local files, and/or make links + # to map to another directory. def recurse - # are we at the end of the recursion? - return unless self.recurse? - - recurse = self[:recurse] - # we might have a string, rather than a number - if recurse.is_a?(String) - if recurse =~ /^[0-9]+$/ - recurse = Integer(recurse) - else # anything else is infinite recursion - recurse = true - end + children = recurse_local + + if self[:target] + recurse_link(children) + elsif self[:source] + recurse_remote(children) end - if recurse.is_a?(Integer) - recurse -= 1 + return children.values.sort { |a, b| a[:path] <=> b[:path] } + end + + # A simple method for determining whether we should be recursing. + def recurse? + return false unless @parameters.include?(:recurse) + + val = @parameters[:recurse].value + + if val and (val == true or val > 0) + return true + else + return false end - - children = [] - - # We want to do link-recursing before normal recursion so that all - # of the target stuff gets copied over correctly. - if @parameters.include? :target and ret = self.linkrecurse(recurse) - children += ret + end + + # Recurse the target of the link. + def recurse_link(children) + perform_recursion(self[:target]).each do |meta| + if meta.relative_path == "." + self[:ensure] = :directory + next + end + + children[meta.relative_path] ||= newchild(meta.relative_path) + if meta.ftype == "directory" + children[meta.relative_path][:ensure] = :directory + else + children[meta.relative_path][:ensure] = :link + children[meta.relative_path][:target] = meta.full_path + end end - if ret = self.localrecurse(recurse) - children += ret + children + end + + # Recurse the file itself, returning a Metadata instance for every found file. + def recurse_local + result = perform_recursion(self[:path]) + return {} unless result + result.inject({}) do |hash, meta| + next hash if meta.relative_path == "." + + hash[meta.relative_path] = newchild(meta.relative_path) + hash end + end - # These will be files pulled in by the file source - sourced = false - if @parameters.include?(:source) - ret, sourced = self.sourcerecurse(recurse) - if ret - children += ret + # Recurse against our remote file. + def recurse_remote(children) + sourceselect = self[:sourceselect] + + total = self[:source].collect do |source| + next unless result = perform_recursion(source) + return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory" + result.each { |data| data.source = "%s/%s" % [source, data.relative_path] } + break result if result and ! result.empty? and sourceselect == :first + result + end.flatten + + # This only happens if we have sourceselect == :all + unless sourceselect == :first + found = [] + total.reject! do |data| + result = found.include?(data.relative_path) + found << data.relative_path unless found.include?(data.relative_path) + result end end - # The purge check needs to happen after all of the other recursion. + total.each do |meta| + if meta.relative_path == "." + parameter(:source).metadata = meta + next + end + children[meta.relative_path] ||= newchild(meta.relative_path) + children[meta.relative_path][:source] = meta.source + children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file" + + children[meta.relative_path].parameter(:source).metadata = meta + end + + # If we're purging resources, then delete any resource that isn't on the + # remote system. if self.purge? - children.each do |child| - if (sourced and ! sourced.include?(child[:path])) or ! child.managed? + # Make a hash of all of the resources we found remotely -- all we need is the + # fast lookup, the values don't matter. + remotes = total.inject({}) { |hash, meta| hash[meta.relative_path] = true; hash } + + children.each do |name, child| + unless remotes.include?(name) child[:ensure] = :absent end end end - + children end - # A simple method for determining whether we should be recursing. - def recurse? - return false unless @parameters.include?(:recurse) - - val = @parameters[:recurse].value - - if val and (val == true or val > 0) - return true - else - return false - end + def perform_recursion(path) + Puppet::FileServing::Metadata.search(path, :links => self[:links], :recurse => self[:recurse], :ignore => self[:ignore]) end # Remove the old backup. @@ -777,7 +656,8 @@ module Puppet # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) - return unless s = stat(true) + expire() + return unless s = stat self.fail "Could not back up; will not replace" unless handlebackup @@ -800,114 +680,15 @@ module Puppet else self.fail "Could not back up files of type %s" % s.ftype end + expire end # a wrapper method to make sure the file exists before doing anything def retrieve - unless stat = self.stat(true) - self.debug "File does not exist" - # If the file doesn't exist but we have a source, then call - # retrieve on that property - - propertyvalues = properties().inject({}) { |hash, property| - hash[property] = :absent - hash - } - - if @parameters.include?(:source) - propertyvalues[:source] = @parameters[:source].retrieve - end - return propertyvalues - end - - return currentpropvalues() - end - - # This recurses against the remote source and makes sure the local - # and remote structures match. It's run after 'localrecurse'. This - # method only does anything when its corresponding remote entry is - # a directory; in that case, this method creates file objects that - # correspond to any contained remote files. - def sourcerecurse(recurse) - # we'll set this manually as necessary - if @arghash.include?(:ensure) - @arghash.delete(:ensure) - end - - r = false - if recurse - unless recurse == 0 - r = 1 - end - end - - ignore = self[:ignore] - - result = [] - found = [] - - # Keep track of all the files we found in the source, so we can purge - # appropriately. - sourced = [] - - success = false - - @parameters[:source].should.each do |source| - sourceobj, path = uri2obj(source) - - # okay, we've got our source object; now we need to - # build up a local file structure to match the remote - # one - - server = sourceobj.server - - desc = server.list(path, self[:links], r, ignore) - if desc == "" - next - end - - success = true - - # Now create a new child for every file returned in the list. - result += desc.split("\n").collect { |line| - file, type = line.split("\t") - next if file == "/" # skip the listing object - name = file.sub(/^\//, '') - - # This makes sure that the first source *always* wins - # for conflicting files. - next if found.include?(name) - - # For directories, keep all of the sources, so that - # sourceselect still works as planned. - if type == "directory" - newsource = @parameters[:source].should.collect do |tmpsource| - tmpsource + file - end - else - newsource = source + file - end - args = {:source => newsource} - if type == file - args[:recurse] = nil - end - - found << name - sourced << File.join(self[:path], name) - - self.newchild(name, false, args) - }.reject {|c| c.nil? } - - if self[:sourceselect] == :first - return [result, sourced] - end + if source = parameter(:source) + source.copy_source_values end - - unless success - raise Puppet::Error, "None of the provided sources exist" - end - - return [result, sourced] + super end # Set the checksum, from another property. There are multiple @@ -926,11 +707,33 @@ module Puppet end end + # Should this thing be a normal file? This is a relatively complex + # way of determining whether we're trying to create a normal file, + # and it's here so that the logic isn't visible in the content property. + def should_be_file? + return true if self[:ensure] == :file + + # I.e., it's set to something like "directory" + return false if e = self[:ensure] and e != :present + + # The user doesn't really care, apparently + if self[:ensure] == :present + return true unless s = stat + return true if s.ftype == "file" + return false + end + + # If we've gotten here, then :ensure isn't set + return true if self[:content] + return true if stat and stat.ftype == "file" + return false + end + # Stat our file. Depending on the value of the 'links' attribute, we # use either 'stat' or 'lstat', and we expect the properties to use the # resulting stat object accordingly (mostly by testing the 'ftype' # value). - def stat(refresh = false) + cached_attr(:stat) do method = :stat # Files are the only types that support links @@ -938,23 +741,15 @@ module Puppet method = :lstat end path = self[:path] - # Just skip them when they don't exist at all. - unless FileTest.exists?(path) or FileTest.symlink?(path) - @stat = nil - return @stat - end - if @stat.nil? or refresh == true - begin - @stat = File.send(method, self[:path]) - rescue Errno::ENOENT => error - @stat = nil - rescue Errno::EACCES => error - self.warning "Could not stat; permission denied" - @stat = nil - end - end - return @stat + begin + File.send(method, self[:path]) + rescue Errno::ENOENT => error + return nil + rescue Errno::EACCES => error + warning "Could not stat; permission denied" + return nil + end end # We have to hack this just a little bit, because otherwise we'll get @@ -968,75 +763,6 @@ module Puppet obj end - def localfileserver - unless defined? @@localfileserver - args = { - :Local => true, - :Mount => { "/" => "localhost" }, - :Config => false - } - @@localfileserver = Puppet::Network::Handler.handler(:fileserver).new(args) - end - @@localfileserver - end - - def uri2obj(source) - sourceobj = Puppet::Type::File::FileSource.new - path = nil - unless source - devfail "Got a nil source" - end - if source =~ /^\// - source = "file://localhost/%s" % URI.escape(source) - sourceobj.mount = "localhost" - sourceobj.local = true - end - begin - uri = URI.parse(URI.escape(source)) - rescue => detail - self.fail "Could not understand source %s: %s" % - [source, detail.to_s] - end - - case uri.scheme - when "file": - sourceobj.server = localfileserver - path = "/localhost" + uri.path - when "puppet": - # FIXME: We should cache clients by uri.host + uri.port - # not by the full source path - unless @clients.include?(source) - host = uri.host - host ||= Puppet[:server] unless Puppet[:name] == "puppet" - if host.nil? - server = localfileserver - else - args = { :Server => host } - if uri.port - args[:Port] = uri.port - end - server = Puppet::Network::Client.file.new(args) - end - @clients[source] = server - end - sourceobj.server = @clients[source] - - tmp = uri.path - if tmp =~ %r{^/(\w+)} - sourceobj.mount = $1 - path = tmp - #path = tmp.sub(%r{^/\w+},'') || "/" - else - self.fail "Invalid source path %s" % tmp - end - else - self.fail "Got other URL type '%s' from %s" % - [uri.scheme, source] - end - - return [sourceobj, path.sub(/\/\//, '/')] - end - # Write out the file. Requires the content to be written, # the property name for logging, and the checksum for validation. def write(content, property, checksum = nil) @@ -1137,13 +863,7 @@ module Puppet end end end - end # Puppet.type(:pfile) - - # the filesource class can't include the path, because the path - # changes for every file instance - class ::Puppet::Type::File::FileSource - attr_accessor :mount, :root, :server, :local - end + end # Puppet::Type.type(:pfile) # We put all of the properties in separate files, because there are so many # of them. The order these are loaded is important, because it determines diff --git a/lib/puppet/type/file/checksum.rb b/lib/puppet/type/file/checksum.rb index 785ed0fee..82fae748c 100755 --- a/lib/puppet/type/file/checksum.rb +++ b/lib/puppet/type/file/checksum.rb @@ -11,7 +11,11 @@ Puppet::Type.type(:file).newproperty(:checksum) do like Tripwire without managing the file contents in any way. You can specify that a file's checksum should be monitored and then subscribe to the file from another object and receive events to signify - checksum changes, for instance." + checksum changes, for instance. + + There are a number of checksum types available including MD5 hashing (and + an md5lite variation that only hashes the first 500 characters of the + file." @event = :file_changed diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index 1eb1423aa..6d6dad4f1 100755 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,6 +1,9 @@ +require 'puppet/util/checksums' + module Puppet Puppet::Type.type(:file).newproperty(:content) do include Puppet::Util::Diff + include Puppet::Util::Checksums desc "Specify the contents of a file as a string. Newlines, tabs, and spaces can be specified using the escaped syntax (e.g., \\n for a @@ -23,7 +26,11 @@ module Puppet `PuppetTemplating templating`:trac:." def change_to_s(currentvalue, newvalue) - newvalue = "{md5}" + Digest::MD5.hexdigest(newvalue) + if source = resource.parameter(:source) + newvalue = source.metadata.checksum + else + newvalue = "{md5}" + Digest::MD5.hexdigest(newvalue) + end if currentvalue == :absent return "created file with contents %s" % newvalue else @@ -31,18 +38,41 @@ module Puppet return "changed file contents from %s to %s" % [currentvalue, newvalue] end end + + def content + self.should || (s = resource.parameter(:source) and s.content) + end # Override this method to provide diffs if asked for. # Also, fix #872: when content is used, and replace is true, the file # should be insync when it exists def insync?(is) - if ! @resource.replace? and File.exists?(@resource[:path]) + if resource.should_be_file? + return false if is == :absent + else + return true + end + + return true if ! @resource.replace? + + if self.should + return super + elsif source = resource.parameter(:source) + fail "Got a remote source with no checksum" unless source.checksum + unless sum_method = sumtype(source.checksum) + fail "Could not extract checksum type from source checksum '%s'" % source.checksum + end + + newsum = "{%s}" % sum_method + send(sum_method, is) + result = (newsum == source.checksum) + else + # We've got no content specified, and no source from which to + # get content. return true end - result = super - if ! result and Puppet[:show_diff] and File.exists?(@resource[:path]) - string_file_diff(@resource[:path], self.should) + if ! result and Puppet[:show_diff] + string_file_diff(@resource[:path], content) end return result end @@ -50,31 +80,31 @@ module Puppet def retrieve return :absent unless stat = @resource.stat - return self.should if stat.ftype == "link" and @resource[:links] == :ignore - - # Don't even try to manage the content on directories + # Don't even try to manage the content on directories or links return nil if stat.ftype == "directory" begin - currentvalue = File.read(@resource[:path]) - return currentvalue + return File.read(@resource[:path]) rescue => detail - raise Puppet::Error, "Could not read %s: %s" % - [@resource.title, detail] + raise Puppet::Error, "Could not read %s: %s" % [@resource.title, detail] end end # Make sure we're also managing the checksum property. def should=(value) super - @resource.newattr(:checksum) unless @resource.property(:checksum) + @resource.newattr(:checksum) unless @resource.parameter(:checksum) end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created - @resource.write(self.should, :content) + # We're safe not testing for the 'source' if there's no 'should' + # because we wouldn't have gotten this far if there weren't at least + # one valid value somewhere. + content = self.should || resource.parameter(:source).content + @resource.write(content, :content) return return_event end diff --git a/lib/puppet/type/file/ensure.rb b/lib/puppet/type/file/ensure.rb index 175b821b3..7466c5e3a 100755 --- a/lib/puppet/type/file/ensure.rb +++ b/lib/puppet/type/file/ensure.rb @@ -1,5 +1,5 @@ module Puppet - Puppet.type(:file).ensurable do + Puppet::Type.type(:file).ensurable do require 'etc' desc "Whether to create files that don't currently exist. Possible values are *absent*, *present*, *file*, and *directory*. @@ -110,10 +110,8 @@ module Puppet end def change_to_s(currentvalue, newvalue) - if property = (@resource.property(:content) || @resource.property(:source)) and ! property.insync?(currentvalue) - currentvalue = property.retrieve - - return property.change_to_s(property.retrieve, property.should) + if property = @resource.property(:content) and content = property.retrieve and ! property.insync?(content) + return property.change_to_s(content, property.should) else super(currentvalue, newvalue) end @@ -165,6 +163,7 @@ module Puppet end def sync + expire @resource.remove_existing(self.should) if self.should == :absent return :file_removed diff --git a/lib/puppet/type/file/group.rb b/lib/puppet/type/file/group.rb index 56883add6..3aeac21ff 100755 --- a/lib/puppet/type/file/group.rb +++ b/lib/puppet/type/file/group.rb @@ -2,7 +2,7 @@ require 'puppet/util/posix' # Manage file group ownership. module Puppet - Puppet.type(:file).newproperty(:group) do + Puppet::Type.type(:file).newproperty(:group) do include Puppet::Util::POSIX require 'etc' diff --git a/lib/puppet/type/file/mode.rb b/lib/puppet/type/file/mode.rb index ada1b5b47..fd9c27ae6 100755 --- a/lib/puppet/type/file/mode.rb +++ b/lib/puppet/type/file/mode.rb @@ -2,7 +2,7 @@ # for specification (e.g., u+rwx, or -0011), but for now only supports # specifying the full mode. module Puppet - Puppet.type(:file).newproperty(:mode) do + Puppet::Type.type(:file).newproperty(:mode) do require 'etc' desc "Mode the file should be. Currently relatively limited: you must specify the exact mode the file should be." diff --git a/lib/puppet/type/file/owner.rb b/lib/puppet/type/file/owner.rb index 6bc40ecbd..e4339f05b 100755 --- a/lib/puppet/type/file/owner.rb +++ b/lib/puppet/type/file/owner.rb @@ -1,5 +1,5 @@ module Puppet - Puppet.type(:file).newproperty(:owner) do + Puppet::Type.type(:file).newproperty(:owner) do include Puppet::Util::POSIX include Puppet::Util::Warnings diff --git a/lib/puppet/type/file/selcontext.rb b/lib/puppet/type/file/selcontext.rb index 990035005..717f58805 100644 --- a/lib/puppet/type/file/selcontext.rb +++ b/lib/puppet/type/file/selcontext.rb @@ -58,7 +58,7 @@ module Puppet end end - Puppet.type(:file).newproperty(:seluser, :parent => Puppet::SELFileContext) do + Puppet::Type.type(:file).newproperty(:seluser, :parent => Puppet::SELFileContext) do desc "What the SELinux user component of the context of the file should be. Any valid SELinux user component is accepted. For example ``user_u``. If not specified it defaults to the value returned by matchpathcon for @@ -69,7 +69,7 @@ module Puppet defaultto { self.retrieve_default_context(:seluser) } end - Puppet.type(:file).newproperty(:selrole, :parent => Puppet::SELFileContext) do + Puppet::Type.type(:file).newproperty(:selrole, :parent => Puppet::SELFileContext) do desc "What the SELinux role component of the context of the file should be. Any valid SELinux role component is accepted. For example ``role_r``. If not specified it defaults to the value returned by matchpathcon for @@ -80,7 +80,7 @@ module Puppet defaultto { self.retrieve_default_context(:selrole) } end - Puppet.type(:file).newproperty(:seltype, :parent => Puppet::SELFileContext) do + Puppet::Type.type(:file).newproperty(:seltype, :parent => Puppet::SELFileContext) do desc "What the SELinux type component of the context of the file should be. Any valid SELinux type component is accepted. For example ``tmp_t``. If not specified it defaults to the value returned by matchpathcon for @@ -91,7 +91,7 @@ module Puppet defaultto { self.retrieve_default_context(:seltype) } end - Puppet.type(:file).newproperty(:selrange, :parent => Puppet::SELFileContext) do + Puppet::Type.type(:file).newproperty(:selrange, :parent => Puppet::SELFileContext) do desc "What the SELinux range component of the context of the file should be. Any valid SELinux range component is accepted. For example ``s0`` or ``SystemHigh``. If not specified it defaults to the value returned by diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb index 2514d3d1e..f7c0fa4fc 100755 --- a/lib/puppet/type/file/source.rb +++ b/lib/puppet/type/file/source.rb @@ -1,10 +1,14 @@ + +require 'puppet/file_serving/content' +require 'puppet/file_serving/metadata' + module Puppet # Copy files from a local or remote source. This state *only* does any work # when the remote file is an actual file; in that case, this state copies # the file down. If the remote file is a dir or a link or whatever, then # this state, during retrieval, modifies the appropriate other states # so that things get taken care of appropriately. - Puppet.type(:file).newproperty(:source) do + Puppet::Type.type(:file).newparam(:source) do include Puppet::Util::Diff attr_accessor :source, :local @@ -59,218 +63,113 @@ module Puppet to ``follow`` if any remote sources are links. " - uncheckable - - validate do |source| - unless @resource.uri2obj(source) - raise Puppet::Error, "Invalid source %s" % source + validate do |sources| + sources = [sources] unless sources.is_a?(Array) + sources.each do |source| + begin + uri = URI.parse(URI.escape(source)) + rescue => detail + self.fail "Could not understand source %s: %s" % [source, detail.to_s] + end + + unless uri.scheme.nil? or %w{file puppet}.include?(uri.scheme) + self.fail "Cannot use URLs of type '%s' as source for fileserving" % [uri.scheme] + end end end - munge do |source| - # if source.is_a? Symbol - # return source - # end - - # Remove any trailing slashes - source.sub(/\/$/, '') + munge do |sources| + sources = [sources] unless sources.is_a?(Array) + sources.collect { |source| source.sub(/\/$/, '') } end def change_to_s(currentvalue, newvalue) - # newvalue = "{md5}" + @stats[:checksum] + # newvalue = "{md5}" + @metadata.checksum if @resource.property(:ensure).retrieve == :absent - return "creating from source %s with contents %s" % [@source, @stats[:checksum]] + return "creating from source %s with contents %s" % [metadata.source, metadata.checksum] else - return "replacing from source %s with contents %s" % [@source, @stats[:checksum]] + return "replacing from source %s with contents %s" % [metadata.source, metadata.checksum] end end def checksum - if defined?(@stats) - @stats[:checksum] + if metadata + metadata.checksum else nil end end - # Ask the file server to describe our file. - def describe(source) - sourceobj, path = @resource.uri2obj(source) - server = sourceobj.server + # Look up (if necessary) and return remote content. + cached_attr(:content) do + raise Puppet::DevError, "No source for content was stored with the metadata" unless metadata.source - begin - desc = server.describe(path, @resource[:links]) - rescue Puppet::Network::XMLRPCClientError => detail - fail detail, "Could not describe %s: %s" % [path, detail] + unless tmp = Puppet::FileServing::Content.find(metadata.source) + fail "Could not find any content at %s" % metadata.source end + tmp.content + end - return nil if desc == "" + # Copy the values from the source to the resource. Yay. + def copy_source_values + devfail "Somehow got asked to copy source values without any metadata" unless metadata - # Collect everything except the checksum - values = desc.split("\t") - other = values.pop - args = {} - pinparams.zip(values).each { |param, value| - if value =~ /^[0-9]+$/ - value = value.to_i - end - unless value.nil? - args[param] = value + # Take each of the stats and set them as states on the local file + # if a value has not already been provided. + [:owner, :mode, :group, :checksum].each do |param| + next if param == :owner and Puppet::Util::SUIDManager.uid != 0 + next if param == :checksum and metadata.ftype == "directory" + unless value = @resource[param] and value != :absent + @resource[param] = metadata.send(param) end - } - - # Now decide whether we're doing checksums or symlinks - if args[:type] == "link" - args[:target] = other - else - args[:checksum] = other end - # we can't manage ownership unless we're root, so don't even try - unless Puppet::Util::SUIDManager.uid == 0 - args.delete(:owner) - end - - return args - end - - # Use the info we get from describe() to check if we're in sync. - def insync?(currentvalue) - if currentvalue == :nocopy - return true - end - - # the only thing this actual state can do is copy files around. Therefore, - # only pay attention if the remote is a file. - unless @stats[:type] == "file" - return true - end - - #FIXARB: Inefficient? Needed to call retrieve on parent's ensure and checksum - parentensure = @resource.property(:ensure).retrieve - if parentensure != :absent and ! @resource.replace? - return true - end - # Now, we just check to see if the checksums are the same - parentchecksum = @resource.property(:checksum).retrieve - result = (!parentchecksum.nil? and (parentchecksum == @stats[:checksum])) + # Set the 'ensure' value, unless we're trying to delete the file. + @resource[:ensure] = metadata.ftype unless @resource[:ensure] == :absent - # Diff the contents if they ask it. This is quite annoying -- we need to do this in - # 'insync?' because they might be in noop mode, but we don't want to do the file - # retrieval twice, so we cache the value. - if ! result and Puppet[:show_diff] and File.exists?(@resource[:path]) and ! @stats[:_diffed] - @stats[:_remote_content] = get_remote_content - string_file_diff(@resource[:path], @stats[:_remote_content]) - @stats[:_diffed] = true + if metadata.ftype == "link" + @resource[:target] = metadata.destination end - return result end def pinparams - [:mode, :type, :owner, :group] + [:mode, :type, :owner, :group, :content] end def found? - ! (@stats.nil? or @stats[:type].nil?) + ! (metadata.nil? or metadata.ftype.nil?) end - # This basically calls describe() on our file, and then sets all - # of the local states appropriately. If the remote file is a normal - # file then we set it to copy; if it's a directory, then we just mark - # that the local directory should be created. - def retrieve(remote = true) - sum = nil - @source = nil - - # This is set to false by the File#retrieve function on the second - # retrieve, so that we do not do two describes. - if remote - # Find the first source that exists. @shouldorig contains - # the sources as specified by the user. - @should.each { |source| - if @stats = self.describe(source) - @source = source + # Provide, and retrieve if necessary, the metadata for this file. Fail + # if we can't find data about this host, and fail if there are any + # problems in our query. + cached_attr(:metadata) do + return nil unless value + result = nil + value.each do |source| + begin + if data = Puppet::FileServing::Metadata.find(source) + result = data + result.source = source break end - } - end - - if !found? - raise Puppet::Error, "No specified source was found from" + @should.inject("") { |s, source| s + " #{source},"}.gsub(/,$/,"") - end - - case @stats[:type] - when "directory", "file", "link": - @resource[:ensure] = @stats[:type] unless @resource.deleting? - else - self.info @stats.inspect - self.err "Cannot use files of type %s as sources" % @stats[:type] - return :nocopy + rescue => detail + fail detail, "Could not retrieve file metadata for %s: %s" % [source, detail] + end end - - # Take each of the stats and set them as states on the local file - # if a value has not already been provided. - @stats.each { |stat, value| - next if stat == :checksum - next if stat == :type - - # was the stat already specified, or should the value - # be inherited from the source? - @resource[stat] = value unless @resource.argument?(stat) - } - - return @stats[:checksum] + fail "Could not retrieve information from source(s) %s" % value.join(", ") unless result + result end - - def should - @should - end - + # Make sure we're also checking the checksum - def should=(value) + def value=(value) super checks = (pinparams + [:ensure]) checks.delete(:checksum) - @resource[:check] = checks - @resource[:checksum] = :md5 unless @resource.property(:checksum) - end - - def sync - contents = @stats[:_remote_content] || get_remote_content() - - exists = File.exists?(@resource[:path]) - - @resource.write(contents, :source, @stats[:checksum]) - - if exists - return :file_changed - else - return :file_created - end - end - - private - - def get_remote_content - raise Puppet::DevError, "Got told to copy non-file %s" % @resource[:path] unless @stats[:type] == "file" - - sourceobj, path = @resource.uri2obj(@source) - - begin - contents = sourceobj.server.retrieve(path, @resource[:links]) - rescue => detail - self.fail "Could not retrieve %s: %s" % [path, detail] - end - - contents = CGI.unescape(contents) unless sourceobj.server.local - - if contents == "" - self.notice "Could not retrieve contents for %s" % @source - end - - return contents + resource[:check] = checks + resource[:checksum] = :md5 unless resource.property(:checksum) end end end diff --git a/lib/puppet/type/file/target.rb b/lib/puppet/type/file/target.rb index 8949c2af6..1d85e05bc 100644 --- a/lib/puppet/type/file/target.rb +++ b/lib/puppet/type/file/target.rb @@ -1,5 +1,5 @@ module Puppet - Puppet.type(:file).newproperty(:target) do + Puppet::Type.type(:file).newproperty(:target) do desc "The target for creating a link. Currently, symlinks are the only type supported." diff --git a/lib/puppet/type/file/type.rb b/lib/puppet/type/file/type.rb index 65539795b..1835078a1 100755 --- a/lib/puppet/type/file/type.rb +++ b/lib/puppet/type/file/type.rb @@ -1,5 +1,5 @@ module Puppet - Puppet.type(:file).newproperty(:type) do + Puppet::Type.type(:file).newproperty(:type) do require 'etc' desc "A read-only state to check the file type." diff --git a/lib/puppet/type/filebucket.rb b/lib/puppet/type/filebucket.rb index b268610e9..03970aee8 100755 --- a/lib/puppet/type/filebucket.rb +++ b/lib/puppet/type/filebucket.rb @@ -54,21 +54,9 @@ module Puppet defaultto { Puppet[:clientbucketdir] } end - # get the actual filebucket object - def self.bucket(name) - if object = self[name] - return object.bucket - else - return nil - end - end - # Create a default filebucket. def self.mkdefaultbucket - unless default = self["puppet"] - return self.create(:name => "puppet", :path => Puppet[:clientbucketdir]) - end - return nil + new(:name => "puppet", :path => Puppet[:clientbucketdir]) end def self.instances diff --git a/lib/puppet/type/maillist.rb b/lib/puppet/type/maillist.rb index 7273488ee..088004a1c 100755 --- a/lib/puppet/type/maillist.rb +++ b/lib/puppet/type/maillist.rb @@ -46,7 +46,7 @@ module Puppet if atype[name] nil else - malias = Puppet::Type.type(:mailalias).create(:name => name, :recipient => recipient, :ensure => should) + malias = Puppet::Type.type(:mailalias).new(:name => name, :recipient => recipient, :ensure => should) end end.compact end diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb index 0cea39197..9ed1bf1c8 100644 --- a/lib/puppet/type/package.rb +++ b/lib/puppet/type/package.rb @@ -306,28 +306,6 @@ module Puppet @provider.get(:ensure) != :absent end - def initialize(params) - self.initvars - provider = nil - [:provider, "use"].each { |label| - if params.include?(label) - provider = params[label] - params.delete(label) - end - } - if provider - self[:provider] = provider - else - self.setdefaults(:provider) - end - - super(params) - - unless @parameters.include?(:provider) - raise Puppet::DevError, "No package provider set" - end - end - def retrieve @provider.properties.inject({}) do |props, ary| name, value = ary @@ -337,6 +315,6 @@ module Puppet props end end - end # Puppet.type(:package) + end # Puppet::Type.type(:package) end diff --git a/lib/puppet/type/schedule.rb b/lib/puppet/type/schedule.rb index b8479e01d..5c9cfd7f4 100755 --- a/lib/puppet/type/schedule.rb +++ b/lib/puppet/type/schedule.rb @@ -241,7 +241,9 @@ module Puppet :daily => :day, :monthly => :month, :weekly => proc do |prev, now| - prev.strftime("%U") != now.strftime("%U") + # 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 } @@ -314,24 +316,20 @@ module Puppet end def self.mkdefaultschedules - return [] if self["puppet"] - result = [] Puppet.debug "Creating default schedules" - result << self.create( + result << self.new( :name => "puppet", :period => :hourly, :repeat => "2" ) # And then one for every period - @parameters.find { |p| p.name == :period }.values.each { |value| - unless self[value.to_s] - result << self.create( - :name => value.to_s, - :period => value - ) - end + @parameters.find { |p| p.name == :period }.value_collection.values.each { |value| + result << self.new( + :name => value.to_s, + :period => value + ) } result diff --git a/lib/puppet/type/tidy.rb b/lib/puppet/type/tidy.rb index b4df4a9a2..98d69bc3a 100755 --- a/lib/puppet/type/tidy.rb +++ b/lib/puppet/type/tidy.rb @@ -1,332 +1,327 @@ -module Puppet - newtype(:tidy, :parent => Puppet.type(:file)) do - @doc = "Remove unwanted files based on specific criteria. Multiple - criteria are OR'd together, so a file that is too large but is not - old enough will still get tidied. - - You must specify either the size or age of the file (or both) for - files to be tidied." - - newparam(:path) do - desc "The path to the file or directory to manage. Must be fully - qualified." - isnamevar - end +Puppet::Type.newtype(:tidy) do + require 'puppet/file_serving/fileset' - newparam(:matches) do - desc "One or more file glob patterns, which restrict the list of - files to be tidied to those whose basenames match at least one - of the patterns specified. Multiple patterns can be specified - using an array." - end + @doc = "Remove unwanted files based on specific criteria. Multiple + criteria are OR'd together, so a file that is too large but is not + old enough will still get tidied. - copyparam(Puppet.type(:file), :backup) - - newproperty(:ensure) do - desc "An internal attribute used to determine which files should be removed." - - @nodoc = true - - TATTRS = [:age, :size] - - defaultto :anything # just so we always get this property - - def change_to_s(currentvalue, newvalue) - start = "Tidying" - if @out.include?(:age) - start += ", older than %s seconds" % @resource.should(:age) - end - if @out.include?(:size) - start += ", larger than %s bytes" % @resource.should(:size) - end - - start - end + If you don't specify either 'age' or 'size', then all files will + be removed. + + This resource type works by generating a file resource for every file + that should be deleted and then letting that resource perform the + actual deletion. + " - def insync?(is) - begin - stat = File.lstat(resource[:path]) - rescue Errno::ENOENT - info "Tidy target does not exist; ignoring" - return true - end - - if stat.ftype == "directory" and ! @resource[:rmdirs] - self.debug "Not tidying directories" - return true - end - - if is.is_a?(Symbol) - if [:absent, :notidy].include?(is) - return true - else - return false - end - else - @out = [] - if @resource[:matches] - basename = File.basename(@resource[:path]) - flags = File::FNM_DOTMATCH | File::FNM_PATHNAME - unless @resource[:matches].any? {|pattern| File.fnmatch(pattern, basename, flags) } - self.debug "No patterns specified match basename, skipping" - return true - end - end - TATTRS.each do |param| - if property = @resource.property(param) - self.debug "No is value for %s", [param] if is[property].nil? - unless property.insync?(is[property]) - @out << param - end - end - end + newparam(:path) do + desc "The path to the file or directory to manage. Must be fully + qualified." + isnamevar + end + + newparam(:matches) do + desc "One or more (shell type) file glob patterns, which restrict + the list of files to be tidied to those whose basenames match + at least one of the patterns specified. Multiple patterns can + be specified using an array. - if @out.length > 0 - return false - else - return true - end - end - end - - def retrieve - stat = nil - unless stat = @resource.stat - return { self => :absent} - end - - if stat.ftype == "directory" and ! @resource[:rmdirs] - return {self => :notidy} - end - - allprops = TATTRS.inject({}) { |prophash, param| - if property = @resource.property(param) - prophash[property] = property.assess(stat) - end - prophash - } - return { self => allprops } - end + tidy { \"/tmp\": + age => \"1w\", + recurse => false, + matches => [ \"[0-9]pub*.tmp\", \"*.temp\", \"tmpfile?\" ] + } + + The example above removes files from \/tmp if they are one week + old or older, are not in a subdirectory and match one of the shell + globs given. + + Note that the patterns are matched against the + basename of each file -- that is, your glob patterns should not + have any '/' characters in them, since you are only specifying + against the last bit of the file." + + # Make sure we convert to an array. + munge do |value| + value = [value] unless value.is_a?(Array) + value + end - def sync - file = @resource[:path] - case File.lstat(file).ftype - when "directory": - # If 'rmdirs' is disabled, then we would have never - # gotten to this method. - subs = Dir.entries(@resource[:path]).reject { |d| - d == "." or d == ".." - }.length - if subs > 0 - self.info "%s has %s children; not tidying" % - [@resource[:path], subs] - self.info Dir.entries(@resource[:path]).inspect - else - Dir.rmdir(@resource[:path]) - end - when "file": - @resource.handlebackup(file) - File.unlink(file) - when "link": - File.unlink(file) - else - self.fail "Cannot tidy files of type %s" % - File.lstat(file).ftype - end - - return :file_tidied - end + # Does a given path match our glob patterns, if any? Return true + # if no patterns have been provided. + def tidy?(path, stat) + basename = File.basename(path) + flags = File::FNM_DOTMATCH | File::FNM_PATHNAME + return true if value.find {|pattern| File.fnmatch(pattern, basename, flags) } + return false end + end - newproperty(:age) do - desc "Tidy files whose age is equal to or greater than - the specified time. You can choose seconds, minutes, - hours, days, or weeks by specifying the first letter of any - of those words (e.g., '1w')." - - @@ageconvertors = { - :s => 1, - :m => 60 - } - - @@ageconvertors[:h] = @@ageconvertors[:m] * 60 - @@ageconvertors[:d] = @@ageconvertors[:h] * 24 - @@ageconvertors[:w] = @@ageconvertors[:d] * 7 - - def assess(stat) - type = nil - if stat.ftype == "directory" - type = :mtime - else - type = @resource[:type] || :atime - end - - return stat.send(type).to_i - end + newparam(:backup) do + desc "Whether tidied files should be backed up. Any values are passed + directly to the file resources used for actual file deletion, so use + its backup documentation to determine valid values." + end - def convert(unit, multi) - if num = @@ageconvertors[unit] - return num * multi - else - self.fail "Invalid age unit '%s'" % unit - end - end + newparam(:age) do + desc "Tidy files whose age is equal to or greater than + the specified time. You can choose seconds, minutes, + hours, days, or weeks by specifying the first letter of any + of those words (e.g., '1w'). + + Specifying 0 will remove all files." - def insync?(is) - if (Time.now.to_i - is) > self.should - return false - end + @@ageconvertors = { + :s => 1, + :m => 60 + } - true - end + @@ageconvertors[:h] = @@ageconvertors[:m] * 60 + @@ageconvertors[:d] = @@ageconvertors[:h] * 24 + @@ageconvertors[:w] = @@ageconvertors[:d] * 7 - munge do |age| - unit = multi = nil - case age - when /^([0-9]+)(\w)\w*$/: - multi = Integer($1) - unit = $2.downcase.intern - when /^([0-9]+)$/: - multi = Integer($1) - unit = :d - else - self.fail "Invalid tidy age %s" % age - end - - convert(unit, multi) + def convert(unit, multi) + if num = @@ageconvertors[unit] + return num * multi + else + self.fail "Invalid age unit '%s'" % unit end end - newproperty(:size) do - desc "Tidy files whose size is equal to or greater than - the specified size. Unqualified values are in kilobytes, but - *b*, *k*, and *m* can be appended to specify *bytes*, *kilobytes*, - and *megabytes*, respectively. Only the first character is - significant, so the full word can also be used." - - @@sizeconvertors = { - :b => 0, - :k => 1, - :m => 2, - :g => 3 - } - - # Retrieve the size from a File::Stat object - def assess(stat) - return stat.size + def tidy?(path, stat) + # If the file's older than we allow, we should get rid of it. + if (Time.now.to_i - stat.send(resource[:type]).to_i) > value + return true + else + return false end + end - def convert(unit, multi) - if num = @@sizeconvertors[unit] - result = multi - num.times do result *= 1024 end - return result - else - self.fail "Invalid size unit '%s'" % unit - end + munge do |age| + unit = multi = nil + case age + when /^([0-9]+)(\w)\w*$/: + multi = Integer($1) + unit = $2.downcase.intern + when /^([0-9]+)$/: + multi = Integer($1) + unit = :d + else + self.fail "Invalid tidy age %s" % age end - - def insync?(is) - if is > self.should - return false - end - true - end - - munge do |size| - case size - when /^([0-9]+)(\w)\w*$/: - multi = Integer($1) - unit = $2.downcase.intern - when /^([0-9]+)$/: - multi = Integer($1) - unit = :k - else - self.fail "Invalid tidy size %s" % age - end - - convert(unit, multi) - end + convert(unit, multi) end + end - newparam(:type) do - desc "Set the mechanism for determining age." - - newvalues(:atime, :mtime, :ctime) - - defaultto :atime + newparam(:size) do + desc "Tidy files whose size is equal to or greater than + the specified size. Unqualified values are in kilobytes, but + *b*, *k*, and *m* can be appended to specify *bytes*, *kilobytes*, + and *megabytes*, respectively. Only the first character is + significant, so the full word can also be used." + + @@sizeconvertors = { + :b => 0, + :k => 1, + :m => 2, + :g => 3 + } + + def convert(unit, multi) + if num = @@sizeconvertors[unit] + result = multi + num.times do result *= 1024 end + return result + else + self.fail "Invalid size unit '%s'" % unit + end end - - newparam(:recurse) do - desc "If target is a directory, recursively descend - into the directory looking for files to tidy." - - newvalues(:true, :false, :inf, /^[0-9]+$/) - - # Replace the validation so that we allow numbers in - # addition to string representations of them. - validate { |arg| } - munge do |value| - newval = super(value) - case newval - when :true, :inf: true - when :false: false - when Integer, Fixnum, Bignum: value - when /^\d+$/: Integer(value) - else - raise ArgumentError, "Invalid recurse value %s" % value.inspect - end + + def tidy?(path, stat) + if stat.size > value + return true + else + return false end end + + munge do |size| + case size + when /^([0-9]+)(\w)\w*$/: + multi = Integer($1) + unit = $2.downcase.intern + when /^([0-9]+)$/: + multi = Integer($1) + unit = :k + else + self.fail "Invalid tidy size %s" % age + end - newparam(:rmdirs) do - desc "Tidy directories in addition to files; that is, remove - directories whose age is older than the specified criteria. - This will only remove empty directories, so all contained - files must also be tidied before a directory gets removed." + convert(unit, multi) end + end + + newparam(:type) do + desc "Set the mechanism for determining age." - # Erase PFile's validate method - validate do + newvalues(:atime, :mtime, :ctime) + + defaultto :atime + end + + newparam(:recurse) do + desc "If target is a directory, recursively descend + into the directory looking for files to tidy." + + newvalues(:true, :false, :inf, /^[0-9]+$/) + + # Replace the validation so that we allow numbers in + # addition to string representations of them. + validate { |arg| } + munge do |value| + newval = super(value) + case newval + when :true, :inf: true + when :false: false + when Integer, Fixnum, Bignum: value + when /^\d+$/: Integer(value) + else + raise ArgumentError, "Invalid recurse value %s" % value.inspect + end end + end + + newparam(:rmdirs, :boolean => true) do + desc "Tidy directories in addition to files; that is, remove + directories whose age is older than the specified criteria. + This will only remove empty directories, so all contained + files must also be tidied before a directory gets removed." + + newvalues :true, :false + end + + # Erase PFile's validate method + validate do + end + + def self.instances + [] + end + + @depthfirst = true - def self.instances - [] + def initialize(hash) + super + + # only allow backing up into filebuckets + unless self[:backup].is_a? Puppet::Network::Client.dipper + self[:backup] = false end + end + + # Make a file resource to remove a given file. + def mkfile(path) + # Force deletion, so directories actually get deleted. + Puppet::Type.type(:file).new :path => path, :backup => self[:backup], :ensure => :absent, :force => true + end - @depthfirst = true + def retrieve + # Our ensure property knows how to retrieve everything for us. + if obj = @parameters[:ensure] + return obj.retrieve + else + return {} + end + end + + # Hack things a bit so we only ever check the ensure property. + def properties + [] + end - def initialize(hash) - super + def eval_generate + [] + end - unless @parameters.include?(:age) or - @parameters.include?(:size) - unless FileTest.directory?(self[:path]) - # don't do size comparisons for directories - self.fail "Tidy must specify size, age, or both" - end - end + def generate + return [] unless stat(self[:path]) - # only allow backing up into filebuckets - unless self[:backup].is_a? Puppet::Network::Client.dipper - self[:backup] = false + if self[:recurse] + files = Puppet::FileServing::Fileset.new(self[:path], :recurse => self[:recurse]).files.collect do |f| + f == "." ? self[:path] : File.join(self[:path], f) end + else + files = [self[:path]] end - - def retrieve - # Our ensure property knows how to retrieve everything for us. - if obj = @parameters[:ensure] - return obj.retrieve + result = files.find_all { |path| tidy?(path) }.collect { |path| mkfile(path) }.each { |file| notice "Tidying %s" % file.ref }.sort { |a,b| b[:path] <=> a[:path] } + + # No need to worry about relationships if we don't have rmdirs; there won't be + # any directories. + return result unless rmdirs? + + # Now make sure that all directories require the files they contain, if all are available, + # so that a directory is emptied before we try to remove it. + files_by_name = result.inject({}) { |hash, file| hash[file[:path]] = file; hash } + + files_by_name.keys.sort { |a,b| b <=> b }.each do |path| + dir = File.dirname(path) + next unless resource = files_by_name[dir] + if resource[:require] + resource[:require] << Puppet::Resource::Reference.new(:file, path) else - return {} + resource[:require] = [Puppet::Resource::Reference.new(:file, path)] end end - - # Hack things a bit so we only ever check the ensure property. - def properties - [] + + return result + end + + # Does a given path match our glob patterns, if any? Return true + # if no patterns have been provided. + def matches?(path) + return true unless self[:matches] + + basename = File.basename(path) + flags = File::FNM_DOTMATCH | File::FNM_PATHNAME + if self[:matches].find {|pattern| File.fnmatch(pattern, basename, flags) } + return true + else + debug "No specified patterns match %s, not tidying" % path + return false end end -end + # Should we remove the specified file? + def tidy?(path) + return false unless stat = self.stat(path) + + return false if stat.ftype == "directory" and ! rmdirs? + + # The 'matches' parameter isn't OR'ed with the other tests -- + # it's just used to reduce the list of files we can match. + return false if param = parameter(:matches) and ! param.tidy?(path, stat) + + tested = false + [:age, :size].each do |name| + next unless param = parameter(name) + tested = true + return true if param.tidy?(path, stat) + end + + # If they don't specify either, then the file should always be removed. + return true unless tested + return false + end + + def stat(path) + begin + File.lstat(path) + rescue Errno::ENOENT => error + info "File does not exist" + return nil + rescue Errno::EACCES => error + warning "Could not stat; permission denied" + return nil + end + end +end diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index 6b6ff82ab..8efc2ef1c 100755 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -229,11 +229,8 @@ module Puppet groups.each { |group| case group when Integer: - if obj = Puppet.type(:group).find { |gobj| - gobj.should(:gid) == group - } - autos << obj - + if resource = catalog.resources.find { |r| r.is_a?(Puppet::Type.type(:group)) and r.should(:gid) == group } + autos << resource end else autos << group diff --git a/lib/puppet/type/yumrepo.rb b/lib/puppet/type/yumrepo.rb index 164deaffc..4f7b26b8a 100644 --- a/lib/puppet/type/yumrepo.rb +++ b/lib/puppet/type/yumrepo.rb @@ -66,8 +66,9 @@ module Puppet class << self attr_accessor :filetype # The writer is only used for testing, there should be no need - # to change yumconf in any other context + # to change yumconf or inifile in any other context attr_accessor :yumconf + attr_writer :inifile end self.filetype = Puppet::Util::FileType.filetype(:flat) @@ -183,11 +184,11 @@ module Puppet end end + # This is only used during testing. def self.clear @inifile = nil @yumconf = "/etc/yum.conf" @defaultrepodir = nil - super end # Return the Puppet::Util::IniConfig::Section for this yumrepo resource diff --git a/lib/puppet/type/zone.rb b/lib/puppet/type/zone.rb index 8c4261241..6e5d784b3 100644 --- a/lib/puppet/type/zone.rb +++ b/lib/puppet/type/zone.rb @@ -77,6 +77,7 @@ Puppet::Type.newtype(:zone) do used to stop zones." @states = {} + @parametervalues = [] def self.alias_state(values) @state_aliases ||= {} diff --git a/lib/puppet/util/cacher.rb b/lib/puppet/util/cacher.rb new file mode 100644 index 000000000..a9fb890c6 --- /dev/null +++ b/lib/puppet/util/cacher.rb @@ -0,0 +1,104 @@ +module Puppet::Util::Cacher + module Expirer + attr_reader :timestamp + + # Cause all cached values to be considered expired. + def expire + @timestamp = Time.now + end + + # Is the provided timestamp earlier than our expiration timestamp? + # If it is, then the associated value is expired. + def expired?(ts) + return false unless timestamp + + return timestamp > ts + end + end + + extend Expirer + + # Our module has been extended in a class; we can only add the Instance methods, + # which become *class* methods in the class. + def self.extended(other) + class << other + extend ClassMethods + include InstanceMethods + end + end + + # Our module has been included in a class, which means the class gets the class methods + # and all of its instances get the instance methods. + def self.included(other) + other.extend(ClassMethods) + other.send(:include, InstanceMethods) + end + + # Methods that can get added to a class. + module ClassMethods + private + + # Provide a means of defining an attribute whose value will be cached. + # Must provide a block capable of defining the value if it's flushed.. + def cached_attr(name, &block) + init_method = "init_" + name.to_s + define_method(init_method, &block) + + define_method(name) do + cached_value(name) + end + + define_method(name.to_s + "=") do |value| + # Make sure the cache timestamp is set + cache_timestamp + value_cache[name] = value + end + end + end + + # Methods that get added to instances. + module InstanceMethods + def expire + # Only expire if we have an expirer. This is + # mostly so that we can comfortably handle cases + # like Puppet::Type instances, which use their + # catalog as their expirer, and they often don't + # have a catalog. + if e = expirer + e.expire + end + end + + def expirer + Puppet::Util::Cacher + end + + private + + def cache_timestamp + unless defined?(@cache_timestamp) + @cache_timestamp = Time.now + end + @cache_timestamp + end + + def cached_value(name) + # Allow a nil expirer, in which case we regenerate the value every time. + if expirer.nil? or expirer.expired?(cache_timestamp) + value_cache.clear + @cache_timestamp = Time.now + end + unless value_cache.include?(name) + value_cache[name] = send("init_%s" % name) + end + value_cache[name] + end + + def value_cache + unless defined?(@value_cache) and @value_cache + @value_cache = {} + end + @value_cache + end + end +end diff --git a/lib/puppet/util/checksums.rb b/lib/puppet/util/checksums.rb index 15d2eadd1..4b51789f6 100644 --- a/lib/puppet/util/checksums.rb +++ b/lib/puppet/util/checksums.rb @@ -1,6 +1,15 @@ # A stand-alone module for calculating checksums # in a generic way. module Puppet::Util::Checksums + # Strip the checksum type from an existing checksum + def sumtype(checksum) + if checksum =~ /^\{(\w+)\}/ + return $1 + else + return nil + end + end + # Calculate a checksum using Digest::MD5. def md5(content) require 'digest/md5' diff --git a/lib/puppet/util/filetype.rb b/lib/puppet/util/filetype.rb index 40c028cc2..34feeab90 100755 --- a/lib/puppet/util/filetype.rb +++ b/lib/puppet/util/filetype.rb @@ -74,8 +74,10 @@ class Puppet::Util::FileType # Pick or create a filebucket to use. def bucket - filebucket = Puppet::Type.type(:filebucket) - (filebucket["puppet"] || filebucket.mkdefaultbucket).bucket + unless defined?(@bucket) + @bucket = Puppet::Type.type(:filebucket).mkdefaultbucket.bucket + end + @bucket end def initialize(path) diff --git a/lib/puppet/util/graph.rb b/lib/puppet/util/graph.rb index d1ef36f8e..fc05cafd9 100644 --- a/lib/puppet/util/graph.rb +++ b/lib/puppet/util/graph.rb @@ -2,7 +2,7 @@ # Copyright (c) 2006. All rights reserved. require 'puppet' -require 'puppet/pgraph' +require 'puppet/simple_graph' # A module that handles the small amount of graph stuff in Puppet. module Puppet::Util::Graph @@ -12,7 +12,7 @@ module Puppet::Util::Graph def to_graph(graph = nil, &block) # Allow our calling function to send in a graph, so that we # can call this recursively with one graph. - graph ||= Puppet::PGraph.new + graph ||= Puppet::SimpleGraph.new self.each do |child| unless block_given? and ! yield(child) diff --git a/lib/puppet/util/log.rb b/lib/puppet/util/log.rb index b57faad42..a74432021 100644 --- a/lib/puppet/util/log.rb +++ b/lib/puppet/util/log.rb @@ -104,6 +104,13 @@ class Puppet::Util::Log end end + def self.close_all + # And close all logs except the console. + destinations.each do |dest| + close(dest) + end + end + # Flush any log destinations that support such operations. def Log.flush @destinations.each { |type, dest| diff --git a/lib/puppet/util/package.rb b/lib/puppet/util/package.rb index 00e04f64a..613aa6b1e 100644 --- a/lib/puppet/util/package.rb +++ b/lib/puppet/util/package.rb @@ -28,4 +28,6 @@ module Puppet::Util::Package end return version_a <=> version_b; end + + module_function :versioncmp end diff --git a/lib/puppet/util/posix.rb b/lib/puppet/util/posix.rb index b969a041c..3f6c1f6e3 100755 --- a/lib/puppet/util/posix.rb +++ b/lib/puppet/util/posix.rb @@ -58,45 +58,6 @@ module Puppet::Util::POSIX return nil end - # Look in memory for an already-managed type and use its info if available. - # Currently unused. - def get_provider_value(type, field, id) - unless typeklass = Puppet::Type.type(type) - raise ArgumentError, "Invalid type %s" % type - end - - id = id.to_s - - chkfield = idfield(type) - obj = typeklass.find { |obj| - if id =~ /^\d+$/ - obj.should(chkfield).to_s == id || - obj.provider.send(chkfield) == id - else - obj[:name] == id - end - } - - return nil unless obj - - if obj.provider - begin - val = obj.provider.send(field) - if val == :absent - return nil - else - return val - end - rescue => detail - if Puppet[:trace] - puts detail.backtrace - Puppet.err detail - return nil - end - end - end - end - # Determine what the field name is for users and groups. def idfield(space) case Puppet::Util.symbolize(space) diff --git a/lib/puppet/util/rdoc.rb b/lib/puppet/util/rdoc.rb index b33e67c71..a18fa1b96 100644 --- a/lib/puppet/util/rdoc.rb +++ b/lib/puppet/util/rdoc.rb @@ -22,6 +22,7 @@ module Puppet::Util::RDoc # specify our own format & where to output options = [ "--fmt", "puppet", "--quiet", + "--force-update", "--op", outputdir ] options += files diff --git a/lib/puppet/util/resource_template.rb b/lib/puppet/util/resource_template.rb index 53066a192..4e333571f 100644 --- a/lib/puppet/util/resource_template.rb +++ b/lib/puppet/util/resource_template.rb @@ -18,7 +18,7 @@ require 'erb' # def generate # template = Puppet::Util::ResourceTemplate.new("/path/to/template", self) # -# return Puppet::Type.type(:file).create :path => "/my/file", +# return Puppet::Type.type(:file).new :path => "/my/file", # :content => template.evaluate # end # diff --git a/lib/puppet/util/settings.rb b/lib/puppet/util/settings.rb index ac25e0815..0af842c8d 100644 --- a/lib/puppet/util/settings.rb +++ b/lib/puppet/util/settings.rb @@ -3,11 +3,11 @@ require 'sync' require 'puppet/transportable' require 'getoptlong' +require 'puppet/external/event-loop' # The class for handling configuration files. class Puppet::Util::Settings include Enumerable - include Puppet::Util attr_accessor :file attr_reader :timer @@ -19,7 +19,7 @@ class Puppet::Util::Settings # Set a config value. This doesn't set the defaults, it sets the value itself. def []=(param, value) - param = symbolize(param) + param = param.to_sym unless element = @config[param] raise ArgumentError, "Attempt to assign a value to unknown configuration parameter %s" % param.inspect @@ -37,6 +37,8 @@ class Puppet::Util::Settings @sync.synchronize do # yay, thread-safe @values[:memory][param] = value @cache.clear + + clearused end return value @@ -45,9 +47,6 @@ class Puppet::Util::Settings # Generate the list of valid arguments, in a format that GetoptLong can # understand, and add them to the passed option list. def addargs(options) - # Hackish, but acceptable. Copy the current ARGV for restarting. - Puppet.args = ARGV.dup - # Add all of the config parameters as valid options. self.each { |name, element| element.getopt_args.each { |args| options << args } @@ -56,24 +55,9 @@ class Puppet::Util::Settings return options end - def apply - trans = self.to_transportable - begin - config = trans.to_catalog - config.store_state = false - config.apply - config.clear - rescue => detail - if Puppet[:trace] - puts detail.backtrace - end - Puppet.err "Could not configure myself: %s" % detail - end - end - # Is our parameter a boolean parameter? def boolean?(param) - param = symbolize(param) + param = param.to_sym if @config.include?(param) and @config[param].kind_of? CBoolean return true else @@ -126,7 +110,7 @@ class Puppet::Util::Settings # Return a value's description. def description(name) - if obj = @config[symbolize(name)] + if obj = @config[name.to_sym] obj.desc else nil @@ -153,7 +137,7 @@ class Puppet::Util::Settings # Return an object by name. def element(param) - param = symbolize(param) + param = param.to_sym @config[param] end @@ -277,7 +261,7 @@ class Puppet::Util::Settings # Return a given object's file metadata. def metadata(param) - if obj = @config[symbolize(param)] and obj.is_a?(CFile) + if obj = @config[param.to_sym] and obj.is_a?(CFile) return [:owner, :group, :mode].inject({}) do |meta, p| if v = obj.send(p) meta[p] = v @@ -383,115 +367,30 @@ class Puppet::Util::Settings end end - private :unsafe_parse - - # Parse the configuration file. As of May 2007, this is a backward-compatibility method and - # will be deprecated soon. - def old_parse(file) - text = nil - - if file.is_a? Puppet::Util::LoadedFile - @file = file - else - @file = Puppet::Util::LoadedFile.new(file) - end - - # Don't create a timer for the old style parsing. - # settimer() - - begin - text = File.read(@file.file) - rescue Errno::ENOENT - raise Puppet::Error, "No such file %s" % file - rescue Errno::EACCES - raise Puppet::Error, "Permission denied to file %s" % file - end - - @sync.synchronize do - @values = Hash.new { |names, name| - names[name] = {} - } - end - - # Get rid of the values set by the file, keeping cli values. - self.clear(true) - - section = "puppet" - metas = %w{owner group mode} - values = Hash.new { |hash, key| hash[key] = {} } - @sync.synchronize do - text.split(/\n/).each { |line| - case line - when /^\[(\w+)\]$/: section = $1 # Section names - when /^\s*#/: next # Skip comments - when /^\s*$/: next # Skip blanks - when /^\s*(\w+)\s*=\s*(.+)$/: # settings - var = $1.intern - if var == :mode - value = $2 - else - value = munge_value($2) - end - - # Only warn if we don't know what this config var is. This - # prevents exceptions later on. - unless @config.include?(var) or metas.include?(var.to_s) - Puppet.warning "Discarded unknown configuration parameter %s" % var.inspect - next # Skip this line. - end - - # Mmm, "special" attributes - if metas.include?(var.to_s) - unless values.include?(section) - values[section] = {} - end - values[section][var.to_s] = value - - # If the parameter is valid, then set it. - if section == Puppet[:name] and @config.include?(var) - #@config[var].value = value - @values[:main][var] = value - end - next - end - - # Don't override set parameters, since the file is parsed - # after cli arguments are handled. - unless @config.include?(var) and @config[var].setbycli - Puppet.debug "%s: Setting %s to '%s'" % [section, var, value] - @values[:main][var] = value - end - @config[var].section = symbolize(section) - - metas.each { |meta| - if values[section][meta] - if @config[var].respond_to?(meta + "=") - @config[var].send(meta + "=", values[section][meta]) - end - end - } - else - raise Puppet::Error, "Could not match line %s" % line - end - } - end - end - - # Create a new config option. + # Create a new element. The value is passed in because it's used to determine + # what kind of element we're creating, but the value itself might be either + # a default or a value, so we can't actually assign it. def newelement(hash) klass = nil if hash[:section] - hash[:section] = symbolize(hash[:section]) - end - case hash[:default] - when true, false, "true", "false": - klass = CBoolean - when /^\$\w+\//, /^\//: - klass = CFile - when String, Integer, Float: # nothing - klass = CElement + hash[:section] = hash[:section].to_sym + end + if type = hash[:type] + unless klass = {:element => CElement, :file => CFile, :boolean => CBoolean}[type] + raise ArgumentError, "Invalid setting type '%s'" % type + end + hash.delete(:type) else - raise Puppet::Error, "Invalid value '%s' for %s" % [value.inspect, hash[:name]] + case hash[:default] + when true, false, "true", "false": + klass = CBoolean + when /^\$\w+\//, /^\//: + klass = CFile + when String, Integer, Float: # nothing + klass = CElement + else + raise Puppet::Error, "Invalid value '%s' for %s" % [value.inspect, hash[:name]] + end end hash[:settings] = self element = klass.new(hash) @@ -504,7 +403,7 @@ class Puppet::Util::Settings # Iterate across all of the objects in a given section. def persection(section) - section = symbolize(section) + section = section.to_sym self.each { |name, obj| if obj.section == section yield obj @@ -526,10 +425,9 @@ class Puppet::Util::Settings def reuse return unless defined? @used @sync.synchronize do # yay, thread-safe - @used.each do |section| - @used.delete(section) - self.use(section) - end + new = @used + @used = [] + self.use(*new) end end @@ -557,45 +455,10 @@ class Puppet::Util::Settings return sectionlist, sections end - # Convert a single section into transportable objects. - def section_to_transportable(section, done = nil) - done ||= Hash.new { |hash, key| hash[key] = {} } - objects = [] - persection(section) do |obj| - if @config[:mkusers] and value(:mkusers) - objects += add_user_resources(section, obj, done) - end - - value = obj.value - - # Only files are convertable to transportable resources. - next unless obj.respond_to? :to_transportable and transobjects = obj.to_transportable - - transobjects = [transobjects] unless transobjects.is_a? Array - transobjects.each do |trans| - # transportable could return nil - next unless trans - unless done[:file].include? trans.name - @created << trans.name - objects << trans - done[:file][trans.name] = trans - end - end - end - - bucket = Puppet::TransBucket.new - bucket.type = "Settings" - bucket.name = section - bucket.push(*objects) - bucket.keyword = "class" - - return bucket - end - # Set a bunch of defaults in a given section. The sections are actually pretty # pointless, but they help break things up a bit, anyway. def setdefaults(section, defs) - section = symbolize(section) + section = section.to_sym call = [] defs.each { |name, hash| if hash.is_a? Array @@ -606,7 +469,7 @@ class Puppet::Util::Settings hash = {} [:default, :desc].zip(tmp).each { |p,v| hash[p] = v } end - name = symbolize(name) + name = name.to_sym hash[:name] = name hash[:section] = section if @config.include?(name) @@ -631,22 +494,30 @@ class Puppet::Util::Settings end # Create a timer to check whether the file should be reparsed. - def settimer - if Puppet[:filetimeout] > 0 - @timer = Puppet.newtimer( - :interval => Puppet[:filetimeout], - :tolerance => 1, - :start? => true - ) do - self.reparse() - end - end + def set_filetimeout_timer + return unless timeout = self[:filetimeout] and timeout > 0 + EventLoop::Timer.new(:interval => timeout, :tolerance => 1, :start? => true) { self.reparse() } end - # Convert our list of objects into a component that can be applied. - def to_configuration - transport = self.to_transportable - return transport.to_catalog + # Convert the settings we manage into a catalog full of resources that model those settings. + # We currently have to go through Trans{Object,Bucket} instances, + # because this hasn't been ported yet. + def to_catalog(*sections) + sections = nil if sections.empty? + + catalog = Puppet::Resource::Catalog.new("Settings") + + @config.values.find_all { |value| value.is_a?(CFile) }.each do |file| + next unless (sections.nil? or sections.include?(file.section)) + next unless resource = file.to_resource + next if catalog.resource(resource.ref) + + catalog.add_resource(resource) + end + + add_user_resources(catalog, sections) + + catalog end # Convert our list of config elements into a configuration file. @@ -677,62 +548,29 @@ Generated on #{Time.now}. return str end - # Convert our configuration into a list of transportable objects. - def to_transportable(*sections) - done = Hash.new { |hash, key| - hash[key] = {} - } - - topbucket = Puppet::TransBucket.new - if defined? @file.file and @file.file - topbucket.name = @file.file - else - topbucket.name = "top" - end - topbucket.type = "Settings" - topbucket.top = true - - # Now iterate over each section - if sections.empty? - eachsection do |section| - sections << section - end - end - sections.each do |section| - obj = section_to_transportable(section, done) - topbucket.push obj - end - - topbucket - end - # Convert to a parseable manifest def to_manifest - transport = self.to_transportable - - manifest = transport.to_manifest + "\n" - eachsection { |section| - manifest += "include #{section}\n" - } - - return manifest + catalog = to_catalog + # The resource list is a list of references, not actual instances. + catalog.resources.collect do |ref| + catalog.resource(ref).to_manifest + end.join("\n\n") end # Create the necessary objects to use a section. This is idempotent; # you can 'use' a section as many times as you want. def use(*sections) + sections = sections.collect { |s| s.to_sym } @sync.synchronize do # yay, thread-safe - sections = sections.reject { |s| @used.include?(s.to_sym) } + sections = sections.reject { |s| @used.include?(s) } return if sections.empty? - bucket = to_transportable(*sections) - begin - catalog = bucket.to_catalog + catalog = to_catalog(*sections).to_ral rescue => detail puts detail.backtrace if Puppet[:trace] - Puppet.err "Could not create resources for managing Puppet's files and directories: %s" % detail + Puppet.err "Could not create resources for managing Puppet's files and directories in sections %s: %s" % [sections.inspect, detail] # We need some way to get rid of any resources created during the catalog creation # but not cleaned up. @@ -748,8 +586,6 @@ Generated on #{Time.now}. raise "Got %s failure(s) while initializing: %s" % [failures.length, failures.collect { |l| l.to_s }.join("; ")] end end - ensure - catalog.clear end sections.each { |s| @used << s } @@ -758,15 +594,15 @@ Generated on #{Time.now}. end def valid?(param) - param = symbolize(param) + param = param.to_sym @config.has_key?(param) end # Find the correct value using our search path. Optionally accept an environment # in which to search before the other configuration sections. def value(param, environment = nil) - param = symbolize(param) - environment = symbolize(environment) if environment + param = param.to_sym + environment = environment.to_sym if environment # Short circuit to nil for undefined parameters. return nil unless @config.include?(param) @@ -850,20 +686,26 @@ Generated on #{Time.now}. end sync.synchronize(Sync::EX) do - File.open(file, "r+", 0600) do |rf| + File.open(file, ::File::CREAT|::File::RDWR, 0600) do |rf| rf.lock_exclusive do if File.exist?(tmpfile) raise Puppet::Error, ".tmp file already exists for %s; Aborting locked write. Check the .tmp file and delete if appropriate" % [file] end - writesub(default, tmpfile, *args, &bloc) + # If there's a failure, remove our tmpfile + begin + writesub(default, tmpfile, *args, &bloc) + rescue + File.unlink(tmpfile) if FileTest.exist?(tmpfile) + raise + end begin File.rename(tmpfile, file) rescue => detail - Puppet.err "Could not rename %s to %s: %s" % - [file, tmpfile, detail] + Puppet.err "Could not rename %s to %s: %s" % [file, tmpfile, detail] + File.unlink(tmpfile) if FileTest.exist?(tmpfile) end end end @@ -886,45 +728,25 @@ Generated on #{Time.now}. end # Create the transportable objects for users and groups. - def add_user_resources(section, obj, done) - resources = [] - [:owner, :group].each do |attr| - type = nil - if attr == :owner - type = :user - else - type = attr - end - # If a user and/or group is set, then make sure we're - # managing that object - if obj.respond_to? attr and name = obj.send(attr) - # Skip root or wheel - next if %w{root wheel}.include?(name.to_s) - - # Skip owners and groups we've already done, but tag - # them with our section if necessary - if done[type].include?(name) - tags = done[type][name].tags - unless tags.include?(section) - done[type][name].tags = tags << section - end - else - newobj = Puppet::TransObject.new(name, type.to_s) - newobj.tags = ["puppet", "configuration", section] - newobj[:ensure] = :present - if type == :user - newobj[:comment] ||= "%s user" % name - end - # Set the group appropriately for the user - if type == :user - newobj[:gid] = Puppet[:group] - end - done[type][name] = newobj - resources << newobj + def add_user_resources(catalog, sections) + return unless Puppet.features.root? + return unless self[:mkusers] + + @config.each do |name, element| + next unless element.respond_to?(:owner) + next unless sections.nil? or sections.include?(element.section) + + if user = element.owner and user != "root" and catalog.resource(:user, user).nil? + resource = Puppet::Resource.new(:user, user, :ensure => :present) + if self[:group] + resource[:gid] = self[:group] end + catalog.add_resource resource + end + if group = element.group and ! %w{root wheel}.include?(group) and catalog.resource(:group, group).nil? + catalog.add_resource Puppet::Resource.new(:group, group, :ensure => :present) end end - resources end # Yield each search source in turn. @@ -991,7 +813,7 @@ Generated on #{Time.now}. # Create a timer so that this file will get checked automatically # and reparsed if necessary. - settimer() + set_filetimeout_timer() result = Hash.new { |names, name| names[name] = {} @@ -1096,8 +918,9 @@ Generated on #{Time.now}. # Create the new element. Pretty much just sets the name. def initialize(args = {}) - @settings = args.delete(:settings) - raise ArgumentError.new("You must refer to a settings object") if @settings.nil? or !@settings.is_a?(Puppet::Util::Settings) + unless @settings = args.delete(:settings) + raise ArgumentError.new("You must refer to a settings object") + end args.each do |param, value| method = param.to_s + "=" @@ -1177,6 +1000,11 @@ Generated on #{Time.now}. attr_writer :owner, :group attr_accessor :mode, :create + # Should we create files, rather than just directories? + def create_files? + create + end + def group if defined? @group return @settings.convert(@group) @@ -1222,58 +1050,35 @@ Generated on #{Time.now}. end end - # Convert the object to a TransObject instance. - def to_transportable - type = self.type - return nil unless type + # Turn our setting thing into a Puppet::Resource instance. + def to_resource + return nil unless type = self.type path = self.value return nil unless path.is_a?(String) - return nil if path =~ /^\/dev/ - return nil if Puppet::Type.type(:file)[path] # skip files that are in our global resource list. - objects = [] + # Make sure the paths are fully qualified. + path = File.join(Dir.getwd, path) unless path =~ /^\// - # Skip plain files that don't exist, since we won't be managing them anyway. - return nil unless self.name.to_s =~ /dir$/ or File.exist?(path) or self.create - obj = Puppet::TransObject.new(path, "file") + return nil unless type == :directory or create_files? or File.exist?(path) + return nil if path =~ /^\/dev/ - # Only create directories, or files that are specifically marked to - # create. - if type == :directory or self.create - obj[:ensure] = type - end - [:mode].each { |var| - if value = self.send(var) - # Don't bother converting the mode, since the file type - # can handle it any old way. - obj[var] = value - end - } + resource = Puppet::Resource.new(:file, path) + resource[:mode] = self.mode if self.mode - # Only chown or chgrp when root if Puppet.features.root? - [:group, :owner].each { |var| - if value = self.send(var) - obj[var] = value - end - } + resource[:owner] = self.owner if self.owner + resource[:group] = self.group if self.group end - # And set the loglevel to debug for everything - obj[:loglevel] = "debug" - - # We're not actually modifying any files here, and if we allow a - # filebucket to get used here we get into an infinite recursion - # trying to set the filebucket up. - obj[:backup] = false + resource[:ensure] = type + resource[:loglevel] = :debug + resource[:backup] = false - if self.section - obj.tags += ["puppet", "configuration", self.section, self.name] - end - objects << obj - objects + resource.tag(self.section, self.name, "settings") + + resource end # Make sure any provided variables look up to something. |