diff --git a/bin/puppetrun b/bin/puppetrun index cc78f81a8..5f99d6325 100755 --- a/bin/puppetrun +++ b/bin/puppetrun @@ -1,404 +1,404 @@ #!/usr/bin/env ruby # # = Synopsis # # Trigger a puppetd run on a set of hosts. # # = Usage # # puppetrun [-a|--all] [-c|--class ] [-d|--debug] [-f|--foreground] # [-h|--help] [--host ] [--no-fqdn] [--ignoreschedules] # [-t|--tag ] [--test] # # = Description # # This script can be used to connect to a set of machines running +puppetd+ # and trigger them to run their configurations. The most common usage would # be to specify a class of hosts and a set of tags, and +puppetrun+ would # look up in LDAP all of the hosts matching that class, then connect to # each host and trigger a run of all of the objects with the specified tags. # # If you are not storing your host configurations in LDAP, you can specify # hosts manually. # # You will most likely have to run +puppetrun+ as root to get access to # the SSL certificates. # # +puppetrun+ reads +puppetmaster+'s configuration file, so that it can copy # things like LDAP settings. # # = Usage Notes # # +puppetrun+ is useless unless +puppetd+ is listening. See its documentation # for more information, but the gist is that you must enable +listen+ on the # +puppetd+ daemon, either using +--listen+ on the command line or adding # 'listen: true' in its config file. In addition, you need to set the daemons # up to specifically allow connections by creating the +namespaceauth+ file, # normally at '/etc/puppet/namespaceauth.conf'. This file specifies who has # access to each namespace; if you create the file you must add every namespace # you want any Puppet daemon to allow -- it is currently global to all Puppet # daemons. # -# An example file looks like this: +# An example file looks like this:: # # [fileserver] # allow *.madstop.com # # [puppetmaster] # allow *.madstop.com # # [puppetrunner] # allow culain.madstop.com # # This is what you would install on your Puppet master; non-master hosts could # leave off the 'fileserver' and 'puppetmaster' namespaces. # # Expect more documentation on this eventually. # # = Options # # Note that any configuration parameter that's valid in the configuration file # is also a valid long argument. For example, 'ssldir' is a valid configuration # parameter, so you can specify '--ssldir ' as an argument. # # See the configuration file documentation at # http://reductivelabs.com/projects/puppet/reference/configref.html for # the full list of acceptable parameters. A commented list of all # configuration options can also be generated by running puppetmasterdd with # '--genconfig'. # # # all:: # Connect to all available hosts. Requires LDAP support at this point. # # class:: # Specify a class of machines to which to connect. This only works if you # have LDAP configured, at the moment. # # debug:: # Enable full debugging. # # foreground:: # Run each configuration in the foreground; that is, when connecting to a host, # do not return until the host has finished its run. The default is false. # # help:: # Print this help message # # host:: # A specific host to which to connect. This flag can be specified more # than once. # # ignoreschedules:: # Whether the client should ignore schedules when running its configuration. # This can be used to force the client to perform work it would not normally # perform so soon. The default is false. # # parallel:: # How parallel to make the connections. Parallelization is provided by forking # for each client to which to connect. The default is 1, meaning serial execution. # # tag:: # Specify a tag for selecting the objects to apply. Does not work with the # --test option. # # # test:: # Print the hosts you would connect to but do not actually connect. This # option requires LDAP support at this point. # # = Example # # sudo puppetrun -p 10 --host host1 --host host2 -t remotefile -t webserver # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2005 Reductive Labs, LLC # Licensed under the GNU Public License [:INT, :TERM].each do |signal| trap(signal) do $stderr.puts "Cancelling" exit(1) end end begin require 'rubygems' rescue LoadError # Nothing; we were just doing this just in case end begin require 'ldap' rescue LoadError $stderr.puts "Failed to load ruby LDAP library. LDAP functionality will not be available" end require 'puppet' require 'puppet/network/client' require 'getoptlong' # Look up all nodes matching a given class in LDAP. def ldapnodes(klass, fqdn = true) unless defined? @ldap setupldap() end hosts = [] filter = nil if klass == :all filter = "objectclass=puppetclient" else filter = "puppetclass=#{klass}" end @ldap.search(Puppet[:ldapbase], 2, filter, "cn") do |entry| # Skip the default host entry if entry.dn =~ /cn=default,/ $stderr.puts "Skipping default host entry" next end if fqdn hosts << entry.dn.sub("cn=",'').sub(/ou=hosts,/i, '').gsub(",dc=",".") else hosts << entry.get_values("cn")[0] end end return hosts end def setupldap begin @ldap = Puppet::Parser::Interpreter.ldap() rescue => detail $stderr.puts "Could not connect to LDAP: %s" % detail exit(34) end end flags = [ [ "--all", "-a", GetoptLong::NO_ARGUMENT ], [ "--tag", "-t", GetoptLong::REQUIRED_ARGUMENT ], [ "--class", "-c", GetoptLong::REQUIRED_ARGUMENT ], [ "--foreground", "-f", GetoptLong::NO_ARGUMENT ], [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ], [ "--host", GetoptLong::REQUIRED_ARGUMENT ], [ "--parallel", "-p", GetoptLong::REQUIRED_ARGUMENT ], [ "--no-fqdn", "-n", GetoptLong::NO_ARGUMENT ], [ "--test", GetoptLong::NO_ARGUMENT ], [ "--version", "-V", GetoptLong::NO_ARGUMENT ] ] # Add all of the config parameters as valid options. Puppet.settings.addargs(flags) result = GetoptLong.new(*flags) options = { :ignoreschedules => false, :foreground => false, :parallel => 1, :debug => false, :test => false, :all => false, :verbose => true, :fqdn => true } hosts = [] classes = [] tags = [] Puppet::Util::Log.newdestination(:console) begin result.each { |opt,arg| case opt when "--version" puts "%s" % Puppet.version exit when "--ignoreschedules" options[:ignoreschedules] = true when "--no-fqdn" options[:fqdn] = false when "--all" options[:all] = true when "--test" options[:test] = true when "--tag" tags << arg when "--class" classes << arg when "--host" hosts << arg when "--help" if Puppet.features.usage? RDoc::usage && exit else puts "No help available unless you have RDoc::usage installed" exit end when "--parallel" begin options[:parallel] = Integer(arg) rescue $stderr.puts "Could not convert %s to an integer" % arg.inspect exit(23) end when "--foreground" options[:foreground] = true when "--debug" options[:debug] = true else Puppet.settings.handlearg(opt, arg) end } rescue GetoptLong::InvalidOption => detail $stderr.puts "Try '#{$0} --help'" exit(1) end if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end # Now parse the config config = File.join(Puppet[:confdir], "puppetmasterd.conf") Puppet.parse_config(config) if File.exists? config Puppet.settings.parse(config) end if Puppet[:ldapnodes] if options[:all] hosts = ldapnodes(:all, options[:fqdn]) puts "all: %s" % hosts.join(", ") else classes.each do |klass| list = ldapnodes(klass, options[:fqdn]) puts "%s: %s" % [klass, list.join(", ")] hosts += list end end elsif ! classes.empty? $stderr.puts "You must be using LDAP to specify host classes" exit(24) end if tags.empty? tags = "" else tags = tags.join(",") end children = {} # If we get a signal, then kill all of our children and get out. [:INT, :TERM].each do |signal| trap(signal) do Puppet.notice "Caught #{signal}; shutting down" children.each do |pid, host| Process.kill("INT", pid) end waitall exit(1) end end if options[:test] puts "Skipping execution in test mode" exit(0) end todo = hosts.dup failures = [] # Now do the actual work go = true while go # If we don't have enough children in process and we still have hosts left to # do, then do the next host. if children.length < options[:parallel] and ! todo.empty? host = todo.shift pid = fork do # First make sure the client is up out = %x{ping -c 1 #{host}} unless $? == 0 $stderr.print "Could not contact %s\n" % host next end client = Puppet::Network::Client.runner.new( :Server => host, :Port => Puppet[:puppetport] ) print "Triggering %s\n" % host begin result = client.run(tags, options[:ignoreschedules], options[:foreground]) rescue => detail $stderr.puts "Host %s failed: %s\n" % [host, detail] exit(2) end case result when "success": exit(0) when "running": $stderr.puts "Host %s is already running" % host exit(3) else $stderr.puts "Host %s returned unknown answer '%s'" % [host, result] exit(12) end end children[pid] = host else # Else, see if we can reap a process. begin pid = Process.wait if host = children[pid] # Remove our host from the list of children, so the parallelization # continues working. children.delete(pid) if $?.exitstatus != 0 failures << host end print "%s finished with exit code %s\n" % [host, $?.exitstatus] else $stderr.puts "Could not find host for PID %s with status %s" % [pid, $?.exitstatus] end rescue Errno::ECHILD # There are no children left, so just exit unless there are still # children left to do. next unless todo.empty? if failures.empty? puts "Finished" exit(0) else puts "Failed: %s" % failures.join(", ") exit(3) end end end end diff --git a/bin/ralsh b/bin/ralsh index e43177d35..fdf64916f 100755 --- a/bin/ralsh +++ b/bin/ralsh @@ -1,269 +1,270 @@ #!/usr/bin/ruby # vim: softtabstop=4 shiftwidth=4 expandtab # # = Synopsis # # Use the Puppet RAL to directly interact with the system. # # = Usage # # ralsh [-h|--help] [-d|--debug] [-v|--verbose] [-e|--edit] [-H|--host ] # [-p|--param ] [-t|--types] type # # = Description # # This command provides simple facilities for converting current system state # into Puppet code, along with some ability to use Puppet to affect the current # state. # # By default, you must at least provide a type to list, which case ralsh # will tell you everything it knows about all instances of that type. You can # optionally specify an instance name, and ralsh will only describe that single # instance. # # You can also add +--edit+ as an argument, and ralsh will write its output # to a file, open that file in an editor, and then apply the file as a Puppet # transaction. You can easily use this to use Puppet to make simple changes to # a system. # # = Options # # Note that any configuration parameter that's valid in the configuration file # is also a valid long argument. For example, 'ssldir' is a valid configuration # parameter, so you can specify '--ssldir ' as an argument. # # See the configuration file documentation at # http://reductivelabs.com/projects/puppet/reference/configref.html for # the full list of acceptable parameters. A commented list of all # configuration options can also be generated by running puppet with # '--genconfig'. # # debug:: # Enable full debugging. # # edit: # Write the results of the query to a file, open the file in an editor, # and read the file back in as an executable Puppet manifest. # # host: # When specified, connect to the resource server on the named host # and retrieve the list of resouces of the type specified. # # help: # Print this help message. # # param: # Add more parameters to be outputted from queries. # # types: # List all available types. # # verbose:: # Print extra information. # # = Example # -# $ ralsh user luke -# user { 'luke': -# home => '/home/luke', -# uid => '100', -# ensure => 'present', -# comment => 'Luke Kanies,,,', -# gid => '1000', -# shell => '/bin/bash', -# groups => ['sysadmin','audio','video','puppet'] -# } -# $ +# This example uses ``ralsh`` to return Puppet configuration for the user ``luke``:: +# +# $ ralsh user luke +# user { 'luke': +# home => '/home/luke', +# uid => '100', +# ensure => 'present', +# comment => 'Luke Kanies,,,', +# gid => '1000', +# shell => '/bin/bash', +# groups => ['sysadmin','audio','video','puppet'] +# } # # = Author # # Luke Kanies # # = Copyright # # Copyright (c) 2005-2007 Reductive Labs, LLC # Licensed under the GNU Public License require 'getoptlong' require 'puppet' options = [ [ "--debug", "-d", GetoptLong::NO_ARGUMENT ], [ "--verbose", "-v", GetoptLong::NO_ARGUMENT ], [ "--types", "-t", GetoptLong::NO_ARGUMENT ], [ "--param", "-p", GetoptLong::REQUIRED_ARGUMENT ], [ "--host", "-H", GetoptLong::REQUIRED_ARGUMENT ], [ "--edit", "-e", GetoptLong::NO_ARGUMENT ], [ "--help", "-h", GetoptLong::NO_ARGUMENT ] ] # Add all of the config parameters as valid options. Puppet.settings.addargs(options) result = GetoptLong.new(*options) debug = false verbose = false edit = false extra_params = [] host = nil result.each { |opt,arg| case opt when "--host" host = arg when "--types" types = [] Puppet::Type.loadall Puppet::Type.eachtype do |t| next if t.name == :component types << t.name.to_s end puts types.sort exit when "--param" extra_params << arg.to_sym when "--edit" edit = true when "--help" if Puppet.features.usage? RDoc::usage else puts "install RDoc:usage for help" end exit when "--verbose" verbose = true when "--debug" debug = true else # Anything else is handled by the config stuff Puppet.settings.handlearg(opt, arg) end } Puppet::Util::Log.newdestination(:console) # Now parse the config Puppet.parse_config if debug Puppet::Util::Log.level = :debug elsif verbose Puppet::Util::Log.level = :info end if ARGV.length > 0 type = ARGV.shift else raise "You must specify the type to display" end name = nil params = {} if ARGV.length > 0 name = ARGV.shift end if ARGV.length > 0 ARGV.each do |setting| if setting =~ /^(\w+)=(.+)$/ params[$1] = $2 else raise "Invalid parameter setting %s" % setting end end end if edit and host raise "You cannot edit a remote host" end typeobj = nil unless typeobj = Puppet::Type.type(type) raise "Could not find type %s" % type end properties = typeobj.properties.collect { |s| s.name } format = proc {|trans| trans.dup.collect do |param, value| if value == "" or value == [] trans.delete(param) end unless properties.include?(param) or extra_params.include?(param) trans.delete(param) end end trans.to_manifest } text = if host client = Puppet::Network::Client.resource.new(:Server => host, :Port => Puppet[:puppetport]) unless client.read_cert raise "client.read_cert failed" end begin # They asked for a single resource. if name transbucket = [client.describe(type, name)] else # Else, list the whole thing out. transbucket = client.instances(type) end rescue Puppet::Network::XMLRPCClientError => exc raise "client.list(#{type}) failed: #{exc.message}" end transbucket.sort { |a,b| a.name <=> b.name }.collect(&format) else if name obj = typeobj.create(:name => name, :check => properties) vals = obj.retrieve unless params.empty? params.each do |param, value| obj[param] = value end catalog = Puppet::Node::Catalog.new catalog.add_resource obj begin catalog.apply rescue => detail if Puppet[:trace] puts detail.backtrace end end end [format.call(obj.to_trans(true))] else typeobj.instances.collect do |obj| next if ARGV.length > 0 and ! ARGV.include? obj.name trans = obj.to_trans(true) format.call(trans) end end end.compact.join("\n") if edit file = "/tmp/x2puppet-#{Process.pid}.pp" begin File.open(file, "w") do |f| f.puts text end ENV["EDITOR"] ||= "vi" system(ENV["EDITOR"], file) system("puppet -v " + file) ensure #if FileTest.exists? file # File.unlink(file) #end end else puts text end diff --git a/lib/puppet/reports/tagmail.rb b/lib/puppet/reports/tagmail.rb index a2c973f8f..9b3711148 100644 --- a/lib/puppet/reports/tagmail.rb +++ b/lib/puppet/reports/tagmail.rb @@ -1,169 +1,180 @@ require 'puppet' require 'pp' require 'net/smtp' Puppet::Reports.register_report(:tagmail) do desc "This report sends specific log messages to specific email addresses based on the tags in the log messages. See the `UsingTags tag documentation`:trac: for more information on tags. To use this report, you must create a ``tagmail.conf`` (in the location specified by ``tagmap``). This is a simple file that maps tags to email addresses: Any log messages in the report that match the specified tags will be sent to the specified email addresses. Tags must be comma-separated, and they can be negated so that messages only match when they do not have that tag. The tags are separated from the email addresses by a colon, and the email addresses should also be comma-separated. Lastly, there is an ``all`` tag that will always match all log messages. Here is an example tagmail.conf:: all: me@domain.com webserver, !mailserver: httpadmins@domain.com This will send all messages to ``me@domain.com``, and all messages from webservers that are not also from mailservers to ``httpadmins@domain.com``. + + If you are using anti-spam controls, such as grey-listing, on your mail + server you should whitelist the sending email (controlled by ``reportform`` + configuration option) to ensure your email is not discarded as spam. " Puppet.settings.use(:tagmail) # Find all matching messages. def match(taglists) reports = [] taglists.each do |emails, pos, neg| # First find all of the messages matched by our positive tags messages = nil if pos.include?("all") messages = self.logs else # Find all of the messages that are tagged with any of our # tags. messages = self.logs.find_all do |log| pos.detect { |tag| log.tagged?(tag) } end end # Now go through and remove any messages that match our negative tags messages = messages.reject do |log| if neg.detect do |tag| log.tagged?(tag) end true end end if messages.empty? Puppet.info "No messages to report to %s" % emails.join(",") next else reports << [emails, messages.collect { |m| m.to_report }.join("\n")] end end return reports end # Load the config file def parse(text) taglists = [] text.split("\n").each do |line| taglist = emails = nil case line.chomp when /^\s*#/: next when /^\s*$/: next when /^\s*(.+)\s*:\s*(.+)\s*$/: taglist = $1 emails = $2.sub(/#.*$/,'') else raise ArgumentError, "Invalid tagmail config file" end pos = [] neg = [] taglist.sub(/\s+$/,'').split(/\s*,\s*/).each do |tag| unless tag =~ /^!?[-\w]+$/ raise ArgumentError, "Invalid tag %s" % tag.inspect end case tag when /^\w+/: pos << tag when /^!\w+/: neg << tag.sub("!", '') else raise Puppet::Error, "Invalid tag '%s'" % tag end end # Now split the emails emails = emails.sub(/\s+$/,'').split(/\s*,\s*/) taglists << [emails, pos, neg] end return taglists end # Process the report. This just calls the other associated messages. def process unless FileTest.exists?(Puppet[:tagmap]) Puppet.notice "Cannot send tagmail report; no tagmap file %s" % Puppet[:tagmap] return end taglists = parse(File.read(Puppet[:tagmap])) # Now find any appropriately tagged messages. reports = match(taglists) send(reports) end # Send the email reports. def send(reports) pid = fork do if Puppet[:smtpserver] != "none" begin Net::SMTP.start(Puppet[:smtpserver]) do |smtp| reports.each do |emails, messages| Puppet.info "Sending report to %s" % emails.join(", ") - smtp.send_message(messages, Puppet[:reportfrom], *emails) + smtp.open_message_stream(Puppet[:reportfrom], *emails) do |p| + p.puts "From: #{Puppet[:reportfrom]}" + p.puts "Subject: Puppet Report for %s" % self.host + p.puts "To: " + emails.join(", ") + p.puts "Date: " + Time.now.rfc2822 + p.puts + p.puts messages + end end end rescue => detail if Puppet[:debug] puts detail.backtrace end raise Puppet::Error, "Could not send report emails through smtp: %s" % detail end elsif Puppet[:sendmail] != "" begin reports.each do |emails, messages| Puppet.info "Sending report to %s" % emails.join(", ") # We need to open a separate process for every set of email addresses IO.popen(Puppet[:sendmail] + " " + emails.join(" "), "w") do |p| p.puts "From: #{Puppet[:reportfrom]}" p.puts "Subject: Puppet Report for %s" % self.host p.puts "To: " + emails.join(", ") p.puts messages end end rescue => detail if Puppet[:debug] puts detail.backtrace end raise Puppet::Error, "Could not send report emails via sendmail: %s" % detail end else raise Puppet::Error, "SMTP server is unset and could not find sendmail" end end # Don't bother waiting for the pid to return. Process.detach(pid) end end